Flutter国际化与主题
What — 是什么
Flutter 国际化(i18n)与主题系统是应用的两个核心基础设施。国际化让应用支持多语言和地区适配,主题系统统一管理颜色、字体、组件样式,两者配合实现多语言 + 多主题的企业级应用。
国际化核心概念:
- Locale:语言和地区标识(如 zh_CN、en_US)
- ARB 文件:Application Resource Bundle,标准翻译文件格式
- flutter_localizations:官方本地化支持包
- intl 包:消息格式化、日期/数字本地化
主题核心概念:
- ThemeData:Material 主题数据对象,包含颜色、字体、组件样式
- ColorScheme:Material 3 颜色方案,从种子色生成完整色板
- TextTheme:文本样式体系
- InheritedWidget:主题通过 InheritedWidget 向下传递
Why — 为什么
适用场景:
- 应用需要支持多语言(中文/英文/日文等)
- 需要暗黑模式切换
- 多品牌/多主题白标应用
- 日期/数字/货币需要地区适配
与 Web 端对比:
| 维度 | Flutter i18n | Web i18n (i18next) |
|---|---|---|
| 翻译格式 | ARB (JSON) | JSON |
| 代码生成 | intl_utils / 自动 | 手动 |
| 切换语言 | setState + Locale | i18next.changeLanguage |
| 日期格式 | intl 包 | Intl.DateTimeFormat |
| 暗黑模式 | ThemeData.dark() | CSS 变量 / prefers-color-scheme |
| 主题系统 | ThemeData + ColorScheme | CSS 变量 / CSS-in-JS |
How — 怎么用
1. 国际化基础配置
# pubspec.yaml
dependencies:
flutter_localizations:
sdk: flutter
intl: ^0.19.0
// ===== MaterialApp 配置 =====
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate, // 应用翻译
GlobalMaterialLocalizations.delegate, // Material 组件翻译
GlobalWidgetsLocalizations.delegate, // Widget 翻译
GlobalCupertinoLocalizations.delegate,// Cupertino 组件翻译
],
supportedLocales: const [
Locale('zh', 'CN'),
Locale('en', 'US'),
Locale('ja', 'JP'),
],
locale: _currentLocale, // 当前语言
// 或根据系统语言自动选择
// localeListResolutionCallback: (locales, supported) {
// for (var locale in locales) {
// if (supported.contains(locale)) return locale;
// }
// return const Locale('en', 'US');
// },
home: const HomePage(),
);
2. ARB 文件与 intl_utils
# 安装 intl_utils
dart pub global activate intl_utils
# 初始化
intl_utils init
# 生成代码(从 ARB 文件生成 Dart 类)
intl_utils generate
ARB 文件示例:
// l10n/app_zh.arb
{
"@@locale": "zh",
"appTitle": "我的应用",
"greeting": "你好,{name}!",
"@greeting": {
"placeholders": {
"name": { "type": "String" }
}
},
"itemCount": "{count} 个项目",
"@itemCount": {
"placeholders": {
"count": { "type": "int" }
}
},
"homeTab": "首页",
"profileTab": "我的",
"settingsTitle": "设置"
}
// l10n/app_en.arb
{
"@@locale": "en",
"appTitle": "My App",
"greeting": "Hello, {name}!",
"itemCount": "{count} items",
"homeTab": "Home",
"profileTab": "Profile",
"settingsTitle": "Settings"
}
生成的 Dart 类使用:
// 获取翻译文本
Text(AppLocalizations.of(context)!.appTitle)
Text(AppLocalizations.of(context)!.greeting('Alice'))
Text(AppLocalizations.of(context)!.itemCount(5))
3. Flutter 3.10+ 新版国际化(推荐)
# l10n.yaml(项目根目录)
arb-dir: lib/l10n
template-arb-file: app_zh.arb
output-localization-file: app_localizations.dart
// MaterialApp 配置
MaterialApp(
onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: _locale,
);
# 生成
flutter gen-l10n
4. 运行时切换语言
class LocaleProvider extends ChangeNotifier {
Locale _locale = const Locale('zh', 'CN');
final SharedPreferences _prefs;
LocaleProvider(this._prefs) {
final saved = _prefs.getString('locale');
if (saved != null) {
final parts = saved.split('_');
_locale = Locale(parts[0], parts.length > 1 ? parts[1] : null);
}
}
Locale get locale => _locale;
void setLocale(Locale locale) {
if (_locale == locale) return;
_locale = locale;
_prefs.setString('locale', locale.toString());
notifyListeners();
}
}
// 在顶层提供
ChangeNotifierProvider(
create: (context) => LocaleProvider(prefs),
child: const MyApp(),
);
// 使用
class LanguageSwitcher extends StatelessWidget {
@override
Widget build(BuildContext context) {
final localeProv = context.watch<LocaleProvider>();
return DropdownButton<Locale>(
value: localeProv.locale,
items: const [
DropdownMenuItem(value: Locale('zh', 'CN'), child: Text('中文')),
DropdownMenuItem(value: Locale('en', 'US'), child: Text('English')),
DropdownMenuItem(value: Locale('ja', 'JP'), child: Text('日本語')),
],
onChanged: (locale) => localeProv.setLocale(locale!),
);
}
}
5. RTL/LTR 文本方向
// 自动适配(根据 Locale)
Directionality(
textDirection: TextDirection.rtl, // 阿拉伯语、希伯来语
child: Text('مرحبا'),
);
// 检测当前方向
bool isRTL = Directionality.of(context) == TextDirection.rtl;
// EdgeInsets 方向适配
final padding = EdgeInsetsDirectional.fromSTEB(16, 8, 16, 8);
// start=左(LTR)/右(RTL), end=右(LTR)/左(RTL)
6. 地区特定格式
import 'package:intl/intl.dart';
// ===== 日期格式 =====
var now = DateTime.now();
var dateFormat = DateFormat.yMMMMd('zh_CN'); // 2026年5月11日
var shortDate = DateFormat.yMd('en_US'); // 5/11/2026
var fullDate = DateFormat.EEEE('ja_JP'); // 月曜日
// 自定义格式
var custom = DateFormat('yyyy-MM-dd HH:mm');
print(custom.format(now)); // 2026-05-11 14:30
// ===== 数字格式 =====
var numFormat = NumberFormat.decimalPattern('zh_CN');
print(numFormat.format(1234567)); // 1,234,567
var currencyFormat = NumberFormat.currency(locale: 'zh_CN', symbol: '¥');
print(currencyFormat.format(99.9)); // ¥99.90
var percentFormat = NumberFormat.percentPattern('en_US');
print(percentFormat.format(0.85)); // 85%
7. 主题系统
// ===== ThemeData 基础 =====
ThemeData({
ColorScheme? colorScheme, // 颜色方案(Material 3 核心)
TextTheme? textTheme, // 文本样式
AppBarTheme? appBarTheme, // AppBar 样式
CardTheme? cardTheme, // Card 样式
ElevatedButtonThemeData? elevatedButtonTheme, // 按钮样式
InputDecorationTheme? inputDecorationTheme, // 输入框样式
// ... 几十种组件主题
})
// ===== Material 3 主题(推荐)=====
ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue, // 种子色,自动生成完整色板
brightness: Brightness.light,
),
);
// 暗黑模式
ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.dark,
),
);
// ===== 动态颜色(Android 12+ Dynamic Color)=====
// flutter pub add dynamic_color
DynamicColorBuilder(
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
ColorScheme lightColorScheme;
ColorScheme darkColorScheme;
if (lightDynamic != null) {
lightColorScheme = lightDynamic;
darkColorScheme = darkDynamic!;
} else {
lightColorScheme = ColorScheme.fromSeed(seedColor: Colors.blue);
darkColorScheme = ColorScheme.fromSeed(seedColor: Colors.blue, brightness: Brightness.dark);
}
return MaterialApp(
theme: ThemeData(colorScheme: lightColorScheme, useMaterial3: true),
darkTheme: ThemeData(colorScheme: darkColorScheme, useMaterial3: true),
themeMode: ThemeMode.system,
home: const HomePage(),
);
},
);
8. 暗黑模式
// ===== 三种模式 =====
ThemeMode.system // 跟随系统
ThemeMode.light // 强制浅色
ThemeMode.dark // 强制深色
// ===== 切换实现 =====
class ThemeProvider extends ChangeNotifier {
ThemeMode _mode = ThemeMode.system;
final SharedPreferences _prefs;
ThemeProvider(this._prefs) {
final saved = _prefs.getString('themeMode');
if (saved != null) {
_mode = ThemeMode.values.firstWhere((m) => m.name == saved, orElse: () => ThemeMode.system);
}
}
ThemeMode get mode => _mode;
void setMode(ThemeMode mode) {
_mode = mode;
_prefs.setString('themeMode', mode.name);
notifyListeners();
}
void toggle() {
setMode(_mode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light);
}
}
// MaterialApp 配置
MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: Brightness.dark),
useMaterial3: true,
),
themeMode: context.watch<ThemeProvider>().mode,
);
9. 自定义主题
// ===== 自定义颜色扩展 =====
class AppColors extends ThemeExtension<AppColors> {
final Color brand;
final Color success;
final Color warning;
const AppColors({
required this.brand,
required this.success,
required this.warning,
});
@override
AppColors copyWith({Color? brand, Color? success, Color? warning}) {
return AppColors(
brand: brand ?? this.brand,
success: success ?? this.success,
warning: warning ?? this.warning,
);
}
@override
AppColors lerp(covariant AppColors other, double t) {
return AppColors(
brand: Color.lerp(brand, other.brand, t)!,
success: Color.lerp(success, other.success, t)!,
warning: Color.lerp(warning, other.warning, t)!,
);
}
}
// 注册到 ThemeData
ThemeData(
extensions: const [
AppColors(brand: Color(0xFF6750A4), success: Colors.green, warning: Colors.orange),
],
);
// 使用
final appColors = Theme.of(context).extension<AppColors>()!;
Container(color: appColors.brand);
// ===== 组件样式覆盖 =====
ThemeData(
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
filled: true,
fillColor: Colors.grey.shade100,
),
cardTheme: CardTheme(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
);
10. 多品牌/多主题方案
// ===== 品牌主题定义 =====
class BrandTheme {
final String name;
final Color seedColor;
final String? logoAsset;
const BrandTheme({required this.name, required this.seedColor, this.logoAsset});
}
const brands = [
BrandTheme(name: 'Default', seedColor: Colors.blue),
BrandTheme(name: 'Brand A', seedColor: Colors.red),
BrandTheme(name: 'Brand B', seedColor: Colors.green),
];
// 切换品牌
void setBrand(BrandTheme brand) {
_currentBrand = brand;
_lightTheme = ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: brand.seedColor),
useMaterial3: true,
);
_darkTheme = ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: brand.seedColor, brightness: Brightness.dark),
useMaterial3: true,
);
notifyListeners();
}
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 翻译不生效 | ARB 文件未生成代码 | 运行 flutter gen-l10n |
| 切换语言不更新 | locale 未 setState | 通过状态管理更新 |
| 暗黑模式部分组件不跟随 | 硬编码颜色 | 使用 Theme.of(context).colorScheme |
| RTL 布局错乱 | 用了 LTR 假设 | 用 EdgeInsetsDirectional / AlignmentDirectional |
| 日期格式异常 | 未指定 locale | DateFormat(pattern, localeStr) |
| ColorScheme 不够用 | 需要自定义颜色 | ThemeExtension 扩展 |
| 字体不生效 | 未在 pubspec.yaml 声明 | 添加 fonts 配置 |
最佳实践
- 使用 flutter gen-l10n 管理翻译,ARB 格式与翻译工具兼容
- 语言设置持久化到 SharedPreferences
- 主题切换用 ThemeMode + Provider/Riverpod
- Material 3 用 ColorScheme.fromSeed 自动生成色板
- 自定义颜色用 ThemeExtension 扩展
- 避免硬编码颜色,统一用 Theme.of(context)
- RTL 语言用 Directional 系列属性
- 日期/数字格式化用 intl 包指定 locale
面试题
Q1: Flutter 国际化的完整流程是什么?ARB 文件是什么?
完整流程:①创建 l10n.yaml 配置文件指定 ARB 目录;②编写 ARB 翻译文件(JSON 格式,每种语言一个);③运行 flutter gen-l10n 生成 Dart 代码;④在 MaterialApp 配置 localizationsDelegates 和 supportedLocales;⑤通过 AppLocalizations.of(context)!.key 访问翻译。ARB(Application Resource Bundle)是标准的翻译文件格式,基于 JSON,支持占位符({name})、复数(plural)、性别(gender)等高级特性,被 Google 翻译工具和 Crowdin/Transifex 等平台支持。
Q2: Flutter 的暗黑模式如何实现?ThemeMode 的三种模式有什么区别?
实现方式:在 MaterialApp 中配置 theme(浅色主题)、darkTheme(深色主题)和 themeMode(模式选择)。ThemeMode.system 跟随系统设置自动切换;ThemeMode.light 强制浅色,忽略系统设置;ThemeMode.dark 强制深色。关键点是 theme 和 darkTheme 必须都配置,缺一个就不会切换。使用 ColorScheme.fromSeed 配合 brightness 参数可以自动生成配套的浅色/深色色板。切换时通过状态管理更新 themeMode,持久化用 SharedPreferences。
Q3: Material 3 的 ColorScheme.fromSeed 是如何工作的?种子色生成色板的原理?
ColorScheme.fromSeed 接收一个种子色(seedColor),通过 HCT 色彩空间算法自动生成完整的 ColorScheme(包含 primary、onPrimary、secondary、tertiary、error、surface 等约 30 种颜色)。原理:种子色映射到 HCT 色彩空间(Hue-Chroma-Tone),通过调整 Tone 值生成不同明暗的变体色,确保所有颜色在视觉上和谐统一且满足 WCAG 对比度要求。开发者只需选一个品牌色,无需手动调配色板。配合 brightness 参数,同一种子色可生成浅色和深色两套方案。
Q4: ThemeExtension 是什么?为什么需要它?
ThemeExtension 允许开发者在 ThemeData 中注册自定义主题数据(如品牌色、渐变色、间距系统),与内置的 ColorScheme/TextTheme 并列存在。需要它的原因:ColorScheme 只提供 Material 规范的颜色槽位,无法添加自定义语义色(如 success、warning、brand);ThemeExtension 让自定义颜色也参与主题切换和 lerp 插值动画。实现方式:继承 ThemeExtension
,实现 copyWith 和 lerp 方法,注册到 ThemeData.extensions,通过 Theme.of(context).extension () 获取。
Q5: Flutter 中如何处理 RTL(从右到左)语言的布局?
Flutter 内置 RTL 支持,当 Locale 为阿拉伯语(ar)、希伯来语(he)等 RTL 语言时,Directionality 自动设为 rtl。布局组件(Row/Column/Padding)使用 Directional 变体:EdgeInsetsDirectional 替代 EdgeInsets(start=右、end=左);AlignmentDirectional 替代 Alignment;SliverAppBar 自动翻转。开发原则:①用 start/end 替代 left/right;②用 EdgeInsetsDirectional 替代 EdgeInsets.only;③图标和箭头需要镜像时用 Transform.flip;④测试时强制 RTL:
Directionality(textDirection: TextDirection.rtl, child: widget)。
Q6: intl 包的 DateFormat 和 NumberFormat 如何处理地区差异?
DateFormat 和 NumberFormat 都接受 locale 参数,根据地区自动选择格式规则。日期差异:同一日期,en_US 格式为 “May 11, 2026”,zh_CN 为 “2026年5月11日”,ja_JP 为 “2026年5月11日”,de_DE 为 “11. Mai 2026”。数字差异:1234567.89,en_US 为 “1,234,567.89”,de_DE 为 “1.234.567,89”,fr_FR 为 “1 234 567,89”。货币差异:¥100(中文)、$100(美式)、100 €(法式)。使用时必须传入 locale 字符串,否则使用系统默认。
Q7: 如何实现运行时切换语言?切换后所有页面都更新吗?
运行时切换语言:通过状态管理(Provider/Riverpod)持有当前 Locale,在 MaterialApp 的 locale 属性中监听变化。切换时更新 Locale 对象并 notifyListeners,MaterialApp 重建后所有依赖 AppLocalizations.of(context) 的 Widget 都会更新。关键机制:AppLocalizations.delegate 是 InheritedWidget,Locale 变化时所有 dependOnInheritedWidget 的 Widget 自动 rebuild。注意事项:①需要在 MaterialApp 层级设置 locale,不能在子 Widget 局部切换;②切换后弹出/关闭的 Dialog 也使用新语言;③持久化语言选择避免重启后恢复。
Q8: Flutter Web 端的国际化有什么特殊处理?
Web 端特殊处理:①URL 路径带语言前缀(/en/about、/zh/about),需要 GoRouter 配合 locale 切换;②浏览器语言检测通过 navigator.language 获取,Flutter 自动处理;③HTML lang 属性需要随 Locale 更新(通过 flutter.js 或自定义脚本);④SEO 考虑:每种语言的页面应独立 URL,便于搜索引擎索引;⑤Cookie/LocalStorage 存储语言偏好;⑥刷新页面时需要从 URL 或 Storage 恢复语言设置;⑦Web 端不支持 dynamic_color 插件,需要 fallback 主题。
相关链接:
- Flutter核心与Widget体系
- Flutter布局与样式
- [[国际化i18n]]
- CSS变量与主题系统