Flutter个性化主题系统:Material Design 3的深度定制
本文基于BeeCount(蜜蜂记账)项目的实际开发经验,深入探讨如何构建灵活、美观的Material Design 3主题系统。
项目背景
BeeCount(蜜蜂记账)是一款开源、简洁、无广告的个人记账应用。所有财务数据完全由用户掌控,支持本地存储和可选的云端同步,确保数据绝对安全。
引言
在现代移动应用开发中,个性化体验已成为用户的基本期望。一个优秀的主题系统不仅能提升应用的视觉效果,更能让用户产生情感连接,提升使用体验。Material Design 3带来了全新的设计理念和技术实现,为Flutter开发者提供了强大的主题定制能力。
BeeCount采用了完全基于Material Design 3的主题系统,支持动态主色调整、深浅模式切换、以及丰富的个性化选项,为用户提供了极具个性的视觉体验。
Material Design 3核心特性
动态颜色系统
Material Design 3最大的亮点是动态颜色系统,它能:
- 自适应配色:根据主色自动生成完整配色方案
- 语义化颜色:每个颜色都有明确的语义和用途
- 无障碍支持:自动保证颜色对比度符合无障碍标准
- 深浅模式:完美支持明暗主题切换
全新的设计语言
- 更大的圆角:更加柔和友好的视觉效果
- 增强的层级:通过颜色和阴影表达信息层级
- 动态形状:组件形状可以跟随主题动态调整
主题架构设计
核心主题类
dart
class BeeTheme {
// 预定义主色方案
static const Color honeyGold = Color(0xFFFFB000);
static const Color forestGreen = Color(0xFF4CAF50);
static const Color oceanBlue = Color(0xFF2196F3);
static const Color sunsetOrange = Color(0xFFFF5722);
static const Color lavenderPurple = Color(0xFF9C27B0);
static const Color cherryRed = Color(0xFFE91E63);
// 预设主色列表
static const List<Color> presetColors = [
honeyGold,
forestGreen,
oceanBlue,
sunsetOrange,
lavenderPurple,
cherryRed,
];
// 生成完整主题数据
static ThemeData createTheme({
required Color primaryColor,
required Brightness brightness,
String? fontFamily,
}) {
final colorScheme = ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: brightness,
);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
fontFamily: fontFamily,
// 应用栏主题
appBarTheme: AppBarTheme(
centerTitle: true,
elevation: 0,
scrolledUnderElevation: 1,
backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface,
titleTextStyle: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
// 卡片主题
cardTheme: CardTheme(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: colorScheme.outlineVariant,
width: 1,
),
),
),
// 输入框主题
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: colorScheme.surfaceVariant.withOpacity(0.5),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: colorScheme.primary,
width: 2,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
// 按钮主题
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size(0, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
shadowColor: Colors.transparent,
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size(0, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
// 列表瓦片主题
listTileTheme: ListTileThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
),
// 底部导航栏主题
navigationBarTheme: NavigationBarThemeData(
height: 72,
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
backgroundColor: colorScheme.surface,
indicatorColor: colorScheme.secondaryContainer,
labelTextStyle: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.selected)) {
return TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: colorScheme.onSecondaryContainer,
);
}
return TextStyle(
fontSize: 12,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
);
}),
),
// 浮动操作按钮主题
floatingActionButtonTheme: FloatingActionButtonThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 3,
highlightElevation: 6,
),
);
}
// 获取主题相关的语义颜色
static BeeColors colorsOf(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final brightness = Theme.of(context).brightness;
return BeeColors(
// 收入颜色 - 使用绿色系
income: brightness == Brightness.light
? const Color(0xFF1B5E20) // 深绿色
: const Color(0xFF4CAF50), // 亮绿色
// 支出颜色 - 使用红色系
expense: brightness == Brightness.light
? const Color(0xFFD32F2F) // 深红色
: const Color(0xFFF44336), // 亮红色
// 转账颜色 - 使用蓝色系
transfer: colorScheme.primary,
// 中性颜色
neutral: colorScheme.onSurfaceVariant,
// 成功状态
success: const Color(0xFF4CAF50),
// 警告状态
warning: const Color(0xFFFF9800),
// 错误状态
error: colorScheme.error,
// 信息状态
info: const Color(0xFF2196F3),
);
}
}
// 语义颜色定义
class BeeColors {
final Color income;
final Color expense;
final Color transfer;
final Color neutral;
final Color success;
final Color warning;
final Color error;
final Color info;
const BeeColors({
required this.income,
required this.expense,
required this.transfer,
required this.neutral,
required this.success,
required this.warning,
required this.error,
required this.info,
});
}
Riverpod主题管理
dart
// 主题模式Provider
final themeModeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.system);
// 主色Provider
final primaryColorProvider = StateProvider<Color>((ref) => BeeTheme.honeyGold);
// 字体Provider(可选)
final fontFamilyProvider = StateProvider<String?>((ref) => null);
// 主题初始化Provider - 处理持久化
final primaryColorInitProvider = FutureProvider<void>((ref) async {
final prefs = await SharedPreferences.getInstance();
// 加载保存的主色
final savedColor = prefs.getInt('primaryColor');
if (savedColor != null) {
ref.read(primaryColorProvider.notifier).state = Color(savedColor);
}
// 加载主题模式
final savedMode = prefs.getString('themeMode');
if (savedMode != null) {
final mode = ThemeMode.values.firstWhere(
(e) => e.name == savedMode,
orElse: () => ThemeMode.system,
);
ref.read(themeModeProvider.notifier).state = mode;
}
// 监听变化并持久化
ref.listen<Color>(primaryColorProvider, (prev, next) async {
final colorValue = next.value;
await prefs.setInt('primaryColor', colorValue);
});
ref.listen<ThemeMode>(themeModeProvider, (prev, next) async {
await prefs.setString('themeMode', next.name);
});
});
// 计算主题数据的Provider
final lightThemeProvider = Provider<ThemeData>((ref) {
final primaryColor = ref.watch(primaryColorProvider);
final fontFamily = ref.watch(fontFamilyProvider);
return BeeTheme.createTheme(
primaryColor: primaryColor,
brightness: Brightness.light,
fontFamily: fontFamily,
);
});
final darkThemeProvider = Provider<ThemeData>((ref) {
final primaryColor = ref.watch(primaryColorProvider);
final fontFamily = ref.watch(fontFamilyProvider);
return BeeTheme.createTheme(
primaryColor: primaryColor,
brightness: Brightness.dark,
fontFamily: fontFamily,
);
});
// 当前主题颜色Provider
final currentBeeColorsProvider = Provider<BeeColors>((ref) {
// 这个Provider需要在Widget中使用,因为需要BuildContext
throw UnimplementedError('Use BeeTheme.colorsOf(context) instead');
});
主题选择器实现
颜色选择器组件
dart
class ColorPickerSheet extends ConsumerWidget {
const ColorPickerSheet({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentColor = ref.watch(primaryColorProvider);
return DraggableScrollableSheet(
initialChildSize: 0.6,
minChildSize: 0.4,
maxChildSize: 0.8,
builder: (context, scrollController) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(20),
),
),
child: Column(
children: [
// 拖拽指示器
Container(
width: 40,
height: 4,
margin: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.4),
borderRadius: BorderRadius.circular(2),
),
),
// 标题
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'选择主题色',
style: Theme.of(context).textTheme.headlineSmall,
),
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('完成'),
),
],
),
),
const Divider(height: 1),
// 颜色网格
Expanded(
child: SingleChildScrollView(
controller: scrollController,
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 预设颜色
Text(
'预设颜色',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
_buildPresetColors(context, ref, currentColor),
const SizedBox(height: 32),
// 自定义颜色
Text(
'自定义颜色',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
_buildCustomColorPicker(context, ref, currentColor),
],
),
),
),
],
),
);
},
);
}
Widget _buildPresetColors(BuildContext context, WidgetRef ref, Color currentColor) {
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 1,
),
itemCount: BeeTheme.presetColors.length,
itemBuilder: (context, index) {
final color = BeeTheme.presetColors[index];
final isSelected = color.value == currentColor.value;
return _ColorSwatch(
color: color,
isSelected: isSelected,
onTap: () {
ref.read(primaryColorProvider.notifier).state = color;
HapticFeedback.selectionClick();
},
);
},
);
}
Widget _buildCustomColorPicker(BuildContext context, WidgetRef ref, Color currentColor) {
return Container(
height: 200,
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
),
borderRadius: BorderRadius.circular(12),
),
child: ColorPicker(
pickerColor: currentColor,
onColorChanged: (Color color) {
ref.read(primaryColorProvider.notifier).state = color;
},
colorPickerWidth: 300,
pickerAreaHeightPercent: 0.7,
enableAlpha: false,
displayThumbColor: true,
showLabel: false,
paletteType: PaletteType.hsl,
pickerAreaBorderRadius: BorderRadius.circular(8),
),
);
}
}
class _ColorSwatch extends StatelessWidget {
final Color color;
final bool isSelected;
final VoidCallback onTap;
const _ColorSwatch({
required this.color,
required this.isSelected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: isSelected
? Border.all(
color: Theme.of(context).colorScheme.outline,
width: 3,
)
: null,
boxShadow: isSelected
? [
BoxShadow(
color: color.withOpacity(0.4),
blurRadius: 8,
spreadRadius: 2,
),
]
: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: isSelected
? const Icon(
Icons.check,
color: Colors.white,
size: 24,
)
: null,
),
);
}
}
主题模式切换器
dart
class ThemeModeSelector extends ConsumerWidget {
const ThemeModeSelector({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentMode = ref.watch(themeModeProvider);
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'外观模式',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
...ThemeMode.values.map((mode) {
return RadioListTile<ThemeMode>(
title: Text(_getThemeModeLabel(mode)),
subtitle: Text(_getThemeModeDescription(mode)),
value: mode,
groupValue: currentMode,
onChanged: (ThemeMode? value) {
if (value != null) {
ref.read(themeModeProvider.notifier).state = value;
HapticFeedback.selectionClick();
}
},
contentPadding: EdgeInsets.zero,
);
}).toList(),
],
),
),
);
}
String _getThemeModeLabel(ThemeMode mode) {
switch (mode) {
case ThemeMode.system:
return '跟随系统';
case ThemeMode.light:
return '浅色模式';
case ThemeMode.dark:
return '深色模式';
}
}
String _getThemeModeDescription(ThemeMode mode) {
switch (mode) {
case ThemeMode.system:
return '根据系统设置自动切换';
case ThemeMode.light:
return '始终使用浅色主题';
case ThemeMode.dark:
return '始终使用深色主题';
}
}
}
主题应用实践
在MaterialApp中应用主题
dart
class BeeCountApp extends ConsumerWidget {
const BeeCountApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// 确保主题初始化完成
final themeInit = ref.watch(primaryColorInitProvider);
return themeInit.when(
data: (_) => _buildApp(ref),
loading: () => _buildLoadingApp(),
error: (_, __) => _buildApp(ref), // 错误时使用默认主题
);
}
Widget _buildApp(WidgetRef ref) {
final themeMode = ref.watch(themeModeProvider);
final lightTheme = ref.watch(lightThemeProvider);
final darkTheme = ref.watch(darkThemeProvider);
return MaterialApp(
title: 'BeeCount',
debugShowCheckedModeBanner: false,
// 主题配置
theme: lightTheme,
darkTheme: darkTheme,
themeMode: themeMode,
// 路由配置
home: const AppScaffold(),
// 国际化配置
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('zh', 'CN'),
Locale('en', 'US'),
],
);
}
Widget _buildLoadingApp() {
return MaterialApp(
home: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
const SizedBox(height: 16),
Text('正在加载主题...'),
],
),
),
),
);
}
}
在组件中使用语义颜色
dart
class TransactionCard extends StatelessWidget {
final Transaction transaction;
const TransactionCard({
Key? key,
required this.transaction,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final colors = BeeTheme.colorsOf(context);
final theme = Theme.of(context);
Color getTransactionColor() {
switch (transaction.type) {
case 'income':
return colors.income;
case 'expense':
return colors.expense;
case 'transfer':
return colors.transfer;
default:
return colors.neutral;
}
}
return Card(
child: ListTile(
leading: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: getTransactionColor().withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
_getTransactionIcon(),
color: getTransactionColor(),
),
),
title: Text(
transaction.note ?? '无备注',
style: theme.textTheme.bodyLarge,
),
subtitle: Text(
DateFormat('MM月dd日 HH:mm').format(transaction.happenedAt),
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
trailing: Text(
'${transaction.type == 'expense' ? '-' : '+'}${transaction.amount.toStringAsFixed(2)}',
style: theme.textTheme.titleMedium?.copyWith(
color: getTransactionColor(),
fontWeight: FontWeight.w600,
),
),
),
);
}
IconData _getTransactionIcon() {
switch (transaction.type) {
case 'income':
return Icons.add;
case 'expense':
return Icons.remove;
case 'transfer':
return Icons.swap_horiz;
default:
return Icons.help_outline;
}
}
}
响应式设计适配
dart
class ResponsiveTheme {
static ThemeData adaptForScreen(
ThemeData baseTheme,
BuildContext context,
) {
final screenSize = MediaQuery.of(context).size;
final isTablet = screenSize.shortestSide >= 600;
if (isTablet) {
return baseTheme.copyWith(
// 平板适配
appBarTheme: baseTheme.appBarTheme.copyWith(
titleTextStyle: baseTheme.appBarTheme.titleTextStyle?.copyWith(
fontSize: 24,
),
),
textTheme: baseTheme.textTheme.copyWith(
headlineLarge: baseTheme.textTheme.headlineLarge?.copyWith(
fontSize: 36,
),
headlineMedium: baseTheme.textTheme.headlineMedium?.copyWith(
fontSize: 30,
),
bodyLarge: baseTheme.textTheme.bodyLarge?.copyWith(
fontSize: 18,
),
),
cardTheme: baseTheme.cardTheme.copyWith(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: baseTheme.colorScheme.outlineVariant,
width: 1,
),
),
),
);
}
return baseTheme;
}
}
主题动画与过渡
颜色过渡动画
dart
class AnimatedColorTransition extends StatefulWidget {
final Widget child;
final Duration duration;
const AnimatedColorTransition({
Key? key,
required this.child,
this.duration = const Duration(milliseconds: 300),
}) : super(key: key);
@override
State<AnimatedColorTransition> createState() => _AnimatedColorTransitionState();
}
class _AnimatedColorTransitionState extends State<AnimatedColorTransition>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_controller.forward();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _animation,
child: widget.child,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
主题切换动画
dart
class ThemeAnimatedSwitcher extends ConsumerWidget {
final Widget child;
const ThemeAnimatedSwitcher({
Key? key,
required this.child,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final primaryColor = ref.watch(primaryColorProvider);
return AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: child,
);
},
child: Container(
key: ValueKey(primaryColor.value),
child: child,
),
);
}
}
主题测试与调试
主题预览工具
dart
class ThemePreviewPage extends ConsumerWidget {
const ThemePreviewPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: const Text('主题预览'),
actions: [
PopupMenuButton<Color>(
onSelected: (color) {
ref.read(primaryColorProvider.notifier).state = color;
},
itemBuilder: (context) => BeeTheme.presetColors
.map((color) => PopupMenuItem(
value: color,
child: Row(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 12),
Text('主色 ${color.value.toRadixString(16).toUpperCase()}'),
],
),
))
.toList(),
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildColorShowcase(context),
const SizedBox(height: 24),
_buildComponentShowcase(context),
],
),
),
);
}
Widget _buildColorShowcase(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final colors = BeeTheme.colorsOf(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'颜色方案',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_ColorChip('Primary', colorScheme.primary),
_ColorChip('Secondary', colorScheme.secondary),
_ColorChip('Surface', colorScheme.surface),
_ColorChip('Error', colorScheme.error),
_ColorChip('Income', colors.income),
_ColorChip('Expense', colors.expense),
_ColorChip('Transfer', colors.transfer),
],
),
],
);
}
Widget _buildComponentShowcase(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'组件预览',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
// 按钮组
Row(
children: [
ElevatedButton(
onPressed: () {},
child: const Text('Elevated'),
),
const SizedBox(width: 8),
FilledButton(
onPressed: () {},
child: const Text('Filled'),
),
const SizedBox(width: 8),
OutlinedButton(
onPressed: () {},
child: const Text('Outlined'),
),
],
),
const SizedBox(height: 16),
// 卡片
Card(
child: ListTile(
leading: CircleAvatar(
child: Icon(Icons.account_balance_wallet),
),
title: Text('示例交易'),
subtitle: Text('12月25日 14:30'),
trailing: Text(
'-128.50',
style: TextStyle(
color: BeeTheme.colorsOf(context).expense,
fontWeight: FontWeight.w600,
),
),
),
),
const SizedBox(height: 16),
// 输入框
TextField(
decoration: InputDecoration(
labelText: '备注',
hintText: '请输入备注信息',
prefixIcon: Icon(Icons.note),
),
),
],
);
}
}
class _ColorChip extends StatelessWidget {
final String label;
final Color color;
const _ColorChip(this.label, this.color);
@override
Widget build(BuildContext context) {
final isDark = color.computeLuminance() < 0.5;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(16),
),
child: Text(
label,
style: TextStyle(
color: isDark ? Colors.white : Colors.black,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
);
}
}
性能优化与最佳实践
主题缓存策略
dart
class ThemeCache {
static final Map<String, ThemeData> _cache = {};
static ThemeData getOrCreate({
required Color primaryColor,
required Brightness brightness,
String? fontFamily,
}) {
final key = '${primaryColor.value}_${brightness.name}_${fontFamily ?? 'default'}';
if (_cache.containsKey(key)) {
return _cache[key]!;
}
final theme = BeeTheme.createTheme(
primaryColor: primaryColor,
brightness: brightness,
fontFamily: fontFamily,
);
_cache[key] = theme;
return theme;
}
static void clearCache() {
_cache.clear();
}
}
主题延迟加载
dart
final themeDataProvider = FutureProvider.family<ThemeData, ThemeConfig>((ref, config) async {
// 模拟主题计算耗时(如自定义字体加载等)
await Future.delayed(const Duration(milliseconds: 100));
return ThemeCache.getOrCreate(
primaryColor: config.primaryColor,
brightness: config.brightness,
fontFamily: config.fontFamily,
);
});
class ThemeConfig {
final Color primaryColor;
final Brightness brightness;
final String? fontFamily;
const ThemeConfig({
required this.primaryColor,
required this.brightness,
this.fontFamily,
});
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ThemeConfig &&
runtimeType == other.runtimeType &&
primaryColor == other.primaryColor &&
brightness == other.brightness &&
fontFamily == other.fontFamily;
@override
int get hashCode =>
primaryColor.hashCode ^ brightness.hashCode ^ fontFamily.hashCode;
}
最佳实践总结
1. 设计原则
- 一致性:确保整个应用的视觉风格统一
- 可访问性:遵循无障碍设计原则
- 响应式:适配不同屏幕尺寸和方向
2. 性能考虑
- 主题缓存:避免重复计算主题数据
- 延迟加载:大型主题资源按需加载
- 内存管理:及时清理不需要的主题缓存
3. 用户体验
- 平滑过渡:主题切换使用动画过渡
- 即时反馈:颜色选择提供实时预览
- 持久化:记住用户的主题偏好
4. 开发体验
- 类型安全:使用强类型的主题API
- 代码复用:提取可复用的主题组件
- 调试工具:提供主题预览和调试界面
实际应用效果
在BeeCount项目中,Material Design 3主题系统带来了显著的价值:
- 用户满意度:个性化主题让用户更有归属感
- 视觉一致性:统一的设计语言提升专业感
- 开发效率:规范的主题系统减少了样式代码
- 维护成本:集中的主题管理便于维护和更新
结语
Material Design 3为Flutter应用带来了全新的设计可能性。通过合理的架构设计、灵活的组件化实现和良好的用户体验设计,我们可以构建出既美观又实用的个性化主题系统。
BeeCount的实践证明,一个好的主题系统不仅能提升应用的视觉效果,更能增强用户的使用体验和情感连接。这对于任何注重用户体验的应用都具有重要价值。
关于BeeCount项目
项目特色
- 🎯 现代架构: 基于Riverpod + Drift + Supabase的现代技术栈
- 📱 跨平台支持: iOS、Android双平台原生体验
- 🔄 云端同步: 支持多设备数据实时同步
- 🎨 个性化定制: Material Design 3主题系统
- 📊 数据分析: 完整的财务数据可视化
- 🌍 国际化: 多语言本地化支持
技术栈一览
- 框架: Flutter 3.6.1+ / Dart 3.6.1+
- 状态管理: Flutter Riverpod 2.5.1
- 数据库: Drift (SQLite) 2.20.2
- 云服务: Supabase 2.5.6
- 图表: FL Chart 0.68.0
- CI/CD: GitHub Actions
开源信息
BeeCount是一个完全开源的项目,欢迎开发者参与贡献:
- 项目主页 : https://github.com/TNT-Likely/BeeCount
- 开发者主页 : https://github.com/TNT-Likely
- 发布下载 : GitHub Releases
参考资源
官方文档
- Material Design 3 - Material Design 3设计规范
- Flutter主题指南 - Flutter官方主题文档
学习资源
- Material Theme Builder - 官方主题构建工具
- Flutter色彩系统 - Material色彩系统详解
本文是BeeCount技术文章系列的第4篇,后续将深入探讨数据可视化、CSV导入导出等话题。如果你觉得这篇文章有帮助,欢迎关注项目并给个Star!