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最大的亮点是动态颜色系统,它能:

  • 自适应配色:根据主色自动生成完整配色方案
  • 语义化颜色:每个颜色都有明确的语义和用途
  • 无障碍支持:自动保证颜色对比度符合无障碍标准
  • 深浅模式:完美支持明暗主题切换

全新的设计语言

  • 更大的圆角:更加柔和友好的视觉效果
  • 增强的层级:通过颜色和阴影表达信息层级
  • 动态形状:组件形状可以跟随主题动态调整

主题架构设计

核心主题类

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

  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是一个完全开源的项目,欢迎开发者参与贡献:

参考资源

官方文档

学习资源


本文是BeeCount技术文章系列的第4篇,后续将深入探讨数据可视化、CSV导入导出等话题。如果你觉得这篇文章有帮助,欢迎关注项目并给个Star!