Flutter个性化主题系统:Material Design 3的深度定制
本文基于BeeCount(蜜蜂记账)项目的实际开发经验,深入探讨如何构建灵活、美观的Material Design 3主题系统。
项目背景
BeeCount(蜜蜂记账)是一款开源、简洁、无广告的个人记账应用。所有财务数据完全由用户掌控,支持本地存储和可选的云端同步,确保数据绝对安全。
引言
在现代移动应用开发中,个性化体验已成为用户的基本期望。一个优秀的主题系统不仅能提升应用的视觉效果,更能让用户产生情感连接,提升使用体验。Material Design 3带来了全新的设计理念和技术实现,为Flutter开发者提供了强大的主题定制能力。
BeeCount采用了完全基于Material Design 3的主题系统,支持动态主色调整、深浅模式切换、以及丰富的个性化选项,为用户提供了极具个性的视觉体验。
Material Design 3核心特性
动态颜色系统
Material Design 3最大的亮点是动态颜色系统,它能:
- 自适应配色:根据主色自动生成完整配色方案
- 语义化颜色:每个颜色都有明确的语义和用途
- 无障碍支持:自动保证颜色对比度符合无障碍标准
- 深浅模式:完美支持明暗主题切换
全新的设计语言
- 更大的圆角:更加柔和友好的视觉效果
- 增强的层级:通过颜色和阴影表达信息层级
- 动态形状:组件形状可以跟随主题动态调整
主题架构设计
核心主题类
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主题管理
// 主题模式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中使用,因为需要BuildContextthrow UnimplementedError('Use BeeTheme.colorsOf(context) instead');
});
主题选择器实现
颜色选择器组件
class ColorPickerSheet extends ConsumerWidget {const ColorPickerSheet({Key? key}) : super(key: key);@overrideWidget 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,});@overrideWidget 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,),);}
}
主题模式切换器
class ThemeModeSelector extends ConsumerWidget {const ThemeModeSelector({Key? key}) : super(key: key);@overrideWidget 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中应用主题
class BeeCountApp extends ConsumerWidget {const BeeCountApp({Key? key}) : super(key: key);@overrideWidget 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('正在加载主题...'),],),),),);}
}
在组件中使用语义颜色
class TransactionCard extends StatelessWidget {final Transaction transaction;const TransactionCard({Key? key,required this.transaction,}) : super(key: key);@overrideWidget 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;}}
}
响应式设计适配
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;}
}
主题动画与过渡
颜色过渡动画
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);@overrideState<AnimatedColorTransition> createState() => _AnimatedColorTransitionState();
}class _AnimatedColorTransitionState extends State<AnimatedColorTransition>with SingleTickerProviderStateMixin {late AnimationController _controller;late Animation<double> _animation;@overridevoid initState() {super.initState();_controller = AnimationController(duration: widget.duration,vsync: this,);_animation = CurvedAnimation(parent: _controller,curve: Curves.easeInOut,);}@overridevoid didChangeDependencies() {super.didChangeDependencies();_controller.forward();}@overrideWidget build(BuildContext context) {return FadeTransition(opacity: _animation,child: widget.child,);}@overridevoid dispose() {_controller.dispose();super.dispose();}
}
主题切换动画
class ThemeAnimatedSwitcher extends ConsumerWidget {final Widget child;const ThemeAnimatedSwitcher({Key? key,required this.child,}) : super(key: key);@overrideWidget 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,),);}
}
主题测试与调试
主题预览工具
class ThemePreviewPage extends ConsumerWidget {const ThemePreviewPage({Key? key}) : super(key: key);@overrideWidget 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);@overrideWidget 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,),),);}
}
性能优化与最佳实践
主题缓存策略
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();}
}
主题延迟加载
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,});@overridebool operator ==(Object other) =>identical(this, other) ||other is ThemeConfig &&runtimeType == other.runtimeType &&primaryColor == other.primaryColor &&brightness == other.brightness &&fontFamily == other.fontFamily;@overrideint 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!