Flutter圈子Flutter 之旅Android进阶之路

Flutter之旅 -- 主题切换

2025-08-13  本文已影响0人  开心wonderful

本篇文章主要介绍以下几个内容:

  • Flutter 主题系统的底层渲染原理和 Widget 树重建机制
  • 与 Android/iOS 原生主题系统的对比
  • 使用 Riverpod 实现主题切换的代码示例
Flutter之旅

本文实现效果:


Flutter 主题切换

1. Flutter主题系统底层原理

注:以下涉及到的源码部分是 Flutter 3.24.3 中的简化版本。

1.1 Widget 树与主题传播机制

想象一下,Flutter 的主题系统就像一个大家族的遗传基因一样。
在这个家族中,每个 Widget 都是家族成员,而主题数据就像是家族的"遗传基因",从祖先传递给后代,像家族遗传一样的主题传递。

// 当我们调用 Theme.of(context) 时,就像在问:"我的家族基因是什么?"
static ThemeData of(BuildContext context) {
  // Flutter会沿着家族树向上查找,找到最近的"主题祖先"
  final _InheritedTheme? inheritedTheme = 
      context.dependOnInheritedWidgetOfExactType<_InheritedTheme>();
  
  // 如果找到了主题祖先,就继承它的"基因"(主题数据)
  final ThemeData theme = inheritedTheme?.theme ?? ThemeData.fallback();
  return theme;
}

当在 Widget 中调用 Theme.of(context) 时,就像在家族群里问:"咱家现在流行什么风格?"

// 实际的"家族通讯录"代码 - Element类的核心方法
abstract class Element extends DiagnosticableTree implements BuildContext {
  Map<Type, InheritedElement>? _inheritedWidgets;  // 家族通讯录
  
  @override
  T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
    // 1. 先查通讯录:我之前联系过这个主题祖先吗?
    final InheritedElement? ancestor = _inheritedWidgets?[T];
    if (ancestor != null) {
      return dependOnInheritedElement(ancestor, aspect: aspect) as T?;  // 直接联系
    }
    
    // 2. 没联系过,就向上找最近的主题祖先
    _inheritedWidgets ??= HashMap<Type, InheritedElement>();
    final InheritedElement? ancestor = getElementForInheritedWidgetOfExactType<T>();
    
    if (ancestor != null) {
      _inheritedWidgets![T] = ancestor;  // 3. 把联系方式存到通讯录
      return dependOnInheritedElement(ancestor, aspect: aspect) as T?;  // 4. 建立订阅关系
    }
    
    return null;  // 找不到主题祖先
  }
}

举个例子

class MyButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 就像问家族群:"现在流行什么颜色?"
    final theme = Theme.of(context);
    
    return Container(
      // 按照家族风格来装扮自己
      color: theme.primaryColor,
      child: Text('按钮', style: theme.textTheme.labelLarge),
    );
  }
}

这种机制确保了:

1.2 主题数据的内存结构

ThemeData 就像一家高端定制服装店,每套服装(主题)都是完整的成品套装,包含100多件配套单品(颜色、字体、样式等)。
这家店非常聪明,采用了"成品套装"和"共享配件"的策略来节省成本和空间。

Flutter 中的 ThemeData 实际上是一个不可变对象,包含100多个 final 字段:

// 这是概念示例,用于理解ThemeData的优化策略
@immutable
class ThemeData {
  // 实际上ThemeData的所有字段都是final的
  final ColorScheme colorScheme;    // 颜色搭配
  final TextTheme textTheme;        // 字体风格
  final ButtonThemeData buttonTheme; // 按钮样式
  final AppBarTheme appBarTheme;    // 顶部栏样式
  // ... 还有100多个final字段
  
  const ThemeData.raw({
    required this.colorScheme,
    required this.textTheme,
    required this.buttonTheme,
    required this.appBarTheme,
    // ... 所有字段都在构造时确定
  });
  
  // 写时复制的核心:copyWith方法
  ThemeData copyWith({
    ColorScheme? colorScheme,
    TextTheme? textTheme,
    ButtonThemeData? buttonTheme,
  }) {
    return ThemeData.raw(
      // 只有指定的字段使用新值,其他字段复用原对象的引用
      colorScheme: colorScheme ?? this.colorScheme,
      textTheme: textTheme ?? this.textTheme,
      buttonTheme: buttonTheme ?? this.buttonTheme,
      appBarTheme: this.appBarTheme, // 复用原引用
      // ... 其他未变更的字段都复用原引用
    );
  }
}
  1. 所有字段都是final,确保不可变性;
  2. copyWith方法实现写时复制,未变更的字段共享原对象引用;
  3. 工厂构造函数处理复杂的默认值计算和依赖关系。
  1. 成品套装 🎽: 每套主题都是完整的成品,所有配件(final 字段)在制作时就固定好了;
  2. 共享配件 👔: 相同的配件(如相同的颜色、字体)在不同套装间共享,不重复制作;
  3. 定制服务 ✂️: 客户要改款式时,只重新制作需要改的配件,其他配件继续用原来的;
  4. 工厂预制 🏭: 复杂的搭配和计算在工厂(构造函数)里完成,客户拿到的是成品。
// 举个例子:
final 商务套装 = ThemeData.light();                           // 制作一套完整的商务套装
final 商务套装换领带 = 商务套装.copyWith(primaryColor: Colors.blue);  // 只换领带颜色

// 神奇的是:商务套装和商务套装换领带共享了99%的配件(西装、衬衫、鞋子等)
// 只有领带是新制作的,大大节省了材料和成本! ✨

想象一下,一个连锁品牌决定更换店面装修风格。
总部不需要让所有店铺都重新装修,只需要通知那些真正需要更新装修的店铺。
Flutter 的主题切换就是这样一个智能的"连锁管理系统"!

当主题发生变化时,Flutter 就像连锁品牌的总部发出换装通知:

// InheritedWidget 的核心方法 -- 总部检查:"这次的装修风格和上次有什么不同吗?"
class _InheritedTheme extends InheritedTheme {
  const _InheritedTheme({
    required this.theme,
    required super.child,
  });

  final ThemeData theme;

  @override
  bool updateShouldNotify(_InheritedTheme old) {
    // 如果装修风格真的变了,就发通知
    return theme != old.theme;  // 就像对比两套装修方案
  }
}

智能换装的三步走

  1. 🏢 检查变化: "装修风格真的变了吗?"

    • 如果没变化,就不发通知,各店铺继续正常营业
    • 如果变了,才开始下一步
  2. 📋 查看加盟店名单: "哪些店铺需要更新装修?"

    • Flutter 维护一个"订阅列表",记录哪些 Widget 关心主题变化
    • 就像连锁总部的加盟店名单,只通知相关的店铺
  3. 📢 精准通知: "只通知需要换装的店铺"

    • 给列表中的每个 Widget 发送"主题更新"通知
    • 收到通知的 Widget 会重新"装修"(重建)

为什么这样做这么高效?

// 举个连锁店的例子
class MyApp extends StatelessWidget {
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('我是独立小店'),                    // 🏪 不需要换装
        Container(
          color: Theme.of(context).primaryColor,  // 🏢 连锁店,需要换装
          child: Text('我是连锁店分店'),
        ),
        Image.asset('logo.png'),                // 🏪 独立小店,不受影响
      ],
    );
  }
}

当总部发出"从亮色装修改为暗色装修"的通知时:

这套连锁管理机制的优势

这就是为什么 Flutter 的主题切换能做到既快速又流畅的秘密!

2. 跨平台主题系统对比

2.1 Android 原生 vs Flutter 主题

Android 主题机制

<!-- Android themes.xml -->
<style name="AppTheme" parent="Theme.Material3.DayNight">
    <item name="colorPrimary">@color/primary</item>
    <item name="colorOnPrimary">@color/on_primary</item>
    <item name="android:windowBackground">@color/background</item>
</style>

<!-- 夜间模式 themes.xml (night) -->
<style name="AppTheme" parent="Theme.Material3.DayNight">
    <item name="colorPrimary">@color/primary_dark</item>
    <item name="colorOnPrimary">@color/on_primary_dark</item>
</style>

Android主题特点

Flutter vs Android 对比

特性 Android原生 Flutter
主题切换机制 Configuration Change + Activity 重建 Widget 树重建
性能开销 Activity完全重建,开销较大 智能局部重建,开销较小
动画支持 系统级转场动画 自定义动画,更灵活
内存管理 系统自动管理 开发者可控
跨平台一致性 平台特定 完全一致
自定义能力 受系统限制 完全自定义

2.2 iOS 原生主题

iOS 主题实现

// iOS 13+ 系统主题支持
class ThemeManager {
    static func applyTheme() {
        if #available(iOS 13.0, *) {
            // 使用系统提供的动态颜色
            view.backgroundColor = UIColor.systemBackground
            label.textColor = UIColor.label
        } else {
            // 手动管理主题
            view.backgroundColor = isDarkMode ? .black : .white
        }
    }
}

// 监听系统主题变化
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
        applyTheme()
    }
}

关键差异

3. 使用 Riverpod 实现主题切换

3.1 定义颜色常量

首先定义应用中使用的所有颜色常量,分别为亮色和暗色主题:

// lib/common/theme/color.dart
import 'package:flutter/material.dart';

// 主要颜色
const primaryColor = Color(0xFF1880FF);
const PrimaryColorDark = Color(0xFF3791FF);

// 背景颜色
const bgColor = Color(0xFFFFFFFF);
const bgColorDark = Color(0xFF191919);

// 文字颜色
const textPrimaryColor = Color(0xFF1C1D1F);
const textPrimaryColorDark = Color(0xFFFFFFFF);
const textSecondaryColor = Color(0xFF444444);
const textSecondaryColorDark = Color(0xFFA8A8A8);

3.2 创建主题配置

定义亮色和暗色两套完整的主题配置:

// lib/common/theme/theme.dart
import 'package:flutter/material.dart';
import 'color.dart';

/// 亮色主题
ThemeData lightTheme(String fontFamily) =>
    ThemeData.light(useMaterial3: true).copyWith(
      platform: TargetPlatform.iOS,
      primaryColor: primaryColor,
      textTheme: textTheme(fontFamily),
      appBarTheme: const AppBarTheme(
        backgroundColor: bgColor,
        elevation: 0,
        centerTitle: true,
        titleTextStyle: TextStyle(
          fontSize: 16,
          height: 1.5,
          color: textPrimaryColor,
          fontWeight: FontWeight.w500,
        ),
      ),
      dialogTheme: DialogThemeData(
        backgroundColor: bgColor,
        titleTextStyle: textTheme(fontFamily).titleMedium,
        contentTextStyle: textTheme(fontFamily)
            .bodyMedium
            ?.copyWith(color: textSecondaryColor),
      ),
      // 更多亮色主题配置...
    );

/// 暗色主题
ThemeData darkTheme(String fontFamily) =>
    ThemeData.dark(useMaterial3: true).copyWith(
      platform: TargetPlatform.iOS,
      primaryColor: PrimaryColorDark,
      textTheme: textDarkTheme(fontFamily),
      appBarTheme: const AppBarTheme(
        backgroundColor: bgColorDark,
        elevation: 0,
        centerTitle: true,
        titleTextStyle: TextStyle(
          fontSize: 16,
          height: 1.5,
          color: textPrimaryColorDark,
          fontWeight: FontWeight.w500,
        ),
      ),
      dialogTheme: DialogThemeData(
        backgroundColor: bgColorDark,
        titleTextStyle: textDarkTheme(fontFamily).titleMedium,
        contentTextStyle: textDarkTheme(fontFamily)
            .bodyMedium
            ?.copyWith(color: textSecondaryColorDark),
      ),
      // 更多暗色主题配置...
    );

3.3 实现持久化存储

如使用 SharedPreferences 保存用户的主题选择:

// lib/common/utils/share_preferences_utils.dart
class SharePreferenceUtils {
  /// 保存主题模式
  static Future<bool> saveThemeMode(String themeMode) async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    return await prefs.setString(SharePreferenceKey.themeMode, themeMode);
  }

  /// 获取主题模式
  static Future<String> getThemeMode() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    return prefs.getString(SharePreferenceKey.themeMode) ?? 
           ThemeModeConstant.system;
  }
}

class SharePreferenceKey {
  static const String themeMode = "THEME_MODE";  // 主题模式
}

// 主题模式常量
class ThemeModeConstant {
  static const String system = "system";
  static const String light = "light";
  static const String dark = "dark";
}

3.4 创建主题状态管理器

使用 Riverpod 的 StateNotifier 创建主题状态管理器:

// lib/common/theme/theme_provider.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../constant.dart';
import '../utils/share_preferences_utils.dart';

/// 主题模式
class ThemeModeNotifier extends StateNotifier<ThemeMode> {
  ThemeModeNotifier() : super(ThemeMode.system) {
    _initThemeMode();
  }

  /// 初始化主题模式
  Future<void> _initThemeMode() async {
    final savedThemeMode = await SharePreferenceUtils.getThemeMode();
    switch (savedThemeMode) {
      case ThemeModeConstant.light:
        state = ThemeMode.light;
        break;
      case ThemeModeConstant.dark:
        state = ThemeMode.dark;
        break;
      default:
        state = ThemeMode.system;
    }
  }

  /// 设置主题模式
  Future<void> setThemeMode(ThemeMode mode) async {
    state = mode;
    String modeString;
    switch (mode) {
      case ThemeMode.dark:
        modeString = ThemeModeConstant.dark;
        break;
      case ThemeMode.light:
        modeString = ThemeModeConstant.light;
        break;
      default:
        modeString = ThemeModeConstant.system;
    }
    await SharePreferenceUtils.saveThemeMode(modeString);
  }

  /// 切换主题模式
  Future<void> toggleThemeMode(bool isDark) async {
    if (isDark) {
      await setThemeMode(ThemeMode.dark);
    } else {
      await setThemeMode(ThemeMode.light);
    }
  }

  /// 智能切换主题模式(无需传参)
  /// 如果当前是暗黑模式 or 跟随系统且系统是暗黑模式,切换为亮色模式
  /// 如果当前是亮色模式 or 跟随系统且系统是亮色模式,切换为暗黑模式
  Future<void> toggleTheme() async {
    // 获取系统当前主题模式
    final brightness =
        WidgetsBinding.instance.platformDispatcher.platformBrightness;
    final isSystemDark = brightness == Brightness.dark;

    // 根据当前模式智能切换
    if (state == ThemeMode.dark) {
      await setThemeMode(ThemeMode.light);
    } else if (state == ThemeMode.system && isSystemDark) {
      await setThemeMode(ThemeMode.light);
    } else {
      await setThemeMode(ThemeMode.dark);
    }
  }
}

/// 主题模式状态提供者
final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>(
  (ref) => ThemeModeNotifier(),
);

/// 系统主题是否为暗黑模式的提供者
final isSystemDarkModeProvider = Provider<bool>((ref) {
  final brightness =
      WidgetsBinding.instance.platformDispatcher.platformBrightness;
  return brightness == Brightness.dark;
});

/// 主题是否为暗黑模式的提供者
final isDarkModeProvider = Provider<bool>((ref) {
  final themeMode = ref.watch(themeModeProvider);
  final isDarkSystem = ref.watch(isSystemDarkModeProvider);

  if (themeMode == ThemeMode.dark) {
    return true; // 如果是暗黑模式,开关打开
  } else if (themeMode == ThemeMode.light) {
    return false; // 如果是亮色模式,开关关闭
  } else {
    return isDarkSystem; // 如果是跟随系统,则根据系统当前主题决定
  }
});

3.5 在 MaterialApp 中应用主题

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'common/theme/theme.dart';
import 'common/theme/theme_provider.dart';

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final themeMode = ref.watch(themeModeProvider);
    
    return MaterialApp(
      title: 'Flutter主题切换示例',
      theme: lightTheme(),           // 亮色主题
      darkTheme: darkTheme(),        // 暗色主题
      themeMode: themeMode,          // 当前主题模式
      home: MyHomePage(),
    );
  }
}

3.6 在UI中使用主题

有了主题系统,让界面根据主题变化就像变脸一样简单:

// 方式一:直接使用主题数据(推荐)
class MyCard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);  // 获取当前主题
    
    return Card(
      color: theme.colorScheme.surface,        // 自动适配的背景色
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          children: [
            Text(
              '标题',
              style: theme.textTheme.headlineSmall,  // 自动适配的标题样式
            ),
            Text(
              '内容描述',
              style: theme.textTheme.bodyMedium,     // 自动适配的正文样式
            ),
            ElevatedButton(
              onPressed: () {},
              child: Text('按钮'),  // 按钮会自动使用主题色
            ),
          ],
        ),
      ),
    );
  }
}

// 方式二:手动判断主题模式(适用于特殊情况)
class CustomWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final isDarkMode = ref.watch(isDarkModeProvider);
    
    return Container(
      // 根据主题模式选择不同的背景色
      color: isDarkMode ? bgPrimaryColorDark : bgColor,
      child: Column(
        children: [
          Icon(
            Icons.star,
            // 暗色模式用金色,亮色模式用蓝色
            color: isDarkMode ? Colors.amber : Colors.blue,
          ),
          Text(
            '自定义样式文字',
            style: TextStyle(
              color: isDarkMode ? textPrimaryColorDark : textPrimaryColor,
              fontSize: 16,
            ),
          ),
        ],
      ),
    );
  }
}

// 主题切换开关
class ThemeSwitchWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final isDarkMode = ref.watch(isDarkModeProvider);
    
    return Switch(
      value: isDarkMode,
      onChanged: (value) {
        // 切换主题
        ref.read(themeModeProvider.notifier).toggleTheme();
      },
    );
  }
}

使用建议

4. 小结

4.1 核心原理

  1. InheritedWidget 机制: Flutter 主题系统基于 InheritedWidget 实现,像家族遗传基因一样,通过依赖追踪和智能通知机制,确保只有真正依赖主题数据的 Widget 才会重建。

  2. 不可变对象设计: ThemeData 是不可变对象,所有字段都是 final。采用写时复制策略,像 "高端定制服装店" 一样,只有变更的配件才重新制作,其他配件共享引用,最大化内存使用效率。

  3. 智能重建机制: 主题切换时采用 "连锁店管理" 模式,只重建真正依赖主题数据的 Widget,实现高效的界面更新。

  4. 跨平台优势: 相比 Android 和 iOS 原生主题系统,Flutter 提供了更灵活的自定义能力和更好的跨平台一致性。

4.2 实现要点

  1. 状态管理: 使用 Riverpod 的 StateNotifier 管理主题状态,提供类型安全和自动资源管理。

  2. 持久化存储: 通过 SharedPreferences 保存用户主题偏好,确保应用重启后主题设置不丢失。

  3. 智能切换: 实现了支持系统主题跟随的智能切换逻辑,提升用户体验。

  4. 注意事项: 主题数据的不可变性

    • ThemeData是不可变对象,修改主题时必须使用copyWith方法;
    • 避免直接修改 ThemeData 的属性,这会导致不可预期的行为。
上一篇 下一篇

猜你喜欢

热点阅读