第 11 章 主题与深色模式(完整可运行示例)
Material 3 的主题只需一个种子色,其余色调由框架推导;深色模式通过 themeMode 切换并持久化。
创建步骤
flutter create theme_demo && cd theme_demo;flutter pub add shared_preferences(用于记住主题选择);- 替换
lib/main.dart;点右上角图标切换,杀掉 App 重进验证记忆生效。
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// 启动前读出上次选择的主题,避免首帧闪烁
final prefs = await SharedPreferences.getInstance();
runApp(ThemeDemoApp(prefs: prefs));
}
class ThemeDemoApp extends StatefulWidget {
const ThemeDemoApp({super.key, required this.prefs});
final SharedPreferences prefs;
@override
State<ThemeDemoApp> createState() => _ThemeDemoAppState();
}
class _ThemeDemoAppState extends State<ThemeDemoApp> {
static const _key = 'theme_mode';
ThemeMode _mode = ThemeMode.system;
@override
void initState() {
super.initState();
final saved = widget.prefs.getString(_key);
_mode = ThemeMode.values.firstWhere(
(m) => m.name == saved,
orElse: () => ThemeMode.system,
);
}
Future<void> _toggle() async {
final next = switch (_mode) {
ThemeMode.system => ThemeMode.light,
ThemeMode.light => ThemeMode.dark,
ThemeMode.dark => ThemeMode.system,
};
setState(() => _mode = next);
await widget.prefs.setString(_key, next.name);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
// 浅色与深色各给一套;颜色统一来自 ColorScheme
theme: _buildTheme(Brightness.light),
darkTheme: _buildTheme(Brightness.dark),
themeMode: _mode,
home: HomePage(mode: _mode, onToggle: _toggle),
);
}
ThemeData _buildTheme(Brightness brightness) {
final scheme =
ColorScheme.fromSeed(seedColor: const Color(0xFF2E7D6B), brightness: brightness);
return ThemeData(
useMaterial3: true,
colorScheme: scheme,
scaffoldBackgroundColor:
brightness == Brightness.light ? const Color(0xFFF4F6F5) : null,
appBarTheme: AppBarTheme(
backgroundColor: Colors.transparent,
elevation: 0,
titleTextStyle: TextStyle(
fontSize: 20, fontWeight: FontWeight.w700, color: scheme.onSurface),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: scheme.surface,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: scheme.outlineVariant),
),
),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key, required this.mode, required this.onToggle});
final ThemeMode mode;
final VoidCallback onToggle;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final label = switch (mode) {
ThemeMode.system => '跟随系统',
ThemeMode.light => '浅色',
ThemeMode.dark => '深色',
};
return Scaffold(
appBar: AppBar(
title: const Text('主题示例'),
actions: [
IconButton(
tooltip: label,
icon: Icon(switch (mode) {
ThemeMode.system => Icons.brightness_auto,
ThemeMode.light => Icons.light_mode_outlined,
ThemeMode.dark => Icons.dark_mode_outlined,
}),
onPressed: onToggle,
),
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('当前:$label',
style: const TextStyle(fontWeight: FontWeight.w700)),
const SizedBox(height: 6),
Text('所有颜色来自 ColorScheme,切换主题无需改任何页面代码。',
style: TextStyle(
fontSize: 12.5, color: scheme.onSurfaceVariant)),
],
),
),
),
const SizedBox(height: 16),
const TextField(decoration: InputDecoration(labelText: '输入框会跟随主题')),
const SizedBox(height: 16),
FilledButton(onPressed: () {}, child: const Text('主按钮')),
const SizedBox(height: 10),
OutlinedButton(onPressed: () {}, child: const Text('次按钮')),
],
),
);
}
}
关键点
ColorScheme.fromSeed只需一个品牌色,自动生成整套协调色板(M3 的核心)。- 页面里不要写死颜色:用
Theme.of(context).colorScheme.xxx,否则深色模式下必然出问题。 themeMode三态:system / light / dark,跟随系统是默认最佳实践。- 主题选择要在
runApp之前读出来,否则会先闪一下默认主题。 - 自定义字体:在 pubspec 里声明
fonts后,用TextTheme统一设置,别在每个 Text 上单独指定。
动态效果 · 深浅色切换
主题示例
主按钮