前言:
之前写过
Flutter 基于getX搭建通用项目架构,
Flutter 基于 Bloc搭建通用项目架构,
Flutter Provider+MVVM搭建通用项目架构
虽然在实际的项目开发中一直使用的是Bloc,但是对于 riverpod 还是很感兴趣,终于有点时间用 riverpod 写了个小Demo,和大家分享下riverpod的魅力。
一. riverPod 简介
riverpod 和 provider 是出自一人之手,在名称上 riverpod 也是 provider的重新排列,目前的最新版本是 2.4.9已经趋于稳定。provider 本质上是基于可监听对象 Listenable 实现的监听通知,对异步的处理并不理想,并不像Bloc那样可以处理复杂的异步场景。所以,为了弥补provider的缺陷,riverpod应运而生。另外riverpod 无需要全局 BuildContext,可在任何地方访问状态(包括非 Widget 类), 突破 InheritedWidget 的上下文依赖限制,使用起来更加的方便。
二. riverPod 使用
首先来说下环境 我的FlutterSDK版本 是 flutter_3.32.0
添加依赖库
dependencies:
...
flutter_riverpod: ^2.6.1
riverpod_annotation: ^2.6.1
dev_dependencies:
...
build_runner: ^2.4.8
riverpod_generator: ^2.3.9
1. 小试牛刀 - 通过计数器简单了解 riverpod
老规矩,通过计数器来介绍一下 flutter_riverpod 最简单的使用。首先 Counter 负责维护状态数据(相当于控制器),它继承自 AutoDisposeNotifier ,这样它会持有泛型类型的状态对象 state(可以是任意对象或基本数据类型) 。在 点击increment 方法中让数字增加; build方法 中提供初始状态值。可以看出这种方式和Bloc框架的 Cubit 是非常类似的。
代码如下:
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'counter_provider.g.dart';
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state++;
}
在riverpod中,如果想要为外面的页面提供数据,都必须声明为全局变量,当然这个声明的代码,可以自己来写,但是riverpod也提供了代码生成器 riverpod_generator ,可以自动生成模版代码,提供了很大的方便。
riverpod_generator 是基于 build_runner 工作的,代码生成是编码过程中的操作,不需要在运行时依赖,非常安全。所以在 dev_dependencies 节点下添加依赖。上面已经提了依赖代码。
下面就可以执行 art run build_runner build 命令来生成文件了。
| ! |
|---|
在视图逻辑处理中,首先要在最顶层嵌套 ProviderScope 组件:
然后通过 flutter_riverpod 中提供的 Consumer 组件,使用 builder 回调构建组件。其中的第二入参是 WidgetRef ,可以通过该对象来 获取并观察 某个 XXXNotifierProvider 全局变量 。
点击计数器的回调方法,可以通过
ref.read 仅访问 counterProvider ,得到 Counter 对象,触发 increment 方法。
除了 Consumer 组件之外,flutter_riverpod 还提供了 ConsumerWidget 组件,通过继承的关系,子类在 build 回调中得到 WidgetRef 对象,在构建逻辑中使用。如果这里让 一个页面 继承自 ConsumerWidget,那更新通知将会触发 build 方法,页面整体都会重新构建 。
所以如果希望控制更新的小粒度,就需要拆分组件,下面会写几个例子来演示。
2. 单刀直入 - 通过写一个列表来深入了解 riverpod
- 定义
state
import 'package:riverpod_t/page/home/data/model/cartoon_model_entity.dart';
class MineState {
List<CartoonModelDataFeeds> list = [];
final int? page;
final bool? hasNext;
MineState({required this.list, this.hasNext, this.page});
MineState copyWith({List<CartoonModelDataFeeds>? list, int? page, bool? hasNext}) =>
MineState(list: list ?? this.list, page: page, hasNext: hasNext);
}
里面的三个属性不用多说了,都知道干啥用的,定义出一个copyWith供调用。
- 定义
river
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_t/config/response_model.dart';
import 'package:riverpod_t/page/home/data/model/cartoon_model_entity.dart';
import 'package:riverpod_t/page/home/data/repository/home_repository.dart';
import 'package:riverpod_t/page/mine/state/mine_state.dart';
import 'package:riverpod_t/until/app_util.dart';
part 'mine_list_river.g.dart';
@riverpod
class MineListRiver extends _$MineListRiver {
@override
Future<MineState> build() async {
return MineState(list: await _fetchAllList(1));
}
/// 刷新方法
void refresh({bool isRefresh = false}) async {
try {
state = AsyncLoading();
int page = state.value?.page ?? 1;
List<CartoonModelDataFeeds> list = state.value?.list ?? [];
bool hasNext = true;
if (isRefresh) {
page = 1;
} else {
page++;
}
List<CartoonModelDataFeeds>? moreList = await _fetchAllList(page);
if (AppUtil.isNotEmpty(moreList)) {
if (isRefresh) {
list.clear();
}
list.addAll(moreList);
if (moreList.length >= 20) {
hasNext = true;
} else {
hasNext = false;
}
} else {
hasNext = false;
}
state = AsyncValue.data(MineState(list: list, page: page, hasNext: hasNext));
} catch (err, stack) {
state = AsyncValue.error(err, stack);
}
}
Future<List<CartoonModelDataFeeds>> _fetchAllList(int page) async {
ResponseModel model = await HomeRepository.getListData<CartoonModelData>(page);
CartoonModelData carDataModel = model.data;
return carDataModel.feeds ?? [];
}
}
因为数据是从网络上获取到的,所以build方法需要返回一个Future并且设置好了泛型MineState。
@override
Future<MineState> build() async {
return MineState(list: await _fetchAllList(1));
}
MineState这个对象,在UI层用来提供数据,也是将控制器和UI进行分离,和Bloc的思想是一样的。只不过在Bloc中全局的state就是你自己定义的state,在riverpod中,如果Provider的类型是AutoDisposeAsyncNotifierProvider,那么全局的state就又被AsyncValue封装了一层,在UI层使用的话,riverpod做了封装可以拿到这个对象,如下第七行代码,如要在river (控制器)中使用的话,需要.value才是自己定义的对象。
AsyncValue这个对象是riverpod封装好的异步状态对象,ref .watch(xxxListRiverProvider).when()观察的就是AsyncValue的状态,在UI层展示不同的页面。
| 状态 | 页面 |
|---|---|
| AsyncLoading | 接口请求中,展示loading |
| AsyncValue.data | 数据请求成功,展示页面 |
| AsyncValue.error | 数据请求失败,展示错误页面 |
int page = state.value?.page ?? 1;
ref
.watch(xxxListRiverProvider)
.when(
skipLoadingOnRefresh: true,
skipLoadingOnReload: true,
skipError: false,
data: (state) {
return _listBody(context, state, ref);
},
error:
(error, stack) =>
ErrorRiverWidget(type: ErrorPageType.component, refreshMethod: () => {}),
loading: () => LoadingWidget(),
);
- 定义
Page
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:riverpod_t/base/base_grid_view.dart';
import 'package:riverpod_t/base/empty_widget.dart';
import 'package:riverpod_t/base/error_river_widget.dart';
import 'package:riverpod_t/base/loading_widget.dart';
import 'package:riverpod_t/page/home/router/home_router.dart';
import 'package:riverpod_t/page/home/widget/car_toon_widget.dart';
import 'package:riverpod_t/page/mine/river/mine_list_river.dart';
import 'package:riverpod_t/page/mine/state/mine_state.dart';
import 'package:riverpod_t/until/app_util.dart';
import '../../../base/base_stateful_page.dart';
class MinePage extends BasePage {
const MinePage({super.key});
@override
BasePageState<BasePage> getState() => _MinePageState();
}
class _MinePageState extends BasePageState<MinePage> {
/// 列表控制器
final ScrollController scrollController = ScrollController();
/// 刷新组建控制器
final RefreshController refreshController = RefreshController(initialRefresh: false);
@override
void initState() {
super.initState();
super.appBarTitle = '我的';
allowBackNavigation = false;
}
@override
Widget buildLeftButton() {
return const SizedBox();
}
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
Widget _listBody(BuildContext context, MineState state, WidgetRef ref) {
if (AppUtil.isEmpty(state.list)) return EmptyWidget();
return BaseGridView(
enablePullDown: true,
enablePullUp: true,
// onRefresh: () => ref.read(mineListRiverProvider.notifier).refresh(isRefresh: true),
onRefresh: () => ref.refresh(mineListRiverProvider.future),
// onRefresh: () => ref.invalidate(mineListRiverProvider),
onLoading: () => ref.read(mineListRiverProvider.notifier).refresh(),
refreshController: refreshController,
scrollController: scrollController,
data: state.list,
padding: EdgeInsets.all(10.h),
childAspectRatio: 0.7,
crossAxisSpacing: 10.w,
mainAxisSpacing: 10.h,
crossAxisCount: 2,
backgroundColor: const Color(0xFFF3F4F8),
itemBuilder: (context22, index) {
return CarTonWidget(
index: index,
model: state.list[index],
onTap: () {
HomeNavigator.toHomeDetailPage(context, imageUrl: state.list[index].image ?? '');
},
);
},
);
}
void _listenOpChange(AsyncValue<MineState>? previous, AsyncValue<MineState> next) {
if (next.value?.loadFinish == true) {
refreshController.refreshCompleted();
refreshController.loadComplete();
}
if (next.value?.hasNext == false) {
refreshController.loadNoData();
}
}
@override
Widget buildPage(BuildContext context) {
return Consumer(
builder: (BuildContext context, WidgetRef ref, Widget? child) {
ref.listen(mineListRiverProvider, (p, n) => _listenOpChange(p, n));
return ref
.watch(mineListRiverProvider)
.when(
skipLoadingOnRefresh: true,
skipLoadingOnReload: true,
skipError: false,
data: (state) {
return _listBody(context, state, ref);
},
error:
(error, stack) =>
ErrorRiverWidget(type: ErrorPageType.component, refreshMethod: () => {}),
loading: () => LoadingWidget(),
);
},
);
}
}
简单来说,UI层通过Consumer或者继承自ConsumerWidget获取到ref,然后通过ref观察river,根据不同的状态,显示不同的页面,当数据请求成功后,拿到state对象,去渲染数据。
下面主要说一下,有几个需要注意的点:
1、ref.listen
ref.listen(mineListRiverProvider, (p, n) => _listenOpChange(p, n));
ref监听mineListRiverProvider里面的状态变化,并且将上一个State和下一个State回调
给业务层,业务层根据State判断需要执行的事件,比如结束刷新,展示暂无数据。如以下代码:
void _listenOpChange(AsyncValue<MineState>? previous, AsyncValue<MineState> next) {
if (next.value?.loadFinish == true) {
refreshController.refreshCompleted();
refreshController.loadComplete();
}
if (next.value?.hasNext == false) {
refreshController.loadNoData();
}
}
reref.refresh和ref.invalidate
ref.refresh 和 ref.invalidate 都用于 强制重新计算 Provider 的状态,但它们在行为和使用场景上有关键区别:
(1) ref.refresh
作用:
立即重新执行 Provider 的创建函数,并返回新的状态值。
适用于需要 立即获取最新数据 的场景(如用户手动刷新)。
特点:
同步操作:会阻塞当前代码,直到 Provider 重新计算完成。
返回新值:直接返回重新计算后的结果。
典型用例:下拉刷新、按钮点击后强制更新数据。
调用ref.refresh会立刻执行build方法,完成计算 Provider 的状态,widget 会立即进行重建。
@override
Future<MineState> build() async {
return MineState(list: await _fetchAllList(1), loadFinish: true, testPage: 1);
(2) ref.invalidate
作用:
标记 Provider 为“过时” ,但不立即重新计算。
依赖该 Provider 的其他 Provider 或 Widget 会在 下次读取时自动触发重新计算。 适用于 延迟更新 或 批量无效化多个 Provider 的场景。
特点:
异步操作:不会阻塞当前代码,实际重新计算延迟到下次访问时。
不返回值:仅标记无效化,不返回新值。
典型用例:退出登录时批量清除多个 Provider 的缓存。
使用场景案例: 退出登录时重置用户信息
final userProvider = FutureProvider((ref) async => fetchUser());
final settingsProvider = Provider((ref) => Settings());
// 在逻辑中
void logout() {
// 标记 userProvider 和 settingsProvider 为过时
ref.invalidate(userProvider);
ref.invalidate(settingsProvider);
// 实际重新计算会在下次读取时触发
}
注意点 :
在使用 Riverpod 中,如果在下拉刷新时使用 ref.invalidate 却发现 Widget 立刻刷新(而不是预期的延迟更新),是由以下原因导致的:
依赖该 Provider 的 Widget 正在监听变化
如果 Widget 通过 ref.watch 监听该 Provider,invalidate 会立即触发 Widget 的重新构建(因为 Riverpod 会标记状态为"过时",并通知所有监听者重新读取)。
为什么 invalidate 有时表现类似 refresh?
监听机制:Riverpod 会通知所有 watch 该 Provider 的 Widget 状态已失效,导致它们立即重新读取数据(看起来像同步刷新)。
自动重建:如果 Widget 的 build 方法中直接 watch 了该 Provider,invalidate 会触发重建。
以下两行代码都会触发重建
onRefresh: () => ref.refresh(mineListRiverProvider.future),
onRefresh: () => ref.invalidate(mineListRiverProvider),
3、skipLoadingOnRefresh 和 skipLoadingOnReload
(1) skipLoadingOnRefresh
作用
当手动刷新(如 ref.refresh)时,跳过显示 loading 状态,直接复用旧数据,直到新数据加载完成。
适用场景:下拉刷新时,避免 UI 闪烁(例如,保留旧数据,静默加载新数据)。
(2) skipLoadingOnReload
作用
当 Provider 自动重新计算(如依赖项变化)时,跳过 loading 状态,直接复用旧数据。
适用场景:依赖变化时(如参数变化),避免 UI 闪烁。
3. 局部刷新案例展示
局部刷新在状态管理这块是一个绕不过去的一个话题,那么 使用Riverpod 如何进行局部刷新,通过具体的案例,来一一说明。
- 案例一
案例分析:
整体页面依赖 网络数据 展示UI,在页面滑动时 修改 头部视图 的 透明度 和 状态栏颜色,也就是说数据请求成功构建一下 总页面,在列表 滑动 时只需要更新 头部视图透明度 即可。
案例实现:
根据之前使用使用 Bloc 的经验,这种典型的页面使用 buildWhen 即可搞定,如果使用 Provider 的话,用 Consumer+Selector 可以实现,使用 Getx 的使用 GetBuilder+Obs 也可以实现。其实 Riverpod 实现的思路也是差不多的,可以使用 多个Consumer+单个River 来实现,通过ref.watch(homeDetailScrollRiverProvider.select** ((state) => state.alpha)) 通过 select方法 监听需要依赖的属性变化,来重新构建对应的 widget。当然也可以通过 多个Consumer+多个River 的思路来实现,用多个 多个River 进行业务代码的拆分,自己有自己的职责,请求数据 的就负责 请求数据,更新页面透明度 的就负责 更新页面透明度,这种方式适用于业务逻辑比较的复杂的场景。
注意点:
凡是需要请求网络数据加载页面的场景,Riverpod 都是生成了 AutoDisposeAsyncNotifierProvider 这个类泛型接受一个 river 和state,但是呢,在业务层中 Riverpod 又对 state 进行了一次封装,默认用了 AsyncValue 包了一层(仅限异步场景),所以呢,上述案例只能使用 多个Consumer+多个River 的思路来实现,因为滑动页面来改变透明度是同步的,不可能在使用 AsyncValue ,所以只能在实现一个 homeDetailScrollRiverProvider 来实现了。代码就不一一贴了,感兴趣的可以下载 Demo 自行观看。
- 案例二
案例分析:
整体页面依赖 网络数据 展示UI,在用户点赞时只需要刷新 点击Widget 即可,别的界面不需要刷新。
案例实现:
使用 两个Consumer+两个比如说,当用户在登录成功之后,会进行页面刷新或者其他的操作,就设计到了状态依赖 即可实现,主页面监听 网络数据变化, 嵌套 点击Widget 的 了,Consumer 仅仅只需要监听点赞属性变化。
Demo 地址。
4. riverProvider传值
代码如下:
@override
Future<CarDetailState> build(int entityId) async {
return CarDetailState(model: await _getDetailInfoData(entityId));
}
家族模式(Family) 进行传值
5. 状态依赖
状态依赖也是个老生常谈的问题,比如说,当用户在登录成功之后,会进行 页面刷新 或者其他的操作,就涉及到了 状态依赖 了,在本Demo中,也写了一个例子,两个 river,一个负责列表的数据请求 listRiverProvider,一个负责对列表数据的操作 operationRiverProvider,操作完成之后通知 listRiverProvider 去进行数据的刷新。具体的代码就不提了,主要是依赖于监听来实现。
/// 初始化请求数据
@override
Future<CarState> build() async {
ref.listen(carInfoOpRiverProvider, _listenOpChange);
return CarState(list: await _getCarList(1));
}
6. 暗黑模式
思路:
class MyHomePage extends ConsumerStatefulWidget {
const MyHomePage({super.key});
@override
ConsumerState<MyHomePage> createState() => _MyHomePageState();
}
MyHomePage 继承 ConsumerStatefulWidget 就可以通过 river 去修改页面的主体。 代码不贴了,看看 Demo 就行。
三. 针对于实际项目的设计和封装
- 项目截图
主要说一下代码的分层吧,职责划分需要明确,提供数据的就提供数据,处理逻辑的就处理逻辑,没啥可说的也。
- 针对于 ConsumerWidget 和 Consumer 的封装 在此处的封装只是针对于 ConsumerWidget 和 Consumer 展示异步数据使用的场景,同步的没啥封装的必要,拿过来用更加方便。
ConsumerWidget:
import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_t/base/error_river_widget.dart';
import 'package:riverpod_t/base/loading_widget.dart';
/// 适用于整个 Widget 需要访问 Provider 的情况 如:列表之类
class BaseConsumerWidget<S> extends ConsumerWidget {
final ProviderListenable<AsyncValue<S>> provider;
final Widget Function(BuildContext context, S state, WidgetRef ref) builder;
final bool skipLoadingOnRefresh;
final bool skipLoadingOnReload;
final Widget Function(Object error, StackTrace stackTrace)? errorBuilder;
final VoidCallback? refresh;
final Widget Function()? loadingBuilder;
/// 监听回调方法
final Function(BuildContext context, AsyncValue<S>, AsyncValue<S>)? listenStateChange;
const BaseConsumerWidget({
super.key,
required this.provider,
required this.builder,
this.skipLoadingOnRefresh = true,
this.skipLoadingOnReload = false,
this.errorBuilder,
this.loadingBuilder,
this.refresh,
this.listenStateChange,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen(provider, (p, n) => _listenStateChange(context, p, n));
return ref
.watch(provider)
.when(
skipLoadingOnRefresh: skipLoadingOnRefresh,
skipLoadingOnReload: skipLoadingOnRefresh,
data: (state) => builder(context, state, ref),
error:
(error, stack) =>
errorBuilder?.call(error, stack) ?? _defaultErrorBuilder(error, stack),
loading: () => loadingBuilder?.call() ?? _loadingBuilder(),
);
}
/// 监听回调
void _listenStateChange(BuildContext context, previous, next) {
if (listenStateChange != null) listenStateChange!(context, previous, next);
}
/// 出错页面
Widget _defaultErrorBuilder(Object error, StackTrace stack) {
return ErrorRiverWidget(type: ErrorPageType.page, refreshMethod: () => refresh?.call());
}
/// loading 页面
Widget _loadingBuilder() {
return LoadingWidget();
}
}
Consumer:
import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_t/base/error_river_widget.dart';
import 'package:riverpod_t/base/loading_widget.dart';
/// 适用于局部刷新 -> 异步的(点赞->转圈->显示结果)
class BaseAsyncConsumer<S> extends StatelessWidget {
final ProviderListenable<AsyncValue<S>> provider;
final Widget Function(BuildContext context, S state, WidgetRef ref) builder;
final bool skipLoadingOnRefresh;
final bool skipLoadingOnReload;
final Widget Function(Object error, StackTrace stackTrace)? errorBuilder;
final Function()? refresh;
final Widget Function()? loadingBuilder;
/// 监听回调方法
final Function(BuildContext context, AsyncValue<S>, AsyncValue<S>)? listenStateChange;
const BaseAsyncConsumer({
super.key,
required this.provider,
required this.builder,
this.skipLoadingOnRefresh = true,
this.skipLoadingOnReload = false,
this.errorBuilder,
this.loadingBuilder,
this.refresh,
this.listenStateChange,
});
@override
Widget build(BuildContext context) {
return Consumer(
builder: (BuildContext context, WidgetRef ref, Widget? child) {
ref.listen(provider, (p, n) => _listenStateChange(context, p, n));
return ref
.watch(provider)
.when(
skipLoadingOnRefresh: skipLoadingOnRefresh,
skipLoadingOnReload: skipLoadingOnReload,
data: (state) => builder(context, state, ref),
error:
(error, stack) =>
errorBuilder?.call(error, stack) ?? _defaultErrorBuilder(error, stack),
loading: () => loadingBuilder?.call() ?? _loadingBuilder(),
);
},
);
}
/// 监听回调
void _listenStateChange(BuildContext context, previous, next) {
if (listenStateChange != null) listenStateChange!(context, previous, next);
}
/// 出错页面
Widget _defaultErrorBuilder(Object error, StackTrace stack) {
return ErrorRiverWidget(type: ErrorPageType.component, refreshMethod: () => refresh?.call());
}
/// loading 页面
Widget _loadingBuilder() {
return LoadingWidget();
}
}
封装思路就是 ConsumerWidget 和 Consumer 做了一个中间层,不需要每次都去写这几行代码,业务层只需要专注于处理业务即可。其实 riverPod在这方面已经很好了。
return ref
.watch(provider)
.when(
skipLoadingOnRefresh: skipLoadingOnRefresh,
skipLoadingOnReload: skipLoadingOnReload,
data: (state) => builder(context, state, ref),
error:
(error, stack) =>
errorBuilder?.call(error, stack) ?? _defaultErrorBuilder(error, stack),
loading: () => loadingBuilder?.call() ?? _loadingBuilder(),
);
- 其他
其他的关于项目的一些设计和想法,看代码就好了,不一一说了。
四. riverPod的优缺点
优点或者说是相对于其他状态管理框架的显著优势吧
1. 零依赖 + 编译安全
- 优势:不依赖 Flutter 的
BuildContext,所有代码在编译时生成,避免运行时错误。 - 对比:传统
Provider需要依赖context,易因上下文丢失导致崩溃。
2. 灵活的状态组合
- 家族模式(Family) :同一 Provider 可参数化(如根据 ID 获取不同数据)。
- 自动销毁(AutoDispose) :自动释放未使用的状态,避免内存泄漏。
3. 极细粒度的性能优化
select监听:仅当特定字段变化时触发 UI 更新。
final username = ref.watch(userProvider.select((user) => user.name));
缺点: 1. 学习曲线较陡
- 概念复杂:
AsyncNotifier、Family、AutoDispose等概念对新手不友好。 - 代码生成:需熟悉
build_runner和注解(如@riverpod)。
2. 模板代码较多
state riverProvider 需要需手动编写。