【Flutter 工程】001-Flutter 状态管理:Riverpod
文章目录
一、概述
1、官方状态管理
状态管理处理应用程序数据流动和 UI 更新的关键概念。在 Flutter 应用程序中,状态管理确保应用程序 UI 和数据保持同步,共享和同步数据,并提供良好的代码结构和可维护性。
Flutter 提供了 StatefulWidget
作为最基本的状态管理方法。有状态组件可以存储和更新自身状态,适用于简单的场景和局部状态。
然而,StatefulWidget
存在以下问题:
- 状态管理复杂性:当组件树庞大且状态需要在多个组件之间共享时,状态管理变得复杂,代码难以理解和维护。
- 性能问题:相比
StatelessWidget
,StatefulWidget
在状态变化时会导致更多组件重建,可能影响应用程序性能,尽管Flutter已经进行了性能优化。 - 生命周期管理复杂性:
StatefulWidget
具有复杂的生命周期,需要处理多个生命周期方法(如initState
、didUpdateWidget
和dispose),导致代码复杂和难以管理。 - 难以测试:由于
StatefulWidget
具有内部状态,编写单元测试和集成测试变得更加困难,可能影响应用程序的质量和可靠性。 - 重用性差:
StatefulWidget
的状态通常与特定实例紧密耦合,降低了组件的可重用性。
2、状态管理解决方案
在 Flutter 中,还有其他的状态管理方法可供选择,以下是一些常见的状态管理方法。
- InheritedWidget 和 InheritedModel:这些是 Flutter 提供的允许状态在组件树中向下传递的特殊类型的组件。它们可以帮助你在应用程序的不同层级之间共享状态。这种方法
对于较小的应用程序或有限的状态共享需求较为合适
。 - Provider: 一个依赖注入和状态管理第三方库,它是在 InheritedWidget 基础上做了封装,有上面组件的能力,但是更简单易用。Provider 可以监听状态变化,并在需要时重新构建关联的组件。这种方法适用于
各种规模的应用程序
,具有良好的可扩展性和灵活性。 - Riverpod: 一个相对较新的状态管理库,类似于 Provider,但提供了更多的功能和改进。Riverpod 允许你创建不可变的、可组合的和可测试的状态管理解决方案。这种方法适用于
需要更高度可控和可测试性的应用程序
。 - BLoC(Business Logic Component) :一种基于响应式编程的状态管理方法。BLoC 将业务逻辑与 UI 分离,使你可以轻松地测试和重用代码。BLoC 通常与 RxDart(一种 Dart 的响应式编程库)一起使用,以提供强大的数据流处理能力。这种方法适用于
需要处理复杂业务逻辑和大量数据流的应用程序
。 - Redux: 一种集中式状态管理库,它将应用程序的状态存储在一个单一的状态树中。Redux 使用纯函数(称为reducers)来处理状态更新,使你可以轻松地跟踪和管理应用程序的状态变化。这种方法适用于
需要严格的状态管理和可预测性的应用程序
。
具体选择什么样的状态管理方法,这取决于你应用程序的需求、复杂性和个人喜好。不同的方法有不同的优缺点,因此在选择状态管理方法时,请务必充分了解每种方法的特点,并权衡其适用性。
3、为什么选择 Riverpod
究其原因,还是 Riverpod 的一些主要特点比较给力,与我们的需求契合,且听我慢慢道来……
- 不可变性。Riverpod 中的状态是不可变的,这意味着状态在更新时会创建一个新的对象,而不是修改现有对象。这有助于减少错误,并使状态更易于理解和跟踪。
- 类型安全。Riverpod 在编译时提供了更强的类型安全性,有助于减少类型错误并提高代码质量。
- 无需 BuildContext。 与 Provider 不同,Riverpod 不依赖于 BuildContext 来访问状态。这使得在组件之外的位置(如函数或类)访问状态变得更加容易,同时提高了可测试性。
- 可组合。Riverpod 允许你组合不同的 Provider 以创建更复杂的状态管理解决方案。这有助于保持代码的模块化和可维护性。
- 易于测试。由于 Riverpod 的状态不依赖于 BuildContext,你可以更轻松地编写单元测试。此外,Riverpod 提供了用于模拟状态和测试的实用工具。
- 家族功能。Riverpod 具有所谓的“家族”功能,允许你根据参数创建多个相同类型的 Provider 实例。这使得在使用相同逻辑但参数不同的多个组件时,可以更好地管理状态。
- 非常灵活。Riverpod 具有很高的灵活性,可以很好地适应不同的应用程序结构和需求。你可以使用 Riverpod 来构建简单的局部状态管理,或者构建复杂的全局状态管理解决方案。
总之,Riverpod 是一个强大的状态管理库,适用于各种规模的 Flutter 应用程序。它提供了不可变性、类型安全性、无需 BuildContext 的访问、可组合性、易于测试和家族功能等多种优点。如果你正在寻找一个现代、灵活且易于使用的状态管理解决方案,Riverpod 是一个值得考虑的选择。
Riverpod 官方文档
https://docs-v2.riverpod.dev/zh-hans/
Riverpod 提供的几种 Provider
二、官方示例
1、安装
flutter pub add flutter_riverpod dev:custom_lint dev:riverpod_lint riverpod_annotation dev:build_runner dev:riverpod_generator
2、官方示例
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'main.g.dart';
// 我们创建一个 “provider”,它将用于保存一个值(这里是 “Hello world”)。
// 通过使用一个 provider,我们能够模拟或覆盖被暴露的值。
String helloWorld(HelloWorldRef ref) {
return 'Hello world';
}
void main() {
runApp(
// 为了能让组件读取 provider,我们需要将整个
// 应用都包裹在 “ProviderScope” 组件内。
// 这里也就是存储我们所有 provider 状态的地方。
ProviderScope(
child: MyApp(),
),
);
}
// 扩展来自 Riverpod 的 HookConsumerWidget 而不是 HookWidget
class MyApp extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final String value = ref.watch(helloWorldProvider);
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Example')),
body: Center(
// 读取 provider 的值
// 此处为了方便查看,设置了大字体
child: Text(value, style: const TextStyle(fontSize: 40),),
),
),
);
}
}
3、代码生成
# --delete-conflicting-outputs 可选,会在生成代码冲突的时候,删除原来的代码,重新生成
flutter pub run build_runner build --delete-conflicting-outputs
4、官方示例运行结果
三、基本使用
1、改造 main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:study/pages/HomePage.dart';
void main() {
runApp(
// 为了能让组件读取 provider,我们需要将整个
// 应用都包裹在 “ProviderScope” 组件内。
// 这里也就是存储我们所有 provider 状态的地方。
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
home: HomePage(),
);
}
}
2、创建 home_page.dart
/lib/pages/home_page.dart
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../states/hello_state.dart';
class HomePage extends HookConsumerWidget {
const HomePage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final hello = ref.watch(helloStateProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: Center(
child: SizedBox(
height: 400,
child: Column(
children: [
// 文本
Text(hello.hello, style: const TextStyle(fontSize: 40),),
// 更新文本
ElevatedButton(
style: ButtonStyle(minimumSize: MaterialStateProperty.all(const Size(200, 50))),
onPressed: () {
ref.read(helloStateProvider.notifier).setHello("文本更新了!");
},
child: const Text('Update'),
),
],
),
)),
);
}
}
3、创建 hello_state.dart
lib/state/hello_state.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
class HelloState {
final String hello;
HelloState({
this.hello = 'Hello World',
});
}
class HelloStateProvider extends StateNotifier<HelloState> {
HelloStateProvider() : super(HelloState());
void setHello(String hello) {
state = HelloState(
hello: hello,
);
}
}
final helloStateProvider = StateNotifierProvider<HelloStateProvider, HelloState>(
(ref) => HelloStateProvider(),
);
4、运行结果
四、使用代码生成
代码生成是指使用工具为我们生成代码。
在Dart中,它的缺点是需要额外的步骤来“编译”应用。 尽管这个问题可能会在不久的将来得到解决, 但Dart团队正在研究并解决这个问题的潜在方案。
使用Riverpod时,代码生成是完全可选的。 当然你也完全可以不使用。
与此同时,Riverpod支持代码生成,且推荐你使用它。
1、改造 hello_state.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'hello_state.g.dart';
class HelloList extends _$HelloList {
List<String> build() {
return ["hello world!"];
}
void addHello(String hello) {
state = [...state, hello];
}
}
2、代码生成
# --delete-conflicting-outputs 可选,会在生成代码冲突的时候,删除原来的代码,重新生成
flutter pub run build_runner build --delete-conflicting-outputs
3、改造 home_page.dart
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../states/hello_state.dart';
class HomePage extends HookConsumerWidget {
const HomePage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final List<String> hellos = ref.watch(helloListProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: Center(
child: SizedBox(
height: 400,
child: Column(
children: [
// 文本:遍历 hellos 列表
...hellos.map((e) => Text(e, style: const TextStyle(fontSize: 40),)),
// 更新文本
ElevatedButton(
style: ButtonStyle(minimumSize: MaterialStateProperty.all(const Size(200, 50))),
onPressed: () {
// 获取当前时间
DateTime now = DateTime.now();
ref.read(helloListProvider.notifier).addHello("hello ${now.second}");
},
child: const Text('Update'),
),
],
),
)),
);
}
}
4、运行结果
5、为什么在Riverpod中使用代码生成
你可能在想:“如果在Riverpod中代码生成是可选的,为什么要使用?”
让你的代码生活更简单。
这包括但不限于:
- 更好的语法, 更可读且更灵活,而且还能减少学习曲线。
- 不需要担心
FutureProvider
、Provider
还是其他 provider。仅需写下你的逻辑, Riverpod将为你选择最合适的provider。 - 向provider传递参数现在不受限制。不再局限于使用 family 和传递单个参数, 现在可以传递任何形式的参数。这包括命名参数、可选参数甚至默认值。
- 不需要担心
- 在Riverpod中编写的代码支持 有状态热重载。
- 更好地调试,通过生成额外的元数据然后用调试器调试。
- Riverpod的一些功能将只支持代码生成。
与此同时,许多应用程序中已经使用了代码生成比如 Freezed 或 json_serializable。 在这种情况下,你的项目可能已经为代码生成配置好了,使用Riverpod应该很简单。
五、实践代码示例
1、状态
part 'film_state.g.dart';
part 'film_state.freezed.dart';
class FilmState with _$FilmState {
factory FilmState({
// EasyRefresh 控制器
EasyRefreshController? controller,
// 当前页码
int? pageNum,
// 每页数量
int? pageSize,
// 电影列表
List<FilmModel>? list,
// 没有更多数据了
bool? noMore,
}) = _FilmState;
}
class FilmStateNotifier extends _$FilmStateNotifier {
FutureOr<FilmState> build() async {
return FilmState(
controller: EasyRefreshController(),
pageNum: 0,
pageSize: 10,
list: [],
noMore: false,
);
}
// 加载电影列表
FutureOr<void> loadFilmList() async {
// 分页查询参数
final dto = FilmPageRequestModel();
dto.pageSize = state.value!.pageSize;
dto.pageNum = state.value!.pageNum;
// 加载电影列表
BasePageModel basePageModel = await FilmApi.page(dto);
basePageModel.content = basePageModel.decode(FilmModel(), basePageModel.content!);
if (basePageModel.content!.isNotEmpty) {
state.value!.list!.addAll(basePageModel.content!.cast<FilmModel>());
state.value!.noMore = false;
} else {
state.value!.noMore = true;
}
}
// 下拉刷新
FutureOr<void> refresh() async {
state.value!.list!.clear();
state.value!.pageNum = 0;
await loadFilmList();
state.value!.controller!.finishRefresh(IndicatorResult.success, true);
}
// 上拉加载
FutureOr<dynamic> loadMore() async {
state.value!.pageNum = state.value!.pageNum! + 1;
await loadFilmList();
if (!state.value!.noMore!) {
state.value!.controller!.finishLoad(IndicatorResult.success, true);
} else {
state.value!.controller!.finishLoad(IndicatorResult.noMore, true);
}
}
}
2、使用示例
class FilmPage extends ConsumerWidget {
const FilmPage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final stateProvider = ref.watch(filmStateNotifierProvider);
final stateNotifier = ref.watch(filmStateNotifierProvider.notifier);
return stateProvider.when(
data: (state) {
return SimpleEasyRefresher(
easyRefreshController: state.controller,
onLoad: stateNotifier.loadMore,
onRefresh: stateNotifier.refresh,
childBuilder: (context, physics) {
return ListView.builder(
physics: physics,
itemCount: state.list!.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => FilmDetailsPage(filmModel: state.list![index]))),
child: Container(color: GlobalColor.bgc, child: $BuildFilmItem(state.list![index])),
);
},
);
},
);
},
loading: () => const CircularProgressIndicator(),
error: (error, stackTrace) => Text('Error: $error'),
);
}
}
六、ref.read
与 ref.watch
1、ref.watch
ref.watch
方法用于订阅状态,并在状态发生变化时重新构建小部件。它返回一个可监听的状态。- 当使用
ref.watch
订阅状态时,如果状态发生变化,相关的小部件会被重新构建,以更新界面展示。 ref.watch
方法在小部件的build
方法中使用,确保当状态变化时,与状态相关的部分会被更新。
2、ref.read
:
ref.read
方法用于读取状态,但不会订阅状态变化。- 当使用
ref.read
读取状态时,它会立即返回当前的状态值,但不会自动更新界面。 ref.read
方法在小部件的build
方法之外使用,例如在回调函数、事件处理程序或其他地方需要读取状态的情况下使用。
3、总结
- 使用
ref.watch
会订阅状态的变化并自动更新界面,适用于需要及时响应状态变化的情况。 - 使用
ref.read
会立即返回当前的状态值,适用于不需要及时更新界面的情况。
七、XXXNotifierProvider
和XXXNotifierProvider.notifier
的区别
1、XXXNotifierProvider
XXXNotifierProvider
是一个Provider
对象,它负责提供状态值。- 当使用
ref.watch(XXXNotifierProvider)
时,它会订阅状态的变化并返回状态值。
2、XXXNotifierProvider.notifier
notifier
是XXXNotifierProvider
提供的一个特殊属性,它指向状态通知器(notifier)对象。- 状态通知器是一个用于管理和触发状态变更的对象,它实现了状态管理逻辑和业务逻辑。
- 当使用
ref.watch(XXXNotifierProvider.notifier)
时,它会订阅状态通知器的变化并返回状态通知器对象。
3、总结
XXXNotifierProvider
返回的是Provider对象,用于访问状态值。XXXNotifierProvider.notifier
返回的是状态通知器对象,用于管理状态的变更和执行业务逻辑。