主题与国际化是App走向专业的关键一步。一个精心设计的主题系统让你的App拥有一致的视觉语言,而国际化(i18n)则让你的App触达全球用户。本课将深入探讨Flutter的ThemeData体系、动态主题切换机制,以及基于flutter_localizations和intl包的多语言实现。
ThemeData是Flutter主题的灵魂,它定义了App的全局视觉规范。理解其核心属性,才能打造出真正一致的设计系统。
// ThemeData核心属性一览
ThemeData(
// 颜色体系 - Material 3的ColorScheme
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.light, // 或 .dark
),
// 文字主题
textTheme: TextTheme(
displayLarge: TextStyle(fontSize: 57, fontWeight: FontWeight.w400),
headlineMedium: TextStyle(fontSize: 28, fontWeight: FontWeight.w400),
titleLarge: TextStyle(fontSize: 22, fontWeight: FontWeight.w500),
bodyLarge: TextStyle(fontSize: 16, fontWeight: FontWeight.w400),
bodyMedium: TextStyle(fontSize: 14, fontWeight: FontWeight.w400),
labelLarge: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
// 组件主题 - 统一组件外观
appBarTheme: AppBarTheme(
centerTitle: true,
elevation: 0,
backgroundColor: Colors.transparent,
),
cardTheme: CardTheme(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
)
ThemeData的xxxTheme属性统一管理,确保整个App的视觉一致性。修改一处,全局生效。
实现亮/暗模式切换需要状态管理。我们用ValueNotifier+InheritedWidget构建轻量级的主题管理方案。
// 主题管理器 - 支持亮色/暗色/跟随系统
class ThemeManager extends ChangeNotifier {
ThemeMode _mode = ThemeMode.system;
Color _seedColor = Colors.deepPurple;
ThemeMode get mode => _mode;
Color get seedColor => _seedColor;
bool get isDark => _mode == ThemeMode.dark;
void setMode(ThemeMode mode) {
if (_mode != mode) {
_mode = mode;
notifyListeners();
}
}
void toggleMode() {
_mode = _mode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
notifyListeners();
}
void setSeedColor(Color color) {
_seedColor = color;
notifyListeners();
}
// 生成亮色主题
ThemeData get lightTheme => ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: _seedColor,
brightness: Brightness.light,
),
useMaterial3: true,
appBarTheme: const AppBarTheme(centerTitle: true, elevation: 0),
);
// 生成暗色主题
ThemeData get darkTheme => ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: _seedColor,
brightness: Brightness.dark,
),
useMaterial3: true,
appBarTheme: const AppBarTheme(centerTitle: true, elevation: 0),
);
}
// 在MaterialApp中使用
class MyApp extends StatelessWidget {
final ThemeManager _themeManager = ThemeManager();
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: _themeManager,
builder: (context, _) {
return MaterialApp(
theme: _themeManager.lightTheme,
darkTheme: _themeManager.darkTheme,
themeMode: _themeManager.mode,
home: const HomePage(),
);
},
);
}
}
Flutter的国际化需要三个包协同工作:flutter_localizations提供基础框架,intl提供消息格式化,intl_utils(代码生成工具)让维护更轻松。
// pubspec.yaml 配置
dependencies:
flutter_localizations:
sdk: flutter
intl: ^0.19.0
flutter:
generate: true # 启用代码生成
// l10n.yaml 配置文件(项目根目录)
arb-dir: lib/l10n
template-arb-file: app_zh.arb
output-localization-file: app_localizations.dart
ARB(Application Resource Bundle)是标准的翻译文件格式。每个语言一个ARB文件,使用{placeholder}语法支持变量。
// lib/l10n/app_zh.arb (中文 - 模板语言)
{
"@@locale": "zh",
"appTitle": "我的应用",
"@appTitle": { "description": "应用标题" },
"helloUser": "你好,{name}!",
"@helloUser": {
"description": "欢迎用户",
"placeholders": {
"name": { "type": "String" }
}
},
"itemCount": "{count, plural, =0{没有项目} =1{1个项目} other{{count}个项目}}",
"@itemCount": {
"description": "项目数量",
"placeholders": {
"count": { "type": "int" }
}
},
"lastUpdated": "最后更新:{date}",
"@lastUpdated": {
"description": "最后更新时间",
"placeholders": {
"date": { "type": "DateTime", "format": "yMd" }
}
}
}
// lib/l10n/app_en.arb (英文)
{
"@@locale": "en",
"appTitle": "My App",
"helloUser": "Hello, {name}!",
"itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
"lastUpdated": "Last updated: {date}"
}
// MaterialApp配置
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: _currentLocale, // 动态切换语言
home: const HomePage(),
);
// 在Widget中使用翻译
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.appTitle)),
body: Column(
children: [
Text(l10n.helloUser('小明')),
Text(l10n.itemCount(5)), // 输出: 5个项目
Text(l10n.lastUpdated(DateTime.now())),
],
),
);
}
}
// 语言切换
class LocaleProvider extends ChangeNotifier {
Locale _locale = const Locale('zh');
Locale get locale => _locale;
void setLocale(String langCode) {
_locale = Locale(langCode);
notifyListeners();
}
}
flutter gen-l10n生成翻译代码initState中使用AppLocalizations.of(context)——context尚未挂载// 使用shared_preferences持久化主题
class ThemeRepository {
static const _keyMode = 'theme_mode';
static const _keySeedColor = 'theme_seed_color';
final SharedPreferences _prefs;
ThemeMode loadMode() {
final saved = _prefs.getString(_keyMode);
switch (saved) {
case 'light': return ThemeMode.light;
case 'dark': return ThemeMode.dark;
default: return ThemeMode.system;
}
}
Future<void> saveMode(ThemeMode mode) async {
await _prefs.setString(_keyMode, mode.name);
}
Color loadSeedColor() {
final value = _prefs.getInt(_keySeedColor);
return value != null ? Color(value) : Colors.deepPurple;
}
Future<void> saveSeedColor(Color color) async {
await _prefs.setInt(_keySeedColor, color.value);
}
}
// 系统主题变化监听
class SystemThemeObserver extends WidgetsBindingObserver {
final VoidCallback onChanged;
SystemThemeObserver(this.onChanged);
@override
void didChangePlatformBrightness() {
onChanged();
}
}
// 完整的主题+国际化设置页面
class SettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final themeMgr = context.watch<ThemeManager>();
return Scaffold(
appBar: AppBar(title: Text(l10n.appTitle)),
body: ListView(
children: [
// 主题模式选择
_SectionTitle(l10n.themeMode),
SwitchListTile(
title: Text('深色模式'),
value: themeMgr.isDark,
onChanged: (_) => themeMgr.toggleMode(),
),
// 主题色选择
_SectionTitle('主题色'),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Wrap(
spacing: 12,
children: [
Colors.deepPurple, Colors.teal, Colors.orange,
Colors.pink, Colors.indigo, Colors.green,
].map((color) => ChoiceChip(
label: Container(width: 24, height: 24,
decoration: BoxDecoration(color: color,
shape: BoxShape.circle)),
selected: themeMgr.seedColor == color,
onSelected: (_) => themeMgr.setSeedColor(color),
)).toList(),
),
),
// 语言选择
_SectionTitle(l10n.language),
RadioListTile<String>(
title: Text('中文'),
value: 'zh',
groupValue: Localizations.localeOf(context).languageCode,
onChanged: (v) => context.read<LocaleProvider>().setLocale(v!),
),
RadioListTile<String>(
title: Text('English'),
value: 'en',
groupValue: Localizations.localeOf(context).languageCode,
onChanged: (v) => context.read<LocaleProvider>().setLocale(v!),
),
],
),
);
}
}
当主题切换时,路由转场动画也应随之变化——暗色主题使用深色过渡,亮色主题使用浅色过渡。通过PageRouteBuilder配合Theme.of(context)实现主题感知的路由动画。
// 主题感知的路由转场
class ThemedPageRoute<T> extends PageRouteBuilder<T> {
final Widget page;
ThemedPageRoute({required this.page})
: super(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
)),
child: FadeTransition(
opacity: animation,
child: Container(
// 暗色主题用深色背景遮罩
foregroundDecoration: BoxDecoration(
color: isDark
? Colors.black.withOpacity(0.1 * (1 - animation.value))
: Colors.white.withOpacity(0.1 * (1 - animation.value)),
),
child: child,
),
),
);
},
);
}
// 使用:Navigator.push(context, ThemedPageRoute(page: DetailPage()));
直接切换主题会导致颜色突变。使用AnimatedTheme让颜色平滑过渡:
// 平滑主题过渡
// 方案1:使用MaterialApp内置动画(推荐)
// MaterialApp的theme/darkTheme切换自带约200ms的颜色过渡动画
// 只要使用ListenableBuilder/Consumer包裹MaterialApp即可
// 方案2:局部区域的主题动画
class AnimatedSection extends StatelessWidget {
final Widget child;
@override
Widget build(BuildContext context) {
return TweenAnimationBuilder<Color>(
tween: ColorTween(
begin: Theme.of(context).colorScheme.surface,
end: Theme.of(context).colorScheme.surface,
),
duration: const Duration(milliseconds: 300),
builder: (context, color, child) {
return Container(
color: color,
child: child,
);
},
child: child,
);
}
}
// 方案3:使用AnimatedContainer包裹需要过渡的元素
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(16),
),
child: content,
)
Theme.of(context)获取颜色而非硬编码。这样主题切换后,所有引用主题色的组件自动更新,无需手动管理每个组件的颜色状态。
AnimatedTheme)你已掌握Flutter主题系统与国际化!你的App现在可以:
下一课预告:我们将用所学知识构建一个完整的待办事项App——CRUD、状态管理、本地持久化,一站式实战!