Flutter 主题管理:构建一致的用户界面
掌握 Flutter 主题管理的核心概念和最佳实践。
一、主题管理的重要性
作为一名追求像素级还原的 UI 匠人,我深知主题管理在 Flutter 开发中的重要性。良好的主题管理能够确保应用在不同设备和场景下保持一致的视觉风格,提高代码的可维护性和可扩展性。从浅色模式到深色模式,从品牌色彩到字体样式,主题管理为我们提供了统一的样式控制机制。
二、基础主题
1. 内置主题
dart
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter 主题示例',
theme: ThemeData(
primaryColor: Colors.blue,
accentColor: Colors.purple,
brightness: Brightness.light,
fontFamily: 'Roboto',
textTheme: TextTheme(
headline1: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
bodyText1: TextStyle(fontSize: 16),
),
),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('主题示例')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Hello, Flutter!', style: Theme.of(context).textTheme.headline1),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {},
child: Text('按钮'),
),
],
),
),
);
}
}
2. 深色模式
dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter 主题示例',
theme: ThemeData(
primaryColor: Colors.blue,
accentColor: Colors.purple,
brightness: Brightness.light,
),
darkTheme: ThemeData(
primaryColor: Colors.blueGrey,
accentColor: Colors.purpleAccent,
brightness: Brightness.dark,
),
themeMode: ThemeMode.system, // 跟随系统设置
home: HomePage(),
);
}
}
三、自定义主题
1. 主题数据类
dart
class AppTheme {
static final lightTheme = ThemeData(
primaryColor: Color(0xFF667eea),
secondaryHeaderColor: Color(0xFF764ba2),
backgroundColor: Colors.white,
scaffoldBackgroundColor: Color(0xFFf5f5f5),
textTheme: TextTheme(
headline1: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black),
bodyText1: TextStyle(fontSize: 16, color: Colors.black87),
bodyText2: TextStyle(fontSize: 14, color: Colors.black54),
),
buttonTheme: ButtonThemeData(
buttonColor: Color(0xFF667eea),
textTheme: ButtonTextTheme.primary,
),
);
static final darkTheme = ThemeData(
primaryColor: Color(0xFF8b5cf6),
secondaryHeaderColor: Color(0xFFa78bfa),
backgroundColor: Color(0xFF121212),
scaffoldBackgroundColor: Color(0xFF1e1e1e),
textTheme: TextTheme(
headline1: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white),
bodyText1: TextStyle(fontSize: 16, color: Colors.white70),
bodyText2: TextStyle(fontSize: 14, color: Colors.white54),
),
buttonTheme: ButtonThemeData(
buttonColor: Color(0xFF8b5cf6),
textTheme: ButtonTextTheme.primary,
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter 主题示例',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
home: HomePage(),
);
}
}
2. 主题扩展
dart
extension ThemeExtension on BuildContext {
Color get primaryColor => Theme.of(this).primaryColor;
Color get secondaryColor => Theme.of(this).secondaryHeaderColor;
Color get backgroundColor => Theme.of(this).backgroundColor;
TextStyle? get headline1 => Theme.of(this).textTheme.headline1;
TextStyle? get bodyText1 => Theme.of(this).textTheme.bodyText1;
TextStyle? get bodyText2 => Theme.of(this).textTheme.bodyText2;
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('主题示例')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Hello, Flutter!', style: context.headline1),
SizedBox(height: 20),
Container(
width: 200,
height: 200,
color: context.primaryColor,
child: Center(
child: Text('主题颜色', style: TextStyle(color: Colors.white)),
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {},
child: Text('按钮'),
),
],
),
),
);
}
}
四、动态主题切换
1. 主题状态管理
dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final themeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.system);
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ProviderScope(
child: Consumer(
builder: (context, ref, child) {
final themeMode = ref.watch(themeProvider);
return MaterialApp(
title: 'Flutter 主题示例',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: themeMode,
home: HomePage(),
);
},
),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('主题示例')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Hello, Flutter!', style: context.headline1),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
context.read(themeProvider).state = ThemeMode.light;
},
child: Text('浅色模式'),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: () {
context.read(themeProvider).state = ThemeMode.dark;
},
child: Text('深色模式'),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: () {
context.read(themeProvider).state = ThemeMode.system;
},
child: Text('跟随系统'),
),
],
),
),
);
}
}
2. 保存主题设置
dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ThemeManager {
static const String _themeKey = 'theme_mode';
static Future<ThemeMode> getThemeMode() async {
final prefs = await SharedPreferences.getInstance();
final themeModeString = prefs.getString(_themeKey);
switch (themeModeString) {
case 'light':
return ThemeMode.light;
case 'dark':
return ThemeMode.dark;
default:
return ThemeMode.system;
}
}
static Future<void> setThemeMode(ThemeMode themeMode) async {
final prefs = await SharedPreferences.getInstance();
String themeModeString;
switch (themeMode) {
case ThemeMode.light:
themeModeString = 'light';
break;
case ThemeMode.dark:
themeModeString = 'dark';
break;
default:
themeModeString = 'system';
}
await prefs.setString(_themeKey, themeModeString);
}
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
ThemeMode _themeMode = ThemeMode.system;
bool _isLoaded = false;
@override
void initState() {
super.initState();
_loadTheme();
}
Future<void> _loadTheme() async {
final themeMode = await ThemeManager.getThemeMode();
setState(() {
_themeMode = themeMode;
_isLoaded = true;
});
}
void _changeTheme(ThemeMode mode) {
setState(() {
_themeMode = mode;
});
ThemeManager.setThemeMode(mode);
}
@override
Widget build(BuildContext context) {
if (!_isLoaded) {
return Container(color: Colors.white);
}
return MaterialApp(
title: 'Flutter 主题示例',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: _themeMode,
home: HomePage(
currentTheme: _themeMode,
onThemeChange: _changeTheme,
),
);
}
}
class HomePage extends StatelessWidget {
final ThemeMode currentTheme;
final Function(ThemeMode) onThemeChange;
HomePage({required this.currentTheme, required this.onThemeChange});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('主题示例')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('当前主题: ${_getThemeName(currentTheme)}', style: context.headline1),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => onThemeChange(ThemeMode.light),
child: Text('浅色模式'),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: () => onThemeChange(ThemeMode.dark),
child: Text('深色模式'),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: () => onThemeChange(ThemeMode.system),
child: Text('跟随系统'),
),
],
),
),
);
}
String _getThemeName(ThemeMode mode) {
switch (mode) {
case ThemeMode.light:
return '浅色';
case ThemeMode.dark:
return '深色';
default:
return '系统';
}
}
}
五、实战案例
1. 完整的主题系统
dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
// 主题数据
class AppTheme {
static final lightTheme = ThemeData(
primaryColor: Color(0xFF667eea),
secondaryHeaderColor: Color(0xFF764ba2),
backgroundColor: Colors.white,
scaffoldBackgroundColor: Color(0xFFf5f5f5),
textTheme: TextTheme(
headline1: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black),
bodyText1: TextStyle(fontSize: 16, color: Colors.black87),
bodyText2: TextStyle(fontSize: 14, color: Colors.black54),
),
buttonTheme: ButtonThemeData(
buttonColor: Color(0xFF667eea),
textTheme: ButtonTextTheme.primary,
),
cardTheme: CardTheme(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
);
static final darkTheme = ThemeData(
primaryColor: Color(0xFF8b5cf6),
secondaryHeaderColor: Color(0xFFa78bfa),
backgroundColor: Color(0xFF121212),
scaffoldBackgroundColor: Color(0xFF1e1e1e),
textTheme: TextTheme(
headline1: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white),
bodyText1: TextStyle(fontSize: 16, color: Colors.white70),
bodyText2: TextStyle(fontSize: 14, color: Colors.white54),
),
buttonTheme: ButtonThemeData(
buttonColor: Color(0xFF8b5cf6),
textTheme: ButtonTextTheme.primary,
),
cardTheme: CardTheme(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
color: Color(0xFF2d2d2d),
),
);
}
// 主题扩展
extension ThemeExtension on BuildContext {
Color get primaryColor => Theme.of(this).primaryColor;
Color get secondaryColor => Theme.of(this).secondaryHeaderColor;
Color get backgroundColor => Theme.of(this).backgroundColor;
TextStyle? get headline1 => Theme.of(this).textTheme.headline1;
TextStyle? get bodyText1 => Theme.of(this).textTheme.bodyText1;
TextStyle? get bodyText2 => Theme.of(this).textTheme.bodyText2;
CardTheme get cardTheme => Theme.of(this).cardTheme;
}
// 主题状态管理
final themeProvider = StateNotifierProvider<ThemeNotifier, ThemeMode>((ref) {
return ThemeNotifier();
});
class ThemeNotifier extends StateNotifier<ThemeMode> {
ThemeNotifier() : super(ThemeMode.system) {
_loadTheme();
}
Future<void> _loadTheme() async {
final prefs = await SharedPreferences.getInstance();
final themeModeString = prefs.getString('theme_mode');
switch (themeModeString) {
case 'light':
state = ThemeMode.light;
break;
case 'dark':
state = ThemeMode.dark;
break;
default:
state = ThemeMode.system;
}
}
Future<void> setThemeMode(ThemeMode mode) async {
state = mode;
final prefs = await SharedPreferences.getInstance();
String themeModeString;
switch (mode) {
case ThemeMode.light:
themeModeString = 'light';
break;
case ThemeMode.dark:
themeModeString = 'dark';
break;
default:
themeModeString = 'system';
}
await prefs.setString('theme_mode', themeModeString);
}
}
// 应用入口
void main() {
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, child) {
final themeMode = ref.watch(themeProvider);
return MaterialApp(
title: 'Flutter 主题示例',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: themeMode,
home: HomePage(),
);
},
);
}
}
// 首页
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final themeNotifier = context.read(themeProvider.notifier);
return Scaffold(
appBar: AppBar(
title: Text('主题管理示例'),
actions: [
PopupMenuButton<ThemeMode>(
onSelected: (mode) {
themeNotifier.setThemeMode(mode);
},
itemBuilder: (context) => [
PopupMenuItem(
value: ThemeMode.light,
child: Text('浅色模式'),
),
PopupMenuItem(
value: ThemeMode.dark,
child: Text('深色模式'),
),
PopupMenuItem(
value: ThemeMode.system,
child: Text('跟随系统'),
),
],
),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('主题管理', style: context.headline1),
SizedBox(height: 20),
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('卡片标题', style: context.bodyText1),
SizedBox(height: 8),
Text('这是卡片内容,展示了主题在不同模式下的表现。', style: context.bodyText2),
],
),
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {},
child: Text('测试按钮'),
),
SizedBox(height: 20),
Container(
width: double.infinity,
height: 100,
decoration: BoxDecoration(
color: context.primaryColor,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text('主题颜色', style: TextStyle(color: Colors.white, fontSize: 18)),
),
),
],
),
),
);
}
}
六、最佳实践
- 统一管理:将主题相关的代码集中管理,便于维护
- 使用扩展:创建主题扩展,简化主题属性的访问
- 保存设置:使用 SharedPreferences 保存用户的主题偏好
- 响应式设计:确保主题在不同屏幕尺寸下都能正常显示
- 测试:在不同主题模式下测试应用的表现
- 性能优化:避免频繁的主题切换导致的重建
七、常见问题
1. 主题切换不生效
dart
// 确保使用 Consumer 或 ProviderListener 监听主题变化
Consumer(
builder: (context, ref, child) {
final themeMode = ref.watch(themeProvider);
return MaterialApp(
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: themeMode,
home: HomePage(),
);
},
)
2. 主题属性不应用
dart
// 确保正确使用主题属性
Text('文本', style: Theme.of(context).textTheme.bodyText1);
// 或使用扩展
Text('文本', style: context.bodyText1);
3. 性能问题
dart
// 避免在 build 方法中创建主题数据
// 正确的做法是在类级别定义主题数据
class AppTheme {
static final lightTheme = ThemeData(...);
static final darkTheme = ThemeData(...);
}
良好的主题管理能够确保应用的视觉一致性,提升用户体验,同时简化代码维护。
#flutter #theme #ui #dark-mode #dart