本篇文章主要介绍以下几个内容:
- Flutter 国际化原理
- 与 Android/iOS 原生国际化的对比
- 使用 Riverpod 实现语言切换的代码示例
Flutter之旅
本文实现效果:
Flutter 国际化
1. Flutter 国际化底层原理
1.1 核心组件与数据流
Flutter 国际化的核心由三部分构成,并通过一条明确的数据流完成 “选择语言 → 加载资源 → 分发到树 → 精准重建”。
-
Localizations
与LocalizationsDelegate
-
Localizations
是一个InheritedWidget
,负责在 Widget 树中提供当前Locale
的本地化资源实例。 -
LocalizationsDelegate<T>
负责“按需加载 + 构建”类型为T
的资源(本项目为S
),并交由Localizations
管理与缓存。
-
-
Locale
的解析与选取- 入口在
MaterialApp
:supportedLocales
定义支持列表;locale
可显式指定当前语言(不指定则跟随系统); - 可选回调
localeListResolutionCallback
/localeResolutionCallback
用于自定义匹配策略(如兼容zh
与zh_CN
)。
- 入口在
-
资源访问类
S
- 由 Intl 工具链根据
.arb
生成,用于类型安全地访问文案:S.of(context)
、S.current
; - 委托
S.delegate
(AppLocalizationDelegate
)在load(Locale)
时构建并更新当前实例。
- 由 Intl 工具链根据
Localizations 树装配流程(逐步)
- 启动时,
MaterialApp
创建并插入Localizations
到根部; - 遍历
localizationsDelegates
,调用每个 delegate 的isSupported
→load(locale)
; - 对于
S.delegate
,load(locale)
会设置Intl.defaultLocale
并创建新的S
实例,赋值给S._current
; - 树中通过
Localizations.of<S>(context, S)
(即S.of(context)
)读取到当前S
实例。
示例(来自生成代码的关键路径):
// lib/generated/l10n.dart(节选逻辑)
static Future<S> load(Locale locale) {
final localeName = Intl.canonicalizedLocale(/* ... */);
return initializeMessages(localeName).then((_) {
Intl.defaultLocale = localeName;
final instance = S();
S._current = instance; // 更新全局当前实例
return instance;
});
}
依赖追踪与重建触发
- 任何使用
S.of(context)
的 Widget,会通过Localizations.of
对Localizations
建立依赖; - 当
locale
或 delegates 改变时,Localizations
会更新并通知依赖者,触发这些节点的“局部重建”; - 这保证了语言切换时,只刷新依赖文案的组件,代价最小。
Delegate 缓存与 shouldReload
- 生成的
AppLocalizationDelegate.shouldReload
返回false
(稳定资源不需要因为 delegate 本身变化而重载); - 真正触发重新加载的,是
locale
变化(MaterialApp.locale
或系统语言更改)导致的Localizations
重新构建。
Locale
解析顺序(默认)
- 指定了
locale
:优先使用该Locale
; - 未指定
locale
:按系统首选语言列表与supportedLocales
匹配; - 匹配失败:回落到
supportedLocales
的第一个条目。
本质上,Flutter 通过 Localizations
在 Widget 树中下发“当前语言资源”的引用;当 locale
变更时,由 Localizations
精准通知依赖节点重建,实现动态更新多语言文案。
1.2 与主题系统的类比
- 主题切换依赖
InheritedWidget
(Theme
),国际化同样依赖InheritedWidget
(Localizations
)。 - 变更
ThemeMode
会导致依赖主题的节点重建;变更Locale
会导致依赖文案的节点重建。二者机制类似,都是高效的“精准重建”。
2. 跨平台国际化方案对比
2.1 Android 原生(资源限定符)
<!-- res/values/strings.xml -->
<resources>
<string name="ok">OK</string>
<string name="cancel">Cancel</string>
<string name="device_list_count">%1$d devices</string>
}</resources>
<!-- res/values-zh/strings.xml -->
<resources>
<string name="ok">确定</string>
<string name="cancel">取消</string>
<string name="device_list_count">%1$d个设备</string>
</resources>
- 特点:基于资源目录(
values-xx
)与Configuration
的系统级匹配;切换语言常伴随 Activity 重建。 - 使用:
getString(R.string.ok)
。
2.2 iOS 原生(Localizable.strings)
// Localizable.strings (English)
"ok" = "OK";
"cancel" = "Cancel";
// Localizable.strings (Chinese - Simplified)
"ok" = "确定";
"cancel" = "取消";
// 代码
let title = NSLocalizedString("ok", comment: "OK button")
- 特点:基于
Bundle
的字符串表;由系统自动按用户首选语言解析。
2.3 Flutter 对比(跨平台一致)
维度 | Android 原生 | iOS 原生 | Flutter |
---|---|---|---|
资源组织 | res/values-xx |
Localizable.strings |
.arb + 代码生成(S 类) |
切换机制 | 配置变更/Activity 重建 | 系统语言变更触发 |
Localizations 局部重建 |
一致性 | 平台差异明显 | 平台差异明显 | 跨平台一致 |
自定义能力 | 依赖系统 | 依赖系统 | 完全代码可控 |
3. 实现步骤与代码示例
用 Flutter Intl 实现国际化:https://juejin.cn/post/7410645914585546779。
3.1 安装 Flutter Intl 插件
在 IDE 中安装 Flutter Intl 插件,以 Android studio 为例:
Android Studio --->Settings --->Plugins ---> Marketplace ---> 搜索框输入Intl ---> Install
3.2 初始化 Flutter Intl
Tools → Flutter Intl → Initialize for the project;
- 生成目录:
-
lib/l10n/
:存放.arb
语言资源文件(配置文件); -
lib/generated/
:存放生成的l10n.dart
与messages_*.dart
(自动生成)。
-
3.3 添加国际化语言
Tools ---> Flutter Intl ---> Add Locale,新增如 `en`、`zh_CN` 等国际化语言
- 在相应
.arb
中新增键值:
{
"drawer_item_theme": "暗黑模式",
"drawer_item_language": "语言设置",
"device_list_count": "{count}个设备"
}
占位符在 .arb
中需声明参数类型:
{
"device_list_count": "{count}个设备",
"@device_list_count": {
"placeholders": { "count": { "type": "int" } }
},
// 复合参数
"storage_usage": "已用{usedGB}GB / 共{totalGB}GB"
}
3.4. 在项目中配置
在 MaterialApp
中接入:
// lib/main.dart(节选)
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'generated/l10n.dart';
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 如果使用 Riverpod 管理语言,直接 watch 对应的 Locale(详见下一节)
final locale = ref.watch(languageProvider); // Locale?,null 表示跟随系统
return MaterialApp(
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
locale: locale,
// ... 其他配置
);
}
}
3.5 在代码中使用
import 'generated/l10n.dart';
// 推荐:会随着语言变化而自动更新
Text(S.of(context).drawer_item_language);
// 特殊场景(不随 context 重建,如后台任务):
final text = S.current.ok; // 注意:不会触发 UI 自动刷新
3.6 注意事项
命名规范(建议)
- 采用 “模块--组件--语义” 三级结构:如
home_title
、device_manage_button_add
; - 常用键:
ok
、cancel
、confirm
等通用词直接命名; - 参数化:
device_list_count
、device_status_{status}
; - 复用性优先:尽量抽出通用短语,避免页面强绑定;
- 保持多语言键一致,避免遗漏。
数字/日期/复数与参数
借助 intl
格式化与 plural/gender 支持:
// .arb
{
"files_count": "{count, plural, =0{没有文件} =1{1个文件} other{{count}个文件}}",
"@files_count": { "placeholders": { "count": { "type": "int" } } }
}
// Dart
Text(S.of(context).files_count(count));
4. 使用 Riverpod 实现语言切换
4.1 实现持久化存储
如使用 SharedPreferences 保存用户的语言选择:
// lib/common/utils/share_preferences_utils.dart
class SharePreferenceUtils {
/// 保存语言
static Future<bool> saveLanguage(String languageCode) async {
return await saveString(SharePreferenceKey.language, languageCode);
}
/// 获取语言
static Future<String> getLanguage() async {
return await getString(SharePreferenceKey.language, "");
}
}
class SharePreferenceKey {
static const String language = "APP_LANGUAGE"; // 语言
}
// 语言常量
class LanguageConstant {
static const Locale en = Locale('en', 'US');
static const Locale zh = Locale('zh', 'CN');
}
4.2 创建主题状态管理器
使用 Riverpod 的 StateNotifier 创建主题状态管理器:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../generated/l10n.dart';
import '../channel/platform_service.dart';
import '../constant.dart';
import '../utils/share_preferences_utils.dart';
class LanguageNotifier extends StateNotifier<Locale?> {
LanguageNotifier() : super(null) {
_initLanguage();
}
/// 初始化语言设置
Future<void> _initLanguage() async {
final savedLanguage = await SharePreferenceUtils.getLanguage();
if (savedLanguage.isNotEmpty) {
state = _localeFromString(savedLanguage);
} else {
// 默认语言改为英文
state = LanguageConstant.en;
}
}
/// 获取系统语言
Future<Locale> _getSystemLocale() async {
final systemLocale = WidgetsBinding.instance.platformDispatcher.locale;
return systemLocale;
}
/// 转换字符串为Locale
Locale _localeFromString(String localeStr) {
final parts = localeStr.split('_');
return Locale(parts[0], parts.length > 1 ? parts[1] : '');
}
/// 获取支持的语言
Locale _getSupportedLocale(Locale systemLocale) {
final supportedLocales = S.delegate.supportedLocales;
final isSupported = supportedLocales.any((loc) =>
loc.languageCode == systemLocale.languageCode &&
(loc.countryCode == null ||
loc.countryCode == systemLocale.countryCode));
return isSupported ? systemLocale : LanguageConstant.en; // 默认英语
}
/// 切换应用语言
Future<void> setLanguage(Locale locale) async {
state = locale;
await SharePreferenceUtils.saveLanguage(
'${locale.languageCode}_${locale.countryCode}');
// 同步语言设置到小组件
await PlatformService.syncLanguage(locale.languageCode);
}
}
/// 语言状态提供者
final languageProvider = StateNotifierProvider<LanguageNotifier, Locale?>(
(ref) => LanguageNotifier(),
);
/// 系统语言提供者
final systemLanguageProvider = Provider<Locale>((ref) {
return WidgetsBinding.instance.platformDispatcher.locale;
});
/// 判断当前是否为中文的提供者
final isChineseProvider = Provider<bool>((ref) {
final currentLocale = ref.watch(languageProvider);
return currentLocale?.languageCode == LanguageConstant.zh.languageCode;
});
/// 字体提供者
final fontFamilyProvider = Provider<String>((ref) {
final currentLocale = ref.watch(languageProvider);
return currentLocale == LanguageConstant.zh
? FontConstant.notoSansSC
: FontConstant.inter;
});
4.3 在 MaterialApp 中应用语言
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'language/language_provider.dart';
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final language = ref.watch(languageProvider);
return MaterialApp(
title: 'Flutter语言切换示例',
locale: language,
localizationsDelegates: [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: MyHomePage(),
);
}
}
使用示例:
// 切换到中文(简体)
ref.read(languageProvider.notifier).setLanguage(const Locale('zh', 'CN'));
// 切换到英文
ref.read(languageProvider.notifier).setLanguage(const Locale('en'));
4.4 为什么要用 Riverpod 管理语言?
- 类型安全与单一数据源:
Locale?
作为应用“语言真相”的唯一输出,避免多处各自维护; - 精准重建:依赖语言的组件通过
ref.watch(languageProvider)
或S.of(context)
获得更新,尽量减少无关重建; - 易于持久化与跨端同步:与
SharePreferenceUtils
、PlatformService.syncLanguage
无缝对接; - 可测试:
StateNotifier
便于注入与单元测试; - 与主题/字体联动:项目中
fontFamilyProvider
已根据语言自动切换字体,提升整体一致性。
5. 其他事项
-
S.of(context)
优先于S.current
-
S.of(context)
:对Localizations
建立依赖,locale
变化时会被精准通知并重建; -
S.current
:读取的是全局当前实例,不会“自己触发”重建。若没有其他状态变化导致该 Widget 重建,使用S.current
的文本不会自动更新。
-
-
将
locale
交由顶层MaterialApp
管控- 避免在子树多处“覆盖”
Localizations.override
,防止维护复杂度与重建范围不可控。
- 避免在子树多处“覆盖”
-
避免在频繁重建的 Widget 中做昂贵的格式化
- 大量
DateFormat/NumberFormat
可复用或上移到较少重建的层级;或做轻量缓存。
- 大量
- FAQ:为什么上面代码里用
S.current
也能更新?
- 根因:
S.current
指向的是由S.delegate.load(locale)
更新的全局实例。每当MaterialApp.locale
变化,Localizations
会触发重新加载,S._current
也会被更新。 - 现象:语言切换通过 Riverpod 改变了顶层
locale
,导致应用根部重建,绝大多数子树也会随之重建。于是即便使用S.current
,因为 Widget 本身经历了重建,文本也跟着更新。 - 关键区别:
S.current
本身不具备“依赖跟踪”能力,它不会促使某个未重建的 Widget 自动刷新。若某组件被“缓存”且未参与这次重建,那么其中基于旧S.current
的字符串不会变化。为了获得确定性的 UI 更新,应优先使用S.of(context)
。