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
四、整体实现思路
整体链路如下:
-
用静态 Map 维护中文、英文两套文案,通过 AppLocalizations 对外提供 get(key) 取值;
-
自定义 AppLocalizationsDelegate,交给 Flutter 本地化框架在当前 Locale 下加载对应的 AppLocalizations 实例;
-
AppState 继承 ChangeNotifier,集中管理 ThemeMode 与 Locale,变更时写入 SharedPreferences 并通知监听者;
-
手写轻量 ChangeNotifierProvider(基于 InheritedWidget)在树根注入 AppState,Consumer 包裹 MaterialApp,状态变化时重建 App 根节点;
-
MaterialApp 通过 locale、supportedLocales、localizationsDelegates 完成本地化注册;
-
页面中统一通过 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 的更新通知机制有两个关键环节:
-
状态对象调用 notifyListeners() 后,ChangeNotifierProvider 监听器执行 setState,_InheritedProvider 被重新构建;
-
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 弹窗仅提供中/英两个选项,扩展时可增加「跟随系统」项)。
八、关键要点总结
-
注册三件套缺一不可:locale、supportedLocales、localizationsDelegates 必须配置在 MaterialApp 上,自定义 AppLocalizationsDelegate 与官方 Global*Localizations.delegate 同时注册。
-
取值方式统一:页面中始终使用 AppLocalizations.of(context).get('key'),不要硬编码中/英文案。
-
切换语言要重建根节点:通过状态管理包裹 MaterialApp,语言变化后重建 App 根节点,本地化框架才会按新 locale 重新加载文案。
-
状态通知必须真正触达下游:手写 InheritedWidget 状态管理时,updateShouldNotify 需返回 true,否则状态变更无法通知到依赖子树。
-
持久化时机:写入 SharedPreferences 与 notifyListeners() 在同一个 setLocale / setThemeMode 方法内完成,避免状态与本地存储不一致。
九、后续扩展建议
-
文案文件分离:文案量增大后,将 _localizedValues 拆分到 strings_zh.dart、strings_en.dart 等独立文件;更规范的做法是使用 Flutter 官方 gen-l10n 配合 .arb 资源文件,自动生成强类型取值代码。
-
Key 缺失兜底:get(key) 中对语言码或 key 不存在的情况返回默认值(如返回 key 本身或英文兜底),避免 ! 空断言抛出异常。
-
语言选项动态化:根据 supportedLocales 动态生成语言列表,并补充「跟随系统」选项;当前 state.locale == null 时副标题会默认展示 English,可一并修正。
-
引入成熟状态管理库:生产项目建议使用 provider 或 Riverpod 替代手写 Provider,减少 updateShouldNotify 这类底层细节带来的隐患。
-
新增语言成本:新增一种语言时,只需在 _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);
}
}