打阗渖 发表于 2025-10-1 17:38:16

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中使用,因为需要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;
      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!;
    }
   
    final theme = BeeTheme.createTheme(
      primaryColor: primaryColor,
      brightness: brightness,
      fontFamily: fontFamily,
    );
   
    _cache = 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>
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: Flutter个性化主题系统:Material Design 3的深度定制