Flutter 使用riverpod 搭建通用项目架构

825 阅读12分钟
前言:
之前写过
Flutter 基于getX搭建通用项目架构
Flutter 基于 Bloc搭建通用项目架构
Flutter Provider+MVVM搭建通用项目架构
虽然在实际的项目开发中一直使用的是Bloc,但是对于 riverpod 还是很感兴趣,终于有点时间用 riverpod 写了个小Demo,和大家分享下riverpod的魅力。

一. riverPod 简介

riverpodprovider 是出自一人之手,在名称上 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 命令来生成文件了。

WX20250714-152641.pngWX20250714-152657.pngWX20250714-152716.png!

在视图逻辑处理中,首先要在最顶层嵌套 ProviderScope 组件:

WX20250714-153311.png

然后通过 flutter_riverpod 中提供的 Consumer 组件,使用 builder 回调构建组件。其中的第二入参是 WidgetRef ,可以通过该对象来 获取并观察 某个 XXXNotifierProvider 全局变量 。

WX20250714-153937.png 点击计数器的回调方法,可以通过 ref.read 仅访问 counterProvider ,得到 Counter 对象,触发 increment 方法。

WX20250714-154704.png

除了 Consumer 组件之外,flutter_riverpod 还提供了 ConsumerWidget 组件,通过继承的关系,子类在 build 回调中得到 WidgetRef 对象,在构建逻辑中使用。如果这里让 一个页面 继承自 ConsumerWidget,那更新通知将会触发 build 方法,页面整体都会重新构建 。

WX20250714-160403.png

所以如果希望控制更新的小粒度,就需要拆分组件,下面会写几个例子来演示。

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();
  }
}
  1. reref.refreshref.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、skipLoadingOnRefreshskipLoadingOnReload

(1) skipLoadingOnRefresh

作用

当手动刷新(如 ref.refresh)时,跳过显示 loading 状态,直接复用旧数据,直到新数据加载完成。

适用场景:下拉刷新时,避免 UI 闪烁(例如,保留旧数据,静默加载新数据)。

(2) skipLoadingOnReload

作用

当 Provider 自动重新计算(如依赖项变化)时,跳过 loading 状态,直接复用旧数据。

适用场景:依赖变化时(如参数变化),避免 UI 闪烁。

3. 局部刷新案例展示

局部刷新在状态管理这块是一个绕不过去的一个话题,那么 使用Riverpod 如何进行局部刷新,通过具体的案例,来一一说明。

  • 案例一

Jul-20-2025 15-14-50.gif

案例分析:

整体页面依赖 网络数据 展示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 这个类泛型接受一个 riverstate,但是呢,在业务层中 Riverpod 又对 state 进行了一次封装,默认用了 AsyncValue 包了一层(仅限异步场景),所以呢,上述案例只能使用 多个Consumer+多个River 的思路来实现,因为滑动页面来改变透明度是同步的,不可能在使用 AsyncValue ,所以只能在实现一个 homeDetailScrollRiverProvider 来实现了。代码就不一一贴了,感兴趣的可以下载 Demo 自行观看。

  • 案例二

Jul-20-2025 18-43-21.gif

案例分析:

整体页面依赖 网络数据 展示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 就行。

三. 针对于实际项目的设计和封装

  • 项目截图

WX20250721-124402@2x.png

主要说一下代码的分层吧,职责划分需要明确,提供数据的就提供数据处理逻辑的就处理逻辑,没啥可说的也。

  • 针对于 ConsumerWidgetConsumer 的封装 在此处的封装只是针对于 ConsumerWidgetConsumer 展示异步数据使用的场景,同步的没啥封装的必要,拿过来用更加方便。

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();
  }
}

封装思路就是 ConsumerWidgetConsumer 做了一个中间层,不需要每次都去写这几行代码,业务层只需要专注于处理业务即可。其实 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. 学习曲线较陡

  • 概念复杂AsyncNotifierFamilyAutoDispose 等概念对新手不友好。
  • 代码生成:需熟悉 build_runner 和注解(如 @riverpod)。

2. 模板代码较多

state riverProvider 需要需手动编写。

Demo 地址