1. 为什么选择Flutter开发Web项目Flutter作为Google推出的跨平台UI工具包最初专注于移动端开发但近年来在Web领域的表现越来越成熟。我最初接触Flutter Web是在2020年当时它还处于beta阶段性能问题比较明显。但经过几个大版本的迭代特别是Flutter 3.0之后Web支持已经达到了生产可用的水平。与传统Web开发技术栈如React、Vue相比Flutter Web最大的优势在于完全一致的UI体验一套代码在移动端和Web端呈现完全相同的视觉效果开发效率提升Dart语言的强类型特性减少了运行时错误热重载功能保存代码后立即看到变化无需手动刷新浏览器丰富的组件库Material和Cupertino风格的组件开箱即用不过需要注意的是Flutter Web更适合应用型网站如后台管理系统、工具类网站对于需要SEO优化的内容型网站可能不是最佳选择。这是因为Flutter Web默认采用Canvas渲染对搜索引擎不太友好。2. 环境准备与项目创建2.1 安装Flutter SDK首先需要确保你的开发环境已经安装了Flutter SDK。如果还没有安装可以按照以下步骤操作访问Flutter官网下载最新稳定版的SDK解压到本地目录建议不要放在Program Files等需要管理员权限的目录将flutter/bin目录添加到系统PATH环境变量验证安装是否成功flutter doctor这个命令会检查你的开发环境并给出改进建议。对于Web开发你需要确保Chrome和Web Server两个检查项都显示为可用状态。提示如果遇到initializing the flutter sdk. this could take a few minutes卡住的情况可能是网络问题导致无法下载依赖。可以尝试设置国内镜像export PUB_HOSTED_URLhttps://pub.flutter-io.cn export FLUTTER_STORAGE_BASE_URLhttps://storage.flutter-io.cn2.2 创建Flutter Web项目在VS Code中创建Flutter Web项目非常简单打开VS Code确保已安装Flutter和Dart扩展按CtrlShiftP打开命令面板输入Flutter: New Project并回车选择项目类型为Application输入项目名称如my_web_app选择项目保存位置等待项目初始化完成初始化完成后你会看到一个标准的Flutter项目结构。与移动端项目不同的是Web项目多了一个web目录里面包含了index.html等Web特有的文件。2.3 启用Web支持如果你的Flutter项目还没有启用Web支持需要先运行flutter create --platforms web .这个命令会在现有项目中添加Web支持。完成后你可以通过以下命令启动开发服务器flutter run -d chrome这将启动一个本地开发服务器并在Chrome浏览器中打开你的应用。3. VS Code插件推荐与配置3.1 必备Flutter开发插件Flutter官方插件提供项目创建、运行、调试等核心功能Dart提供Dart语言支持包括代码补全、语法高亮等Error Lens直接在代码行内显示错误和警告提高调试效率Pubspec Assist快速添加依赖到pubspec.yaml文件3.2 提高开发效率的插件Better Comments为不同类型的注释添加颜色区分Bracket Pair Colorizer彩色显示匹配的括号避免嵌套混乱GitLens增强Git功能方便查看代码修改历史Live Server快速启动本地Web服务器虽然Flutter自带但这个更轻量3.3 代码风格与质量插件Dart Data Class Generator快速生成数据类模板代码Flutter Widget Snippets提供常用Widget的代码片段Bloc如果你使用BLoC模式这个插件能极大提高开发效率Dart Import Sorter自动整理import语句3.4 调试技巧在VS Code中调试Flutter Web应用时有几点需要注意确保选择了正确的启动配置在.vscode/launch.json中使用Debugger for Chrome扩展可以启用更强大的调试功能在调试控制台输入flutter.debugExtension可以查看扩展调试信息注意如果遇到there was an error while deleting a directory这类权限问题可能是因为VS Code进程没有完全退出。可以尝试完全关闭VS Code后重新打开。4. 项目结构与核心文件解析4.1 Flutter Web项目结构一个典型的Flutter Web项目包含以下重要目录和文件my_web_app/ ├── lib/ # Dart源代码 │ └── main.dart # 应用入口文件 ├── web/ # Web特有文件 │ ├── index.html # 主HTML文件 │ └── favicon.png # 网站图标 ├── pubspec.yaml # 项目配置和依赖 └── build/ # 构建输出自动生成4.2 main.dart解析这是Flutter应用的入口文件基本结构如下import package:flutter/material.dart; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); override Widget build(BuildContext context) { return MaterialApp( title: Flutter Web Demo, theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } }4.3 index.html解析web/index.html是Web应用的入口HTML文件Flutter会在这个文件中初始化!DOCTYPE html html head meta charsetUTF-8 titleMy Web App/title /head body script srcmain.dart.js typeapplication/javascript/script /body /html4.4 pubspec.yaml配置这个文件定义了项目元数据和依赖关系name: my_web_app description: A Flutter Web application version: 1.0.01 environment: sdk: 3.0.0 4.0.0 dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.2 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.05. 构建与部署实战5.1 开发环境运行在开发过程中使用以下命令启动开发服务器flutter run -d chrome这会启动一个热重载服务器代码修改后会立即反映在浏览器中。5.2 生产环境构建准备部署到生产环境时需要先构建优化版本flutter build web构建完成后产物会放在build/web目录下。你可以将这个目录下的所有文件部署到任何Web服务器上。5.3 性能优化建议启用CanvasKit渲染在web/index.html中添加以下代码script window.flutterWebRenderer canvaskit; /script减小初始加载体积flutter build web --web-renderer canvaskit --release --dart-defineFLUTTER_WEB_CANVASKIT_URL/canvaskit/使用代码分割在pubspec.yaml中添加flutter: deferred-components: - name: my_feature libraries: - package:my_app/my_feature.dart5.4 常见部署问题解决路由问题如果使用Flutter的路由系统需要确保服务器配置了URL重写将所有请求指向index.html资源加载404检查构建后的文件路径是否正确特别是使用了相对路径的情况CORS问题如果请求API遇到跨域问题需要在服务器端配置CORS头或者在开发时使用代理6. 进阶技巧与最佳实践6.1 响应式设计Flutter Web应用需要考虑不同屏幕尺寸。可以使用LayoutBuilder和MediaQuery来实现响应式布局LayoutBuilder( builder: (context, constraints) { if (constraints.maxWidth 600) { return DesktopLayout(); } else { return MobileLayout(); } }, )6.2 状态管理选择对于Web应用推荐使用以下状态管理方案Provider适合中小型应用RiverpodProvider的改进版更灵活BLoC适合复杂业务逻辑GetX轻量级但功能全面6.3 与JavaScript互操作通过dart:js库可以实现与JavaScript的互操作import dart:js as js; void callJavaScriptFunction() { js.context.callMethod(alert, [Hello from Dart!]); }6.4 性能监控使用Flutter的DevTools可以监控Web应用的性能启动DevToolsflutter pub global activate devtools flutter pub global run devtools在应用中启用性能覆盖层void main() { debugPaintSizeEnabled true; // 显示布局边界 runApp(const MyApp()); }7. 调试与问题排查7.1 常见错误解决You are applying Flutters main Gradle plugin imperatively解决方案更新Flutter SDK和所有依赖VS Code商店搜不出来Error while fetching extensions检查网络连接尝试更换VS Code的扩展市场镜像Flutter SDK初始化卡住设置国内镜像检查防火墙设置7.2 Chrome开发者工具技巧查看Flutter Widget树在Chrome开发者工具中切换到Flutter选项卡网络请求分析使用Network选项卡监控所有HTTP请求性能分析使用Performance选项卡记录并分析运行时性能7.3 日志收集在Web环境中可以使用以下方式输出日志import dart:developer as developer; void someFunction() { developer.log(Log message, name: my.app.category); }日志可以在Chrome开发者工具的Console选项卡中查看。8. 项目实战创建一个简单的博客网站让我们通过一个实际例子来巩固所学知识。我们将创建一个简单的博客网站包含以下功能文章列表文章详情页响应式布局暗黑模式支持8.1 项目初始化按照前面介绍的方法创建一个新的Flutter Web项目命名为flutter_blog。8.2 添加依赖修改pubspec.yaml添加所需依赖dependencies: flutter: sdk: flutter http: ^0.13.5 provider: ^6.0.5然后运行flutter pub get8.3 实现数据模型创建lib/models/post.dartclass Post { final int id; final String title; final String body; Post({required this.id, required this.title, required this.body}); factory Post.fromJson(MapString, dynamic json) { return Post( id: json[id], title: json[title], body: json[body], ); } }8.4 实现API服务创建lib/services/api_service.dartimport dart:convert; import package:http/http.dart as http; import ../models/post.dart; class ApiService { static const String baseUrl https://jsonplaceholder.typicode.com; FutureListPost getPosts() async { final response await http.get(Uri.parse($baseUrl/posts)); if (response.statusCode 200) { Listdynamic body jsonDecode(response.body); return body.map((dynamic item) Post.fromJson(item)).toList(); } else { throw Exception(Failed to load posts); } } }8.5 实现UI界面更新lib/main.dartimport package:flutter/material.dart; import package:provider/provider.dart; import models/post.dart; import services/api_service.dart; void main() { runApp( MultiProvider( providers: [ Provider(create: (_) ApiService()), ], child: const MyApp(), ), ); } class MyApp extends StatelessWidget { const MyApp({super.key}); override Widget build(BuildContext context) { return MaterialApp( title: Flutter Blog, theme: ThemeData.light(), darkTheme: ThemeData.dark(), home: const PostListScreen(), ); } } class PostListScreen extends StatefulWidget { const PostListScreen({super.key}); override StatePostListScreen createState() _PostListScreenState(); } class _PostListScreenState extends StatePostListScreen { late FutureListPost futurePosts; override void initState() { super.initState(); futurePosts Provider.ofApiService(context, listen: false).getPosts(); } override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text(Blog Posts)), body: FutureBuilderListPost( future: futurePosts, builder: (context, snapshot) { if (snapshot.hasData) { return ListView.builder( itemCount: snapshot.data!.length, itemBuilder: (context, index) { Post post snapshot.data![index]; return ListTile( title: Text(post.title), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) PostDetailScreen(post: post), ), ); }, ); }, ); } else if (snapshot.hasError) { return Center(child: Text(${snapshot.error})); } return const Center(child: CircularProgressIndicator()); }, ), ); } } class PostDetailScreen extends StatelessWidget { final Post post; const PostDetailScreen({super.key, required this.post}); override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(post.title)), body: Padding( padding: const EdgeInsets.all(16.0), child: Text(post.body), ), ); } }8.6 添加响应式支持更新PostListScreen的build方法override Widget build(BuildContext context) { final isDesktop MediaQuery.of(context).size.width 600; return Scaffold( appBar: AppBar(title: const Text(Blog Posts)), body: FutureBuilderListPost( future: futurePosts, builder: (context, snapshot) { if (snapshot.hasData) { if (isDesktop) { // 桌面端布局 return GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, childAspectRatio: 1.5, ), itemCount: snapshot.data!.length, itemBuilder: (context, index) { Post post snapshot.data![index]; return Card( child: ListTile( title: Text(post.title), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) PostDetailScreen(post: post), ), ); }, ), ); }, ); } else { // 移动端布局 return ListView.builder( itemCount: snapshot.data!.length, itemBuilder: (context, index) { Post post snapshot.data![index]; return ListTile( title: Text(post.title), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) PostDetailScreen(post: post), ), ); }, ); }, ); } } else if (snapshot.hasError) { return Center(child: Text(${snapshot.error})); } return const Center(child: CircularProgressIndicator()); }, ), ); }8.7 运行与测试使用以下命令运行项目flutter run -d chrome在浏览器中打开应用你应该能看到从API获取的博客文章列表。点击文章可以查看详情。调整浏览器窗口大小可以看到布局在桌面和移动视图之间的切换。9. 项目优化与扩展9.1 添加缓存支持为了提高性能我们可以添加缓存机制。修改ApiServiceclass ApiService { static const String baseUrl https://jsonplaceholder.typicode.com; final MapString, dynamic _cache {}; FutureListPost getPosts() async { if (_cache.containsKey(posts)) { return _cache[posts]; } final response await http.get(Uri.parse($baseUrl/posts)); if (response.statusCode 200) { Listdynamic body jsonDecode(response.body); ListPost posts body.map((dynamic item) Post.fromJson(item)).toList(); _cache[posts] posts; return posts; } else { throw Exception(Failed to load posts); } } }9.2 实现离线支持通过添加hive缓存实现离线支持添加依赖dependencies: hive: ^2.2.3 hive_flutter: ^1.1.0初始化Hivevoid main() async { await Hive.initFlutter(); Hive.registerAdapter(PostAdapter()); runApp( MultiProvider( providers: [ Provider(create: (_) ApiService()), ], child: const MyApp(), ), ); }创建PostAdapterclass PostAdapter extends TypeAdapterPost { override final int typeId 0; override Post read(BinaryReader reader) { return Post( id: reader.read(), title: reader.read(), body: reader.read(), ); } override void write(BinaryWriter writer, Post obj) { writer.write(obj.id); writer.write(obj.title); writer.write(obj.body); } }修改ApiService使用Hive缓存FutureListPost getPosts() async { final box await Hive.openBoxPost(posts); if (box.isNotEmpty) { return box.values.toList(); } final response await http.get(Uri.parse($baseUrl/posts)); if (response.statusCode 200) { Listdynamic body jsonDecode(response.body); ListPost posts body.map((dynamic item) Post.fromJson(item)).toList(); await box.addAll(posts); return posts; } else { throw Exception(Failed to load posts); } }9.3 添加搜索功能在PostListScreen中添加搜索功能class _PostListScreenState extends StatePostListScreen { late FutureListPost futurePosts; ListPost filteredPosts []; final TextEditingController _searchController TextEditingController(); override void initState() { super.initState(); futurePosts Provider.ofApiService(context, listen: false) .getPosts() .then((posts) { filteredPosts posts; return posts; }); } void _filterPosts(String query) { futurePosts.then((posts) { setState(() { filteredPosts posts .where((post) post.title.toLowerCase().contains(query.toLowerCase())) .toList(); }); }); } override Widget build(BuildContext context) { final isDesktop MediaQuery.of(context).size.width 600; return Scaffold( appBar: AppBar( title: const Text(Blog Posts), actions: [ IconButton( icon: const Icon(Icons.search), onPressed: () { showSearch( context: context, delegate: PostSearchDelegate(futurePosts), ); }, ), ], ), body: Column( children: [ Padding( padding: const EdgeInsets.all(8.0), child: TextField( controller: _searchController, decoration: InputDecoration( labelText: Search, suffixIcon: IconButton( icon: const Icon(Icons.clear), onPressed: () { _searchController.clear(); _filterPosts(); }, ), ), onChanged: _filterPosts, ), ), Expanded( child: FutureBuilderListPost( future: futurePosts, builder: (context, snapshot) { if (snapshot.hasData) { return ListView.builder( itemCount: filteredPosts.length, itemBuilder: (context, index) { Post post filteredPosts[index]; return ListTile( title: Text(post.title), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) PostDetailScreen(post: post), ), ); }, ); }, ); } else if (snapshot.hasError) { return Center(child: Text(${snapshot.error})); } return const Center(child: CircularProgressIndicator()); }, ), ), ], ), ); } } class PostSearchDelegate extends SearchDelegatePost? { final FutureListPost futurePosts; PostSearchDelegate(this.futurePosts); override ListWidget buildActions(BuildContext context) { return [ IconButton( icon: const Icon(Icons.clear), onPressed: () { query ; }, ), ]; } override Widget buildLeading(BuildContext context) { return IconButton( icon: const Icon(Icons.arrow_back), onPressed: () { close(context, null); }, ); } override Widget buildResults(BuildContext context) { return FutureBuilderListPost( future: futurePosts, builder: (context, snapshot) { if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator()); } final results snapshot.data! .where((post) post.title.toLowerCase().contains(query.toLowerCase())) .toList(); return ListView.builder( itemCount: results.length, itemBuilder: (context, index) { return ListTile( title: Text(results[index].title), onTap: () { close(context, results[index]); }, ); }, ); }, ); } override Widget buildSuggestions(BuildContext context) { return buildResults(context); } }9.4 添加暗黑模式切换更新MyApp widget以支持主题切换class MyApp extends StatefulWidget { const MyApp({super.key}); override StateMyApp createState() _MyAppState(); } class _MyAppState extends StateMyApp { ThemeMode _themeMode ThemeMode.system; override Widget build(BuildContext context) { return MaterialApp( title: Flutter Blog, theme: ThemeData.light(), darkTheme: ThemeData.dark(), themeMode: _themeMode, home: Builder( builder: (context) { return Scaffold( appBar: AppBar( title: const Text(Flutter Blog), actions: [ IconButton( icon: Icon( _themeMode ThemeMode.dark ? Icons.light_mode : Icons.dark_mode, ), onPressed: () { setState(() { _themeMode _themeMode ThemeMode.dark ? ThemeMode.light : ThemeMode.dark; }); }, ), ], ), body: const PostListScreen(), ); }, ), ); } }10. 项目部署与持续集成10.1 构建生产版本运行以下命令构建优化后的生产版本flutter build web --release构建完成后所有静态文件会生成在build/web目录下。10.2 部署到GitHub Pages在项目根目录创建docs目录将build/web目录下的所有文件复制到docs目录在GitHub仓库设置中启用GitHub Pages选择docs目录作为源访问https://[your-username].github.io/[repository-name] 查看部署结果10.3 部署到Firebase Hosting安装Firebase CLInpm install -g firebase-tools登录Firebasefirebase login初始化项目firebase init hosting选择现有项目或创建新项目设置public目录为build/web部署flutter build web --release firebase deploy10.4 设置持续集成可以在GitHub Actions中添加自动化构建和部署流程。创建.github/workflows/deploy.ymlname: Deploy to Firebase on: push: branches: [ main ] jobs: build_and_deploy: runs-on: ubuntu-latest steps: - uses: actions/checkoutv2 - uses: subosito/flutter-actionv1 with: channel: stable - run: flutter pub get - run: flutter build web --release - uses: FirebaseExtended/action-hosting-deployv0 with: repoToken: ${{ secrets.GITHUB_TOKEN }} firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }} channelId: live projectId: your-project-id11. 性能优化进阶11.1 代码分割与懒加载对于大型应用可以使用代码分割减少初始加载体积在pubspec.yaml中定义延迟加载组件flutter: deferred-components: - name: postDetail libraries: - package:flutter_blog/screens/post_detail.dart修改路由使用延迟加载onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) FutureBuilder( future: DeferredLibrary.loadLibrary(), builder: (context, snapshot) { if (snapshot.connectionState ConnectionState.done) { return PostDetailScreen(post: post); } return const CircularProgressIndicator(); }, ), ), ); },11.2 图片优化使用webp格式图片实现懒加载Image.network( https://example.com/image.webp, frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { if (wasSynchronouslyLoaded) { return child; } return AnimatedOpacity( child: child, opacity: frame null ? 0 : 1, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); }, )11.3 预加载关键资源在web/index.html中添加预加载link relpreload hrefmain.dart.js asscript link relpreload hrefassets/fonts/MaterialIcons-Regular.otf asfont crossorigin11.4 使用Service Worker缓存创建web/sw.jsconst CACHE_NAME flutter-web-cache; const urlsToCache [ /, /main.dart.js, /assets/fonts/MaterialIcons-Regular.otf, ]; self.addEventListener(install, (event) { event.waitUntil( caches.open(CACHE_NAME) .then((cache) cache.addAll(urlsToCache)) ); }); self.addEventListener(fetch, (event) { event.respondWith( caches.match(event.request) .then((response) response || fetch(event.request)) ); });在web/index.html中注册script if (serviceWorker in navigator) { window.addEventListener(load, () { navigator.serviceWorker.register(/sw.js); }); } /script12. 安全最佳实践12.1 防止HTTP抓包使用HTTPS加密所有通信添加安全头在web/index.html中添加meta http-equivContent-Security-Policy contentupgrade-insecure-requests在Flutter中禁用HTTP明文传输void main() { HttpClient httpClient HttpClient(); httpClient.badCertificateCallback (cert, host, port) false; runApp(const MyApp()); }12.2 输入验证与清理对所有用户输入进行验证final sanitizer HtmlEscape(); String safeInput sanitizer.convert(userInput);12.3 认证与授权使用Firebase Authentication或其他OAuth提供商实现安全认证import package:firebase_auth/firebase_auth.dart; FutureUserCredential signInWithEmailAndPassword( String email, String password, ) async { try { return await FirebaseAuth.instance.signInWithEmailAndPassword( email: email, password: password, ); } on FirebaseAuthException catch (e) { throw AuthException(e.message ?? Authentication failed); } }12.4 敏感数据保护不要在前端存储敏感信息使用flutter_secure_storage存储本地敏感数据dependencies: flutter_secure_storage: ^5.0.2使用示例final storage FlutterSecureStorage(); await storage.write(key: token, value: sensitive-data); String? token await storage.read(key: token);13. 测试策略13.1 单元测试创建测试文件test/post_test.dartimport package:flutter_test/flutter_test.dart; import package:flutter_blog/models/post.dart; void main() { group(Post Model, () { test(fromJson creates Post with correct values, () { final json { id: 1, title: Test Title, body: Test Body, }; final post Post.fromJson(json); expect(post.id, 1); expect(post.title, Test Title); expect(post.body, Test Body); }); }); }运行测试flutter test13.2 Widget测试创建测试文件test/post_list_test.dartimport package:flutter/material.dart; import package:flutter_test/flutter_test.dart; import package:flutter_blog/main.dart; void main() { testWidgets(PostListScreen displays loading indicator, (tester) async { await tester.pumpWidget(const MyApp()); expect(find.byType(CircularProgressIndicator), findsOneWidget); }); }13.3 集成测试创建测试文件integration_test/app_test.dartimport package:flutter_test/flutter_test.dart; import package:integration_test/integration_test.dart; import package:flutter_blog/main.dart as app; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group(end-to-end test, () { testWidgets(tap on post item, (tester) async { app.main(); await tester.pumpAndSettle(); await tester.tap(find.byType(ListTile).first); await tester.pumpAndSettle(); expect(find.byType(PostDetailScreen), findsOneWidget); }); }); }运行集成测试flutter test integration_test13.4 性能测试使用Flutter Driver进行性能测试import package:flutter_driver/flutter_driver.dart; import package:test/test.dart; void main() { FlutterDriver driver; setUpAll(() async { driver await FlutterDriver.connect(); }); tearDownAll(() async { if (driver ! null) { driver.close(); } }); test(scroll performance, () async { final timeline await driver.traceAction(() async { await driver.scroll( find.byType(ListView), 0.0, -300.0, const Duration(milliseconds: 300), ); }); final summary TimelineSummary.summarize(timeline); await summary.writeTimelineToFile(scroll_performance, pretty: true); }); }14. 国际化支持14.1 添加依赖在pubspec.yaml中添加dependencies: flutter_localizations: sdk: flutter intl: ^0.18.1运行flutter pub get14.2 创建arb文件创建lib/l10n/intl_en.arb{ locale: en, appTitle: Flutter Blog, posts: Posts, search: Search, darkMode: Dark Mode, lightMode: Light Mode }创建lib/l10n/intl_zh.arb{ locale: zh, appTitle: Flutter博客, posts: 文章列表, search: 搜索, darkMode: 暗黑模式, lightMode: 明亮模式 }14.3 生成本地化类运行flutter pub run intl_utils:generate这会生成lib/generated/l10n.dart文件。14.4 使用本地化更新MyAppimport package:flutter_localizations/flutter_localizations.dart; import generated/l10n.dart; class MyApp extends StatelessWidget { const MyApp({super.key}); override Widget build(BuildContext context) { return MaterialApp( title: Flutter Blog, localizationsDelegates: const [ S.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], supportedLocales: S.delegate.supportedLocales, home: const PostListScreen(), ); } }在界面中使用Text(S.of(context).appTitle),14.5 语言切换添加语言切换功能class LanguageSwitcher extends