Flutter实现多语言切换

Flutter实现多语言切换

适用于flutter App 切换多语言场景:应用内一键切换中/英文,切换结果持久化保存,重启 App 后依然生效;同时附带主题模式切换作为全局状态管理的完整示例。

一、前言

公司割草机项目使用 Flutter 开发,需要在 App 内支持中英文切换(面向欧洲、美国、澳洲等海外市场)。本文基于一个「商城 + 设备 + 购物车 + 我的」四 Tab Demo,完整讲解 Flutter 多语言切换的实现方式,包括:

  • 自定义多语言文案资源与 LocalizationsDelegate;

  • 全局状态管理(语言、主题)与 SharedPreferences 持久化;

  • 在 MaterialApp 中注册本地化代理与支持语言列表;

  • 排查并解决「切换语言后界面不刷新」的典型问题。

二、项目目录结构

标准 Flutter 工程结构,多语言相关代码集中在 lib/main.dart:

复制代码
flutter-setttings/
├── android/
├── ios/
├── lib/
│   └── main.dart                  # 多语言、状态管理、页面与入口全部在此文件
├── linux/
├── macos/
├── test/
├── web/
├── windows/
├── .gitignore
├── analysis_options.yaml
├── LICENSE
├── pubspec.lock
├── pubspec.yaml
└── README.md

三、依赖配置

编辑 pubspec.yaml,引入 flutter_localizations(官方组件本地化)与 shared_preferences(语言/主题持久化):

复制代码
name: flutter_settings
description: A Flutter demo with theme and in-app language switching.
publish_to: 'none'
version: 1.0.0+1


environment:
  sdk: ^3.0.0


dependencies:
  flutter:
    sdk: flutter
  # 官方组件本地化(按钮、日期、选择框等内置文案)
  flutter_localizations:
    sdk: flutter
  # 本地持久化:保存用户选择的语言与主题
  shared_preferences: ^2.2.2


dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0


flutter:
  uses-material-design: true

执行依赖安装:

复制代码
flutter pub get

四、整体实现思路

整体链路如下:

  1. 用静态 Map 维护中文、英文两套文案,通过 AppLocalizations 对外提供 get(key) 取值;

  2. 自定义 AppLocalizationsDelegate,交给 Flutter 本地化框架在当前 Locale 下加载对应的 AppLocalizations 实例;

  3. AppState 继承 ChangeNotifier,集中管理 ThemeMode 与 Locale,变更时写入 SharedPreferences 并通知监听者;

  4. 手写轻量 ChangeNotifierProvider(基于 InheritedWidget)在树根注入 AppState,Consumer 包裹 MaterialApp,状态变化时重建 App 根节点;

  5. MaterialApp 通过 locale、supportedLocales、localizationsDelegates 完成本地化注册;

  6. 页面中统一通过 AppLocalizations.of(context).get('key') 获取文案,切换语言后随根节点重建而刷新。

五、核心代码实现

5.1 多语言文案资源 AppLocalizations

AppLocalizations 持有当前 Locale,内部维护语言码 → 文案 key → 文案内容 的两层 Map;of(context) 从本地化容器中取出当前实例:

复制代码
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:shared_preferences/shared_preferences.dart';


// ---------------------- 多语言文本 ----------------------
class AppLocalizations {
  AppLocalizations(this.locale);


  final Locale locale;


  static AppLocalizations of(BuildContext context) {
    return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
  }


  static const Map<String, Map<String, String>> _localizedValues = {
    'zh': {
      'tab_home': '商城',
      'tab_device': '设备',
      'tab_cart': '购物车',
      'tab_mine': '我的',
      'theme_title': '主题设置',
      'theme_light': '亮色模式',
      'theme_dark': '暗色模式',
      'theme_system': '跟随系统',
      'lang_title': '语言设置',
      'lang_zh': '中文',
      'lang_en': 'English',
      'home_page': '商城页面',
      'device_page': '设备页面',
      'cart_page': '购物车页面',
      'mine_page': '个人中心',
    },
    'en': {
      'tab_home': 'Shop',
      'tab_device': 'Device',
      'tab_cart': 'Cart',
      'tab_mine': 'Mine',
      'theme_title': 'Theme Setting',
      'theme_light': 'Light',
      'theme_dark': 'Dark',
      'theme_system': 'System Default',
      'lang_title': 'Language',
      'lang_zh': 'Chinese',
      'lang_en': 'English',
      'home_page': 'Shop Page',
      'device_page': 'Device Page',
      'cart_page': 'Cart Page',
      'mine_page': 'Mine Page',
    },
  };


  String get(String key) {
    return _localizedValues[locale.languageCode]![key]!;
  }
}

5.2 本地化代理 AppLocalizationsDelegate

LocalizationsDelegate 负责告诉 Flutter 框架:支持哪些语言、如何加载本地化实例、语言资源是否需要重新加载:

复制代码
class AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
  const AppLocalizationsDelegate();


  // 当前工程支持的语言列表
  @override
  bool isSupported(Locale locale) => ['zh', 'en'].contains(locale.languageCode);


  // 加载当前语言对应的本地化实例(本示例文案为内存静态资源,无需异步加载文件)
  @override
  Future<AppLocalizations> load(Locale locale) async {
    return AppLocalizations(locale);
  }


  // 资源无变化,重建时不重新加载
  @override
  bool shouldReload(AppLocalizationsDelegate old) => false;
}

5.3 全局状态管理 AppState(主题 + 语言持久化)

AppState 统一持有主题模式与当前语言:

  • locale 为 null 时表示跟随系统语言;

  • loadSaved() 在 App 启动时从 SharedPreferences 读取上次保存的主题与语言;

  • setThemeMode() / setLocale() 更新状态、写回本地存储,并 notifyListeners() 通知所有监听者。

    // ---------------------- 全局状态管理(主题、语言) ----------------------
    class AppState extends ChangeNotifier {
    ThemeMode _themeMode = ThemeMode.system;
    Locale? _locale;

    复制代码
    ThemeMode get themeMode => _themeMode;
    Locale? get locale => _locale;
    
    
    Future<void> loadSaved() async {
      final sp = await SharedPreferences.getInstance();
      final themeIndex = sp.getInt('theme_mode');
      if (themeIndex != null) {
        _themeMode = ThemeMode.values[themeIndex];
      }
      final langCode = sp.getString('locale');
      if (langCode != null) {
        _locale = Locale(langCode);
      }
      notifyListeners();
    }
    
    
    Future<void> setThemeMode(ThemeMode mode) async {
      _themeMode = mode;
      final sp = await SharedPreferences.getInstance();
      await sp.setInt('theme_mode', mode.index);
      notifyListeners();
    }
    
    
    Future<void> setLocale(Locale? loc) async {
      _locale = loc;
      final sp = await SharedPreferences.getInstance();
      if (loc == null) {
        await sp.remove('locale');
      } else {
        await sp.setString('locale', loc.languageCode);
      }
      notifyListeners();
    }

    }

5.4 四个 Tab 页面

四个页面结构一致,通过 AppLocalizations.of(context).get(...) 取当前语言文案:

复制代码
// ---------------------- 4 个 Tab 页面 ----------------------
class ShopPage extends StatelessWidget {
  const ShopPage({super.key});


  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(
        AppLocalizations.of(context).get('home_page'),
        style: const TextStyle(fontSize: 22),
      ),
    );
  }
}


class DevicePage extends StatelessWidget {
  const DevicePage({super.key});


  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(
        AppLocalizations.of(context).get('device_page'),
        style: const TextStyle(fontSize: 22),
      ),
    );
  }
}


class CartPage extends StatelessWidget {
  const CartPage({super.key});


  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(
        AppLocalizations.of(context).get('cart_page'),
        style: const TextStyle(fontSize: 22),
      ),
    );
  }
}

5.5 MinePage:主题与语言选择弹窗

「我的」页面包含两个 ListTile:主题设置、语言设置。点击分别弹出 AlertDialog 选择项;选中项展示对勾标记。

注意:ChangeNotifierProvider.of(context) 用于读取全局状态;弹窗内同样通过 AppLocalizations 取文案,保证弹窗标题、选项本身也跟随语言切换。

复制代码
class MinePage extends StatelessWidget {
  const MinePage({super.key});


  @override
  Widget build(BuildContext context) {
    final state = ChangeNotifierProvider.of<AppState>(context);
    final loc = AppLocalizations.of(context);
    return ListView(
      children: [
        ListTile(
          title: Text(loc.get('theme_title')),
          subtitle: Text([
            loc.get('theme_system'),
            loc.get('theme_light'),
            loc.get('theme_dark'),
          ][state.themeMode.index]),
          onTap: () => _showThemeDialog(context, state),
        ),
        ListTile(
          title: Text(loc.get('lang_title')),
          subtitle: Text(
            state.locale?.languageCode == 'zh'
                ? loc.get('lang_zh')
                : loc.get('lang_en'),
          ),
          onTap: () => _showLangDialog(context, state),
        ),
      ],
    );
  }


  void _showThemeDialog(BuildContext context, AppState state) {
    final loc = AppLocalizations.of(context);
    showDialog(
      context: context,
      builder: (ctx) {
        return AlertDialog(
          title: Text(loc.get('theme_title')),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              _themeItem(loc.get('theme_system'), ThemeMode.system, state),
              _themeItem(loc.get('theme_light'), ThemeMode.light, state),
              _themeItem(loc.get('theme_dark'), ThemeMode.dark, state),
            ],
          ),
        );
      },
    );
  }


  Widget _themeItem(String title, ThemeMode mode, AppState state) {
    return ListTile(
      title: Text(title),
      onTap: () {
        state.setThemeMode(mode);
        Navigator.pop(context);
      },
      trailing: state.themeMode == mode ? const Icon(Icons.check) : null,
    );
  }


  void _showLangDialog(BuildContext context, AppState state) {
    final loc = AppLocalizations.of(context);
    showDialog(
      context: context,
      builder: (ctx) {
        return AlertDialog(
          title: Text(loc.get('lang_title')),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              _langItem(loc.get('lang_zh'), Locale('zh'), state),
              _langItem(loc.get('lang_en'), Locale('en'), state),
            ],
          ),
        );
      },
    );
  }


  Widget _langItem(String title, Locale loc, AppState state) {
    return ListTile(
      title: Text(title),
      onTap: () {
        state.setLocale(loc);
        Navigator.pop(context);
      },
      trailing:
          state.locale?.languageCode == loc.languageCode
          ? const Icon(Icons.check)
          : null,
    );
  }
}

5.6 底部导航 MainHome

MainHome 维护当前选中的 Tab 索引,底部导航栏的四个标签文案同样来自 AppLocalizations:

复制代码
// ---------------------- 主页面,底部 Tab ----------------------
class MainHome extends StatefulWidget {
  const MainHome({super.key});


  @override
  State<MainHome> createState() => _MainHomeState();
}


class _MainHomeState extends State<MainHome> {
  int _currentIndex = 0;


  final List<Widget> _pages = const [
    ShopPage(),
    DevicePage(),
    CartPage(),
    MinePage(),
  ];


  @override
  Widget build(BuildContext context) {
    final loc = AppLocalizations.of(context);
    return Scaffold(
      body: _pages[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (idx) {
          setState(() => _currentIndex = idx);
        },
        type: BottomNavigationBarType.fixed,
        items: [
          BottomNavigationBarItem(
            icon: const Icon(Icons.storefront),
            label: loc.get('tab_home'),
          ),
          BottomNavigationBarItem(
            icon: const Icon(Icons.devices),
            label: loc.get('tab_device'),
          ),
          BottomNavigationBarItem(
            icon: const Icon(Icons.shopping_cart),
            label: loc.get('tab_cart'),
          ),
          BottomNavigationBarItem(
            icon: const Icon(Icons.person),
            label: loc.get('tab_mine'),
          ),
        ],
      ),
    );
  }
}

5.7 应用入口 MyApp 与 MaterialApp 注册

MyApp 在 initState 中创建 AppState 并异步读取本地缓存;加载完成前展示 Loading。随后用 ChangeNotifierProvider 注入全局状态,Consumer 监听状态变化并重建 MaterialApp:

  • locale: state.locale:应用内强制使用当前语言;为 null 时跟随系统;

  • supportedLocales:声明本工程支持的语言;

  • localizationsDelegates:注册自定义代理与官方三个全局代理,保证 Material、Widget、Cupertino 组件内置文案(日期、按钮、选中提示等)同步切换语言。

    // ---------------------- main 入口 ----------------------
    void main() {
    runApp(const MyApp());
    }

    class MyApp extends StatefulWidget {
    const MyApp({super.key});

    复制代码
    @override
    State<MyApp> createState() => _MyAppState();

    }

    class _MyAppState extends State {
    late AppState _appState;
    bool _loaded = false;

    复制代码
    @override
    void initState() {
      super.initState();
      _appState = AppState();
      _appState.loadSaved().then((_) {
        setState(() => _loaded = true);
      });
    }
    
    
    @override
    Widget build(BuildContext context) {
      if (!_loaded) {
        return const MaterialApp(
          home: Scaffold(body: Center(child: CircularProgressIndicator())),
        );
      }
      return ChangeNotifierProvider.value(
        value: _appState,
        child: Consumer<AppState>(
          builder: (ctx, state, child) {
            return MaterialApp(
              title: '商城Demo',
              theme: ThemeData(
                colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
                useMaterial3: true,
              ),
              darkTheme: ThemeData(
                colorScheme: ColorScheme.fromSeed(
                  seedColor: Colors.blue,
                  brightness: Brightness.dark,
                ),
                useMaterial3: true,
              ),
              themeMode: state.themeMode,
              locale: state.locale,
              localizationsDelegates: const [
                AppLocalizationsDelegate(),
                GlobalMaterialLocalizations.delegate,
                GlobalWidgetsLocalizations.delegate,
                GlobalCupertinoLocalizations.delegate,
              ],
              supportedLocales: const [Locale('zh'), Locale('en')],
              home: const MainHome(),
            );
          },
        ),
      );
    }

    }

5.8 手写状态管理:ChangeNotifierProvider 与 Consumer

本 Demo 未引入第三方状态管理库,直接基于 InheritedWidget 手写一套最简 Provider:

  • ChangeNotifierProvider:在 initState 中监听状态对象,收到通知后 setState 重建自身;

  • _InheritedProvider:真正的 InheritedWidget,向下层暴露状态对象;

  • Consumer:读取状态对象后调用 builder 重建对应子树;

  • of() 通过 dependOnInheritedWidgetOfExactType 建立依赖关系。

    // 每次 build 重新生成 InheritedWidget
    class ChangeNotifierProvider
    extends StatefulWidget {
    final T value;
    final Widget child;

    复制代码
    const ChangeNotifierProvider.value({
      super.key,
      required this.value,
      required this.child,
    });
    
    
    @override
    State<ChangeNotifierProvider<T>> createState() =>
        _ChangeNotifierProviderState<T>();
    
    
    static T of<T>(BuildContext context) {
      return context
          .dependOnInheritedWidgetOfExactType<_InheritedProvider<T>>()!
          .value;
    }

    }

    class _ChangeNotifierProviderState
    extends State<ChangeNotifierProvider> {
    @override
    void initState() {
    super.initState();
    widget.value.addListener(_notify);
    }

    复制代码
    void _notify() {
      setState(() {});
    }
    
    
    @override
    void dispose() {
      widget.value.removeListener(_notify);
      super.dispose();
    }
    
    
    @override
    Widget build(BuildContext context) {
      // 每次 build 都新建 InheritedProvider 实例,触发更新
      return _InheritedProvider<T>(value: widget.value, child: widget.child);
    }

    }

    class _InheritedProvider extends InheritedWidget {
    final T value;

    复制代码
    const _InheritedProvider({required this.value, required super.child});
    
    
    @override
    bool updateShouldNotify(_InheritedProvider<T> oldWidget) {
      return true; // 只要重建就通知所有依赖
    }

    }

    class Consumer extends StatelessWidget {
    final Widget Function(BuildContext context, T value, Widget? child) builder;

    复制代码
    const Consumer({super.key, required this.builder});
    
    
    @override
    Widget build(BuildContext context) {
      final val = ChangeNotifierProvider.of<T>(context);
      return builder(context, val, null);
    }

    }

六、问题排查:多语言切换不生效

6.1 现象

在语言弹窗中切换中文 / 英文后,AppState.setLocale() 已执行、SharedPreferences 中也保存了新语言码,但页面底部 Tab、页面标题等文案没有刷新,需要重启 App 才生效。

6.2 原因分析

InheritedWidget 的更新通知机制有两个关键环节:

  1. 状态对象调用 notifyListeners() 后,ChangeNotifierProvider 监听器执行 setState,_InheritedProvider 被重新构建;

  2. InheritedWidget 重建后,会通过 updateShouldNotify 判断是否通知所有依赖它的子 Widget:返回 false 时,即使组件被重建,下游依赖方也不会收到变更通知,不会触发 didChangeDependencies 与重新构建。

本 Demo 中 AppState 只是内部状态(_locale)发生变化,传入 Provider 的 value 对象引用并未改变;如果 updateShouldNotify 使用默认的 false 比较逻辑,依赖该 Provider 的 Consumer(即 MaterialApp 及其子树)就不会重建,locale 参数不会更新,本地化框架自然不会重新加载文案。

6.3 解决方案

在 _InheritedProvider 中强制 updateShouldNotify 返回 true:每次 InheritedWidget 重建都通知所有依赖方,触发 MaterialApp 与整棵页面树重建,从而应用新的 locale:

复制代码
class _InheritedProvider<T> extends InheritedWidget {
  final T value;


  const _InheritedProvider({required this.value, required super.child});


  @override
  bool updateShouldNotify(_InheritedProvider<T> oldWidget) {
    return true; // 只要重建就通知所有依赖
  }
}

配合 ChangeNotifierProvider 监听器中的 setState(() {}),形成完整闭环:

复制代码
void _notify() {
  setState(() {});
}

状态变更通知链路:

复制代码
点击语言选项
  └─ AppState.setLocale()
       ├─ 更新 _locale 并写入 SharedPreferences
       └─ notifyListeners()
            └─ ChangeNotifierProvider._notify() → setState()
                 └─ _InheritedProvider 重建
                      └─ updateShouldNotify = true,通知 Consumer
                           └─ MaterialApp 以新 locale 重建
                                └─ AppLocalizationsDelegate 重新加载文案
                                     └─ 全部页面 Text 刷新为新语言

七、实现效果

  • 底部导航四个 Tab(商城 / 设备 / 购物车 / 我的)随语言即时切换;

  • 各页面标题、主题弹窗、语言弹窗中的文案同步刷新;

  • 主题模式(跟随系统 / 亮色 / 暗色)与语言均通过 SharedPreferences 持久化,退出并重启 App 后保持上次选择;

  • locale 置空可恢复跟随系统语言(本 Demo 弹窗仅提供中/英两个选项,扩展时可增加「跟随系统」项)。

八、关键要点总结

  1. 注册三件套缺一不可:locale、supportedLocales、localizationsDelegates 必须配置在 MaterialApp 上,自定义 AppLocalizationsDelegate 与官方 Global*Localizations.delegate 同时注册。

  2. 取值方式统一:页面中始终使用 AppLocalizations.of(context).get('key'),不要硬编码中/英文案。

  3. 切换语言要重建根节点:通过状态管理包裹 MaterialApp,语言变化后重建 App 根节点,本地化框架才会按新 locale 重新加载文案。

  4. 状态通知必须真正触达下游:手写 InheritedWidget 状态管理时,updateShouldNotify 需返回 true,否则状态变更无法通知到依赖子树。

  5. 持久化时机:写入 SharedPreferences 与 notifyListeners() 在同一个 setLocale / setThemeMode 方法内完成,避免状态与本地存储不一致。

九、后续扩展建议

  1. 文案文件分离:文案量增大后,将 _localizedValues 拆分到 strings_zh.dart、strings_en.dart 等独立文件;更规范的做法是使用 Flutter 官方 gen-l10n 配合 .arb 资源文件,自动生成强类型取值代码。

  2. Key 缺失兜底:get(key) 中对语言码或 key 不存在的情况返回默认值(如返回 key 本身或英文兜底),避免 ! 空断言抛出异常。

  3. 语言选项动态化:根据 supportedLocales 动态生成语言列表,并补充「跟随系统」选项;当前 state.locale == null 时副标题会默认展示 English,可一并修正。

  4. 引入成熟状态管理库:生产项目建议使用 provider 或 Riverpod 替代手写 Provider,减少 updateShouldNotify 这类底层细节带来的隐患。

  5. 新增语言成本:新增一种语言时,只需在 _localizedValues 中增加映射、在 isSupported 与 supportedLocales 中注册对应 Locale 即可。

附录:main.dart 完整代码

复制代码
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:shared_preferences/shared_preferences.dart';


// ---------------------- 多语言文本 ----------------------
class AppLocalizations {
  AppLocalizations(this.locale);


  final Locale locale;


  static AppLocalizations of(BuildContext context) {
    return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
  }


  static const Map<String, Map<String, String>> _localizedValues = {
    'zh': {
      'tab_home': '商城',
      'tab_device': '设备',
      'tab_cart': '购物车',
      'tab_mine': '我的',
      'theme_title': '主题设置',
      'theme_light': '亮色模式',
      'theme_dark': '暗色模式',
      'theme_system': '跟随系统',
      'lang_title': '语言设置',
      'lang_zh': '中文',
      'lang_en': 'English',
      'home_page': '商城页面',
      'device_page': '设备页面',
      'cart_page': '购物车页面',
      'mine_page': '个人中心',
    },
    'en': {
      'tab_home': 'Shop',
      'tab_device': 'Device',
      'tab_cart': 'Cart',
      'tab_mine': 'Mine',
      'theme_title': 'Theme Setting',
      'theme_light': 'Light',
      'theme_dark': 'Dark',
      'theme_system': 'System Default',
      'lang_title': 'Language',
      'lang_zh': 'Chinese',
      'lang_en': 'English',
      'home_page': 'Shop Page',
      'device_page': 'Device Page',
      'cart_page': 'Cart Page',
      'mine_page': 'Mine Page',
    },
  };


  String get(String key) {
    return _localizedValues[locale.languageCode]![key]!;
  }
}


class AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
  const AppLocalizationsDelegate();


  @override
  bool isSupported(Locale locale) => ['zh', 'en'].contains(locale.languageCode);


  @override
  Future<AppLocalizations> load(Locale locale) async {
    return AppLocalizations(locale);
  }


  @override
  bool shouldReload(AppLocalizationsDelegate old) => false;
}


// ---------------------- 全局状态管理(主题、语言) ----------------------
class AppState extends ChangeNotifier {
  ThemeMode _themeMode = ThemeMode.system;
  Locale? _locale;


  ThemeMode get themeMode => _themeMode;
  Locale? get locale => _locale;


  Future<void> loadSaved() async {
    final sp = await SharedPreferences.getInstance();
    final themeIndex = sp.getInt('theme_mode');
    if (themeIndex != null) {
      _themeMode = ThemeMode.values[themeIndex];
    }
    final langCode = sp.getString('locale');
    if (langCode != null) {
      _locale = Locale(langCode);
    }
    notifyListeners();
  }


  Future<void> setThemeMode(ThemeMode mode) async {
    _themeMode = mode;
    final sp = await SharedPreferences.getInstance();
    await sp.setInt('theme_mode', mode.index);
    notifyListeners();
  }


  Future<void> setLocale(Locale? loc) async {
    _locale = loc;
    final sp = await SharedPreferences.getInstance();
    if (loc == null) {
      await sp.remove('locale');
    } else {
      await sp.setString('locale', loc.languageCode);
    }
    notifyListeners();
  }
}


// ---------------------- 4 个 Tab 页面 ----------------------
class ShopPage extends StatelessWidget {
  const ShopPage({super.key});


  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(
        AppLocalizations.of(context).get('home_page'),
        style: const TextStyle(fontSize: 22),
      ),
    );
  }
}


class DevicePage extends StatelessWidget {
  const DevicePage({super.key});


  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(
        AppLocalizations.of(context).get('device_page'),
        style: const TextStyle(fontSize: 22),
      ),
    );
  }
}


class CartPage extends StatelessWidget {
  const CartPage({super.key});


  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(
        AppLocalizations.of(context).get('cart_page'),
        style: const TextStyle(fontSize: 22),
      ),
    );
  }
}


class MinePage extends StatelessWidget {
  const MinePage({super.key});


  @override
  Widget build(BuildContext context) {
    final state = ChangeNotifierProvider.of<AppState>(context);
    final loc = AppLocalizations.of(context);
    return ListView(
      children: [
        ListTile(
          title: Text(loc.get('theme_title')),
          subtitle: Text([
            loc.get('theme_system'),
            loc.get('theme_light'),
            loc.get('theme_dark'),
          ][state.themeMode.index]),
          onTap: () => _showThemeDialog(context, state),
        ),
        ListTile(
          title: Text(loc.get('lang_title')),
          subtitle: Text(
            state.locale?.languageCode == 'zh'
                ? loc.get('lang_zh')
                : loc.get('lang_en'),
          ),
          onTap: () => _showLangDialog(context, state),
        ),
      ],
    );
  }


  void _showThemeDialog(BuildContext context, AppState state) {
    final loc = AppLocalizations.of(context);
    showDialog(
      context: context,
      builder: (ctx) {
        return AlertDialog(
          title: Text(loc.get('theme_title')),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              _themeItem(loc.get('theme_system'), ThemeMode.system, state),
              _themeItem(loc.get('theme_light'), ThemeMode.light, state),
              _themeItem(loc.get('theme_dark'), ThemeMode.dark, state),
            ],
          ),
        );
      },
    );
  }


  Widget _themeItem(String title, ThemeMode mode, AppState state) {
    return ListTile(
      title: Text(title),
      onTap: () {
        state.setThemeMode(mode);
        Navigator.pop(context);
      },
      trailing: state.themeMode == mode ? const Icon(Icons.check) : null,
    );
  }


  void _showLangDialog(BuildContext context, AppState state) {
    final loc = AppLocalizations.of(context);
    showDialog(
      context: context,
      builder: (ctx) {
        return AlertDialog(
          title: Text(loc.get('lang_title')),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              _langItem(loc.get('lang_zh'), Locale('zh'), state),
              _langItem(loc.get('lang_en'), Locale('en'), state),
            ],
          ),
        );
      },
    );
  }


  Widget _langItem(String title, Locale loc, AppState state) {
    return ListTile(
      title: Text(title),
      onTap: () {
        state.setLocale(loc);
        Navigator.pop(context);
      },
      trailing:
          state.locale?.languageCode == loc.languageCode
          ? const Icon(Icons.check)
          : null,
    );
  }
}


// ---------------------- 主页面,底部 Tab ----------------------
class MainHome extends StatefulWidget {
  const MainHome({super.key});


  @override
  State<MainHome> createState() => _MainHomeState();
}


class _MainHomeState extends State<MainHome> {
  int _currentIndex = 0;


  final List<Widget> _pages = const [
    ShopPage(),
    DevicePage(),
    CartPage(),
    MinePage(),
  ];


  @override
  Widget build(BuildContext context) {
    final loc = AppLocalizations.of(context);
    return Scaffold(
      body: _pages[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (idx) {
          setState(() => _currentIndex = idx);
        },
        type: BottomNavigationBarType.fixed,
        items: [
          BottomNavigationBarItem(
            icon: const Icon(Icons.storefront),
            label: loc.get('tab_home'),
          ),
          BottomNavigationBarItem(
            icon: const Icon(Icons.devices),
            label: loc.get('tab_device'),
          ),
          BottomNavigationBarItem(
            icon: const Icon(Icons.shopping_cart),
            label: loc.get('tab_cart'),
          ),
          BottomNavigationBarItem(
            icon: const Icon(Icons.person),
            label: loc.get('tab_mine'),
          ),
        ],
      ),
    );
  }
}


// ---------------------- main 入口 ----------------------
void main() {
  runApp(const MyApp());
}


class MyApp extends StatefulWidget {
  const MyApp({super.key});


  @override
  State<MyApp> createState() => _MyAppState();
}


class _MyAppState extends State<MyApp> {
  late AppState _appState;
  bool _loaded = false;


  @override
  void initState() {
    super.initState();
    _appState = AppState();
    _appState.loadSaved().then((_) {
      setState(() => _loaded = true);
    });
  }


  @override
  Widget build(BuildContext context) {
    if (!_loaded) {
      return const MaterialApp(
        home: Scaffold(body: Center(child: CircularProgressIndicator())),
      );
    }
    return ChangeNotifierProvider.value(
      value: _appState,
      child: Consumer<AppState>(
        builder: (ctx, state, child) {
          return MaterialApp(
            title: '商城Demo',
            theme: ThemeData(
              colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
              useMaterial3: true,
            ),
            darkTheme: ThemeData(
              colorScheme: ColorScheme.fromSeed(
                seedColor: Colors.blue,
                brightness: Brightness.dark,
              ),
              useMaterial3: true,
            ),
            themeMode: state.themeMode,
            locale: state.locale,
            localizationsDelegates: const [
              AppLocalizationsDelegate(),
              GlobalMaterialLocalizations.delegate,
              GlobalWidgetsLocalizations.delegate,
              GlobalCupertinoLocalizations.delegate,
            ],
            supportedLocales: const [Locale('zh'), Locale('en')],
            home: const MainHome(),
          );
        },
      ),
    );
  }
}


// 每次 build 重新生成 InheritedWidget
class ChangeNotifierProvider<T extends ChangeNotifier>
    extends StatefulWidget {
  final T value;
  final Widget child;


  const ChangeNotifierProvider.value({
    super.key,
    required this.value,
    required this.child,
  });


  @override
  State<ChangeNotifierProvider<T>> createState() =>
      _ChangeNotifierProviderState<T>();


  static T of<T>(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<_InheritedProvider<T>>()!
        .value;
  }
}


class _ChangeNotifierProviderState<T extends ChangeNotifier>
    extends State<ChangeNotifierProvider<T>> {
  @override
  void initState() {
    super.initState();
    widget.value.addListener(_notify);
  }


  void _notify() {
    setState(() {});
  }


  @override
  void dispose() {
    widget.value.removeListener(_notify);
    super.dispose();
  }


  @override
  Widget build(BuildContext context) {
    // 每次 build 都新建 InheritedProvider 实例,触发更新
    return _InheritedProvider<T>(value: widget.value, child: widget.child);
  }
}


class _InheritedProvider<T> extends InheritedWidget {
  final T value;


  const _InheritedProvider({required this.value, required super.child});


  @override
  bool updateShouldNotify(_InheritedProvider<T> oldWidget) {
    return true; // 只要重建就通知所有依赖
  }
}


class Consumer<T> extends StatelessWidget {
  final Widget Function(BuildContext context, T value, Widget? child) builder;


  const Consumer({super.key, required this.builder});


  @override
  Widget build(BuildContext context) {
    final val = ChangeNotifierProvider.of<T>(context);
    return builder(context, val, null);
  }
}
相关推荐
DogDaoDao1 小时前
数字信号处理:数字域(时域)与频域的关系与作用——从傅里叶变换到 FFT 的完整推演
数字信号处理·1024程序员节·傅里叶变换·卷积·dct变换·时域·频域
Percep_gan1 小时前
调用天翼云的对象存储ZOS,很详细
java·1024程序员节
JulyLi20191 小时前
yolo检测适配昇腾Atlas300IDuo笔记
yolo·1024程序员节
遇雪长安1 小时前
MCU 固件启动即崩,bootloader 如何发现
1024程序员节
Zevalin爱灰灰1 小时前
Linux入门第一部分(Ubuntu入门篇) 第二章 浅尝Ubuntu(二)
linux·嵌入式·1024程序员节
泡海椒1 小时前
JQuick-SQL REST API 数据接入:应用层调用接口后注册分析表
数据库·sql·1024程序员节
_MH88881 小时前
2026.10.24|距离上一次写 CSDN,刚好过去了一年
1024程序员节
XIU YUAN1 小时前
ESP32-C6 专业游戏合集系统 - 打造你的掌上游戏机
1024程序员节