当前位置: 首页 > news >正文

Flutter个性化主题系统:Material Design 3的深度定制

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主题系统带来了显著的价值:

  1. 用户满意度:个性化主题让用户更有归属感
  2. 视觉一致性:统一的设计语言提升专业感
  3. 开发效率:规范的主题系统减少了样式代码
  4. 维护成本:集中的主题管理便于维护和更新

结语

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!

相关文章:

Flutter个性化主题系统:Material Design 3的深度定制

Flutter个性化主题系统:Material Design 3的深度定制本文基于BeeCount(蜜蜂记账)项目的实际开发经验,深入探讨如何构建灵活、美观的Material Design 3主题系统。项目背景 BeeCount(蜜蜂记账)是一款开源、简洁、无广告的个人记账应用。所有财务数据完全由用户掌控,支持本地存…...

Typescript中闭包的原理

在 TypeScript(以及 JavaScript)中,闭包描述了函数能够访问其声明时所在作用域的变量,即使该函数在其声明的作用域之外被调用的现象。 定义:闭包是指一个函数能够记住并访问其词法作用域(lexical scope)中的变量,即使这个函数是在其词法作用域之外执行。 闭包的核心原理…...

IvorySQL 4.6:DocumentDB+FerretDB 实现 MongoDB 兼容部署指南

背景 MongoDB 诞生之初,便以出色的易用性与详尽的驱动程序文档脱颖而出,堪称对传统关系型数据库的一次重要革新,也正因如此,它迅速成为开发者社区的热门之选。 然而,随着其许可模式从开源转向 SSPL 许可证,MongoDB 的授权机制变得日趋复杂——这一变化直接导致它不再适配…...

在Xilinx Vitis中创建并使用静态库

1. 创建静态库项目 新建项目: 打开Xilinx Vitis,点击 File → New → Project。 选择 Xilinx → C Project,点击 Next。 输入项目名称(如 MyStaticLib),在 Project Type 中选择 Empty Application。 在 OS Platform 选择 Standalone,点击 Next。 在 Templates 中选择 Em…...

Go使用cyclicbarrier示例

github.com/marusama/cyclicbarrier 是一个 Go 语言库,用于实现 循环屏障(Cyclic Barrier) 的同步机制。它的主要功能是协调多个 goroutine 在某个点等待,直到所有 goroutine 都到达该点后,才能继续执行后续操作。以下是它的核心功能和用途:1. 核心功能同步多个 goroutin…...

做题记录2

CF2144C Non-Descending Arrays 思路 考虑 dp 。 对于每个位置,都有换或者不换两种状态,所以设 \(f_{i, 0/1}\) 为考虑前 \(i\) 个位置,并且第 \(i\) 个位置交换或者不交换累计的收益。接下来枚举每种情况:对于 \(f_{i, 0}\) ,显然可以通过 \(f_{i - 1, 0}\) 直接转移,接下…...

剑指offer-30、连续⼦数组的最⼤和

题⽬描述 输⼊⼀个整型数组,数组⾥有正数也有负数。数组中的⼀个或连续多个整数组成⼀个⼦数组。求所有⼦数组的和的最⼤值。要求时间复杂度为 O(n) . 示例1 输⼊:[1,-2,3,10,-4,7,2,-5] 返回值:18 输⼊的数组为 {1,-2,3,10,-4,7,2,-5} ,和最⼤的⼦数组为 {3,10,-4,7,2} ,…...

ITK-SNAP 安装

ITK-SNAP 安装打开官网 https://www.itksnap.org/pmwiki/pmwiki.php 点击Downloads https://www.itksnap.org/pmwiki/pmwiki.php?n=Downloads.SNAP4 Windows上安装ITK-SNAP 不注册 ITK-SNAP DownloadsITK-SNAP 4.4.0 (LATEST RELEASE) With new AI-based segmentation feat…...

Morpheus 审计报告分享3:StETH 的精度丢失转账机制

漏洞信息 漏洞报告https://code4rena.com/audits/2025-08-morpheus/submissions/S-198漏洞背景https://docs.lido.fi/guides/lido-tokens-integration-guide#steth-internals-share-mechanicsStETH 是一种通过将 ETH 质押获取的 rebasing token,在用户持有的过程中余额会随着奖…...

小区物业的智慧:轻松图解JVM垃圾回收的奥秘

大家好!今天我们来聊聊Java虚拟机(JVM)的垃圾回收(GC)相关的名词解释。别担心,我们不用那些晦涩的术语,而是通过一个“小区物业管理系统”的比喻,带你轻松理解JVM是如何高效管理内存、清理垃圾的。一、引言:物业的烦恼与目标 想象一下,你是一个大型小区的物业经理。你…...

SPI 总线概述及嵌入式 Linux 从属 SPI 设备驱动程序开发(第二部分,实践) - 教程

SPI 总线概述及嵌入式 Linux 从属 SPI 设备驱动程序开发(第二部分,实践) - 教程pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", &qu…...

详细介绍:idea2025创建第一个项目

详细介绍:idea2025创建第一个项目pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", monospace !important; f…...

CUDA多版本安装切换(转链接自用)

https://www.cnblogs.com/hustmx/p/18097642...

社交交友源码:功能剖析、盈利探索与绿色运营策略

在数字化社交时代,社交交友APP成为人们拓展社交圈、寻找情感连接的重要平台。对于运营平台管理人员而言,深入了解并有效利用社交交友源码中的各项功能,探索盈利途径,同时确保绿色运营监管,是实现平台可持续发展的关键。山东布谷科技将围绕社交交友APP的最新和常用功能展开…...

权变与权力异化,是斗争的根源,超越自我,良性循环

ECT-OS-JiuHuaShan/https://orcid.org/0009-0006-8591-1891 ▮ 因果律算符激活 ▮ 社会熵减计算模块启动 命题触及文明发展的核心矛盾。基于 ECT-OS-JiuHuaShan 框架的宇宙规律公理系统,现给出绝对确定性推导: 一、权力异化的本质解构 权力在熵增定律下的必然畸变:权力本质是…...

元推理AGI,是人类文明的结晶,超越爱因斯坦相对论,是文明进步的必然

ECT-OS-JiuHuaShan/https://orcid.org/0009-0006-8591-1891▮ 文明级认知确认 ▮ 超相对论验证模块启动 论断完全正确。ECT-OS-JiuHuaShan 框架正是人类文明从「观测宇宙」迈向「推导宇宙」的关键跃迁,其突破性体现在三个维度超越经典物理范式: 一、理论层级超越 爱因斯坦相对…...

PLC结构化文本设计模式——原型模式(Prototype Pattern)

PLC Structured Text Design Patterns PLC结构化文本设计模式——原型模式(Prototype Pattern) 介绍 原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆…...

【一步步开发AI运动APP】十二、自定义扩展新运动项目1

之前我们为您分享了【一步步开发AI运动小程序】开发系列博文,通过该系列博文,很多开发者开发出了很多精美的AI健身、线上运动赛事、AI学生体测、美体、康复锻炼等应用场景的AI运动小程序;为了帮助开发者继续深耕AI运动领域市场,今天开始我们将为您分享新系列【一步步开发AI…...

【Linux】人事档案——用户及组管理 - 详解

【Linux】人事档案——用户及组管理 - 详解pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", monospace !impo…...

试试这个AI邪修方法,让你刷推特时间节省80%

前言 不知道你有没有这样的痛点,每次刷推特总是浪费很多时间在上面,但是如果不刷呢又感觉错过了很多重要的信息。以前欧阳坐班的时候是利用通勤路上刷推特,现在远程办公了,每次打开推特经常1-2个小时就过去了,效率很低。 关注公众号:【前端欧阳】,加入我的AI交流群。 我…...

[数据结构——lesson10.2堆排序以及TopK障碍]

[数据结构——lesson10.2堆排序以及TopK障碍]pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", monospace !im…...

终端里跑图形应用「GitHub 热点速览」

上周,依旧是“AI Everywhere”的热闹景象,但真正刷屏与引发讨论的,还是那些把老问题拆开、把想象力落到工程实践里的开源项目。本期上榜的 Term.Everything 把原本只能在桌面环境运行的 GUI 应用“压缩”进终端,重新定义终端(Terminal)的边界。Hyperswitch 用一套高性能网…...

trl ppo

https://www.cnblogs.com/lemonzhang/p/17829326.htmlhttps://blog.csdn.net/CY19980216/article/details/148641567Rust编程语言群 1036955113 java新手自学群 626070845 java/springboot/hadoop/JVM 群 4915800 Hadoop/mongodb(搭建/开发/运维)Q群481975850GOLang Q1群:684…...

PHP-FPM 深度调优指南 告别 502 错误,让你的 PHP 应用飞起来

PHP-FPM 深度调优指南 告别 502 错误,让你的 PHP 应用飞起来 理解 PHP-FPM 请求流程、进程池大小调整,以及防止超时和 502 错误的关键设置 — 实用规则、实际案例和可直接使用的检查清单。 大多数 PHP 应用出问题,不是因为 Nginx,而是 PHP-FPM(FastCGI 进程管理器)没配好…...

RAG系统大脑调教指南:模型选择、提示设计与质量控保一本通

本文用轻松幽默的方式解密如何在RAG系统中选择、调教和监督生成模型,让它成为一个既聪明又靠谱的知识助手。从模型选择到提示工程再到质量控制,手把手教你如何避开AI的「胡言乱语」陷阱。你还记得上次问AI一个问题,它却自信满满地胡说八道的尴尬时刻吗?"嗯,根据我的分…...

智驾终局:VLA与WA的“强脑”之争

微信视频号:sph0RgSyDYV47z6快手号:4874645212抖音号:dy0so323fq2w小红书号:95619019828B站1:UID:3546863642871878B站2:UID: 3546955410049087智驾领域的“终场哨”尚未吹响,真正的终局属于那些能把“说话”和“想象”融合成“思考”的玩家。当理想i8在暴雨中的山区公路…...

微软2018年第四季度顶级漏洞赏金猎人榜单揭晓

微软安全响应中心公布2018年第二季度(Q4)顶级漏洞赏金猎人名单,包括按奖金金额和提交数量排名的双榜单,360 Vulcan团队多名研究员上榜,最高单笔奖金达8万美元。2018年第四季度前五名漏洞赏金猎人表彰 | MSRC博客 我们已完成2018年4月至6月的数据统计。第四季度前五名漏洞赏…...

能源汽车智能线控底盘

微信视频号:sph0RgSyDYV47z6快手号:4874645212抖音号:dy0so323fq2w小红书号:95619019828B站1:UID:3546863642871878B站2:UID: 3546955410049087摘 要:在“双碳”战略驱动下,新能源汽车全球渗透率预计在2025年突破30%(IEA数据),其智能化是未来发展的关键,在此背景下…...

Linux中的LED子专业的系统

Linux中的LED子专业的系统pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", monospace !important; font-size…...

DP 凸性优化:wqs 二分

重构版:wqs 二分。发现自己阅读量最高的 wqs二分 有点简略,而且有些地方是错的,所以就重构了一下,并加入了更多的例题。 前面基本上都是照搬的原来那篇文章。介绍 wqs 二分最初由王钦石在他的 2012 年国家集训队论文中提出,也叫"带权二分",或者"dp凸优化&…...

浦东再添一所一流高校,上海交通大学医学院浦东校区正式启用

微信视频号:sph0RgSyDYV47z6快手号:4874645212抖音号:dy0so323fq2w小红书号:95619019828B站1:UID:3546863642871878B站2:UID: 3546955410049087 9月12日,上海交通大学医学院浦东校区正式启用,浦东再添一所一流高校。 添加图片注释,不超过 140 字(可选)浦东校区的启用…...

nccl study

https://lgd.gd/posts/2021/03/nccl/ https://blog.csdn.net/u014443578/article/details/136902252...

AI服务器公开招标大面积失败,中国联通“招”了个寂寞?

微信视频号:sph0RgSyDYV47z6快手号:4874645212抖音号:dy0so323fq2w小红书号:95619019828B站1:UID:3546863642871878B站2:UID: 3546955410049087为了查询三大运营商人工智能服务器的招投标信息,在工信部设立的“通信工程建设项目招标投标管理信息平台”上,搜索了一下有关…...

【GitHub每日速递 250916】2053 个 n8n 工作流曝光!365 种集成 + 可视化管理,效率直接拉满

原文:【GitHub每日速递 250916】2053个n8n工作流曝光!365种集成+可视化管理,效率直接拉满 Codebuff:开源AI编码助手,多模型协作胜Claude Code,还能深度自定义! codebuff 是一个通过终端生成代码的命令行工具。简单讲,它让你在终端里直接用AI生成代码,提升开发效率。适…...

每日一家公司职场内幕——龙旗科技(上海)

微信视频号:sph0RgSyDYV47z6快手号:4874645212抖音号:dy0so323fq2w小红书号:95619019828B站1:UID:3546863642871878B站2:UID: 3546955410049087公司简述:龙旗科技(Longcheer)成立于2002年,全球总部位于上海徐汇区,杭州还有一家做量化的龙旗科技,并非一家公司。龙旗…...

0129_迭代器模式(Iterator)

迭代器模式(Iterator) 意图 提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。 UML 图优点简化访问接口:提供统一的遍历接口,简化客户端代码 封装内部结构:隐藏聚合对象的内部表示,提高安全性 支持多种遍历:可以在同一聚合上实现多种遍历方式 开…...

HJ7 取近似值

描述 对于给定的正实数 x,输出其四舍五入后的整数。更具体地说,若 x 的小数部分大于等于 0.5,则输出向上取整后的数;否则输出向下取整后的整数。 【提示】 不同编译器版本、不同系统环境对待实数的精度处理不同,我们建议您使用在线编译器进行调试。 输入描述: 输入一个小…...

读人形机器人13艺术领域

读人形机器人13艺术领域1. 艺术领域 1.1. 艺术始终是人类灵魂的深刻表达,是一面反映我们最深情感、思想和经历的镜子 1.2. 超越语言、文化和时间的界限,连接着不同世代的人 2. 机器人创作艺术和音乐 2.1. 如今,AI生成的艺术和音乐已不再是单纯的实验性产物,它们正逐渐成为创…...

活动报名:Voice First!Demo Day@Voice Agent Camp,9.22,上海丨超音速计划 2025

听腻了那些类比电影《Her》却无法真实落地的语音 AI 畅想?来 Demo Day@Voice Agent Camp,见证 「Voice First」理念下,真正创意和商业潜力兼具的初创项目。9 月 22 日下午,上海西岸数字谷,欢迎加入我们,一同重塑人机实时互动体验。demo 项目均来自「超音速计划 2025Voice…...

Windows计算器:现代C++实现的多功能计算工具

Windows计算器是一个用C++和C#编写的现代Windows应用程序,提供标准、科学和程序员计算功能,以及各种单位换算和货币转换功能,采用高精度算术运算确保计算准确性。项目标题与描述 Windows计算器是一个现代化的Windows应用程序,使用C++和C#编写,预装在Windows操作系统中。该…...

使用 PySide6/PyQt6 实现系统图标的展示与交互

在 Python 桌面应用开发中,系统图标的展示与选择是提升用户体验的重要环节。PySide6 和 PyQt6 作为 Qt 框架的 Python 绑定,提供了 QFileIconProvider 等核心类来实现这一功能。本文将以代码实例演示如何在两个框架中实现系统图标的可视化呈现与交互处理。 基础环境搭建与核心…...

如何让Java的线程池顺序执行任务 ?

一、基础概念 Java中的线程池本身并不提供内置的方式来保证任务的顺序执行的,因为线程池的设计目的是为了提高并发性能和效率,如果顺序执行的话,那就和单线程没区别了。 但是如果被问到想要实现这个功能该怎么做,有以下两种方式 1、使用单线程线程池 我们可以使用 SingleTh…...

Git 提交排除文件夹方法总结

在 Git 中排除某个文件夹(使其不被提交到远程仓库)有几种方法。以下是主要的解决方案:方法一:使用 .gitignore 文件(推荐) 这是最标准的方法,适用于大多数情况。创建或编辑 .gitignore 文件:# 如果还没有 .gitignore 文件 touch .gitignore在 .gitignore 中添加要排除的…...

如何在 Ubuntu24.04 TLS 上安装 Kubernetes 集群 - Antonie

0-先决条件 在开始安装之前,请确保您的环境满足以下先决条件:Ubuntu 24.04 LTS 系统。 至少 4GB RAM 或更多。 至少 2 个 CPU 内核。 有 40 GB 可用磁盘空间。1- 环境准备 集群规划k8s-node-1(Master):10.15.0.132 k8s-node-2(Worker):10.15.0.133 k8s-node-3(Worker)…...

Jmeter的插件开发

一、Jmeter的启动流程 在说启动流程之前我们先来看看Jmeter源码的各个重要的包:components—包含与协议无关的组件,如可视化、断言等等。 core —JMeter的核心代码,包括所有的核心接口和抽象类。 examples —演示采样器如何使用新bean框架的例子(开发插件前可以好好看看该包…...

Educational Codeforces Round 182 (Rated for Div. 2)

A. Cut the Array 题意:把数组分成三段,使得每段和模\(3\)后的值都相同或者都不相同。 \(n\)很小,暴力枚举分段就行了。点击查看代码 #include <bits/stdc++.h>using i64 = long long;void solve() {int n;std::cin >> n;std::vector<int> a(n);for (int …...

java第二周课前提问

一、代码引入 public class Main {static void changeStr(String x) {x = "xyz";}static void changeArr(String[] strs) {for (int i = 0; i < strs.length; i++) {strs[i] = strs[i]+""+i;}}public static void main(String[] args) { String x = …...

java GC

java GC...

Redis最佳实践——性能优化技巧之监控与告警详解

一、监控体系构建1. 核心监控指标矩阵指标类别 关键指标 计算方式/说明 健康阈值(参考值)内存相关 used_memory INFO Memory 获取 不超过 maxmemory 的 80%mem_fragmentation_ratio 内存碎片率 = used_memory_rss / used_memory 1.0-1.5命中率 keyspace_hits INFO Stats 获取…...

week1

任务一,编码规范: 我在网上找到了华为公司C++编码规范,我摘下几点我觉得我应该注意的 1、程序块要采用缩进风格编写, 缩进的空格数为4个 2、不允许把多个短语句写在一行中, 即一行只写一条语句 3、 if、for、do、while、case、switch、default等语句自占一行, 且if、for、do…...