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中使用,因为需要BuildContext
- throw UnimplementedError('Use BeeTheme.colorsOf(context) instead');
- });
复制代码 主题选择器实现
颜色选择器组件
- 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,
- ),
- );
- }
- }
复制代码 主题模式切换器
- 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中应用主题
- 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('正在加载主题...'),
- ],
- ),
- ),
- ),
- );
- }
- }
复制代码 在组件中使用语义颜色
- 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;
- }
- }
- }
复制代码 响应式设计适配
- 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);
- @override
- State createState() => _AnimatedColorTransitionState();
- }
- class _AnimatedColorTransitionState extends State
- 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();
- }
- }
复制代码 主题切换动画
- 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,
- ),
- );
- }
- }
复制代码 主题测试与调试
主题预览工具
- 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,
- ),
- ),
- );
- }
- }
复制代码 性能优化与最佳实践
主题缓存策略
- 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,
- });
- @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项目
项目特色
<ul>
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |