基于 Flutter 从零开发一款产品(五)—— 状态管理

158 阅读3分钟

前言

本系列是实战课程文章,通过一个完整的 Flutter 项目,上手 Flutter 开发,其中会涉及到路由、网络、状态管理等开发中需要用到的基础知识,通过这些系列的文章讲解,可以快速上手一个 Flutter 项目。这篇文章,我们来谈谈状态管理。说说什么是状态管理,为什么需要状态管理,这常常是困扰新手上路的一个问题。今天我们就来用实际的例子来聊聊如何使用状态管理,在实践当中进行学习。

什么是状态管理

状态是指用户界面(UI)显示的数据,它可能随着用户操作或应用逻辑的变化而发生改变,而状态管理就是如何追踪、更新这些数据,并让 UI 保持同步。例如:一个购物车应用。用户可以添加商品到购物车,删除商品,并查看总价。这里的状态包括:商品列表、购物车中的商品数量、总价这些在用户操作的时候界面都会发生相应的变化,如果没有一个清晰的状态管理方式,数据更新可能会变得难以维护,UI 和数据之间容易失去同步。

Flutter 提供了多种方式来管理状态,分为两大类:

  • 本地状态管理(简单应用):适用于单个组件或页面内的状态。
  • 全局状态管理(复杂应用):适用于需要在多个组件之间共享状态的情况。

我们现在以 BiliVideoDown 项目当中的暗黑模式切换谈谈当中所运用到的状态管理。

GIF 2025-1-21 15-08-33.gif

在这里,切换暗黑模式的按钮和其他界面在不同的组件当中,其他界面的组件是如何知道切换到了暗黑模式,根据暗黑模式的状态改变自身的主题颜色呢?这就是一个典型的状态管理的场景,状态统一进行管理,可以理解统一放在内存当中的某个地方,各个组件在需要的时候读取这个状态即可。下面就来看看如何使用 Riverpod 进行状态的管理。关于 Riverpod,这里就不多做介绍了,Riverpod 是 Flutter 生态中一个现代化的状态管理库,相比于 Provider,它更加灵活、强大,并提供了一些新的特性,让开发状态管理更加简洁高效。

通过主题色切换理解状态管理

理解 Riverpod 当中的核心概念:

  • 状态提供者:用于存储和管理状态
  • 状态消费者:用于从提供者中读取或更新状态。

引入 Riverpod

首先,在 pubspec.yaml 中添加 Riverpod 依赖:

dart pub add riverpod

启用 Riverpod

在应用的根组件中使用 ProviderScope 包裹应用,启用 Riverpod 状态管理功能:

void main() async {
  await init();
  runApp(const ProviderScope(child: MyApp()));
}

定义 ThemeService 来管理 ThemeState,并通过 StateNotifierProvider 暴露状态:


@MappableClass()
class ThemeState with ThemeStateMappable {
  final bool isDark;
  final ThemeMode themeMode;
  const ThemeState({
    required this.isDark,
    required this.themeMode,
  });
}


final themeProvider = StateNotifierProvider<ThemeService, ThemeState>((ref) {
  final themeService = ThemeService();
  themeService.init();
  return themeService;
});

class ThemeService extends StateNotifier<ThemeState> {
  ThemeService()
      : super(const ThemeState(isDark: false, themeMode: ThemeMode.light));

  // Initialize ThemeState
  void init() {
    bool isDark = SpUtil.getBool(SpKey.isDarkTheme) ?? false;
    state = state.copyWith(isDark: isDark, themeMode: getThemeMode(isDark));
  }

  ThemeMode getThemeMode(bool isDark) {
    return isDark ? ThemeMode.dark : ThemeMode.light;
  }

  void switchThemeMode() async {
    bool isDark = !state.isDark;
    state = state.copyWith(isDark: isDark, themeMode: getThemeMode(isDark));
    await SpUtil.putBool(SpKey.isDarkTheme, isDark);
  }
}

创建一个主题切换按钮,我们通过 ConsumerWidgetWidgetRef 在界面组件中访问和使用状态:

class ThemeSwitchIcon extends ConsumerWidget {
  const ThemeSwitchIcon({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final appThemeProvider = ref.watch(themeProvider);
    return MouseRegion(
      cursor: SystemMouseCursors.click,
      child: GestureDetector(
        onTap: () {
          ref.read(themeProvider.notifier).switchThemeMode();
        },
        child: Padding(
          padding: const EdgeInsets.only(bottom: 16, top: 16),
          child: Icon(
            appThemeProvider.isDark
                ? Icons.light_mode_outlined
                : Icons.dark_mode_outlined,
            color: appThemeProvider.isDark ? Colors.white : Colors.black,
          ),
        ),
      ),
    );
  }
}

根组件监听状态,让主题模式动态影响应用的全局主题:

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final appThemeProvider = ref.watch(themeProvider);
    return MaterialApp.router(
      debugShowCheckedModeBanner: false,
      routerConfig: routerConfig,
      theme: ThemeData(
        primarySwatch: Colors.blue,
        brightness: Brightness.light,
      ),
      darkTheme: ThemeData(
        primarySwatch: Colors.blue,
        brightness: Brightness.dark,
      ),
      themeMode: appThemeProvider.themeMode,
    );
  }
}

总结:

  • 状态分离:通过 ThemeState 定义状态模型,数据与逻辑分离。
  • 全局共享:利用 StateNotifierProvider,全局共享主题状态。
  • 界面与状态解耦:UI 通过 Riverpod 监听状态变化,自动响应更新。