Flutter状态管理之Riverpod 2.0

2023-05-16

两年前分享过一篇Flutter状态管理之Riverpod,当时riverpod的版本还是0.8.0(后来文章更新到0.14版本)。当时提到过有一些不足之处:

  • 毕竟诞生不久,它还不能保证是完全稳定的。
  • 可能后期会有API的破坏性改动。
  • 目前生产环境中使用需要谨慎。

随着作者两年来的不断维护完善,这些问题基本上已经不存在了。毕竟最痛苦的API破坏性改动已经在1.0版本时处理了一遍。

目前版本来到了2.1.1,所以本篇是对最新API用法的一个更新,还是沿用之前那篇的用例作说明。

1.配置

将它添加到pubspec.yaml中:

flutter_riverpod: ^2.1.1

然后执行flutter pub get

3.基础使用

Provider

这里使用RiverpodProvider需要三步就可以。

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 1.创建一个全局的provider,里面储存“Hello World!”
final Provider<String> helloWorldProvider = Provider((_) => 'Hello World!');

void main() {
  runApp(
    // 2.添加“ProviderScope”。所有使用Riverpod的Flutter程序都必须
    // 在widget tree的根部添加它,用来储存各个provider。
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Riverpod Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: ProviderExample(),
    );
  }
}

/// 3.使用“ConsumerWidget”,在“build”中获取对应的provider
class ProviderExample extends ConsumerWidget {

  
  Widget build(BuildContext context, WidgetRef ref) {
    final String value = ref.watch(helloWorldProvider);

    return Scaffold(
      appBar: AppBar(title: Text('Provider')),
      body: Center(
        child: Text(value),
      ),
    );
  }
}

这里储存“Hello World!” 使用的是Provider,它提供一个永远不变的对象。不过大部分场景下状态都是可变的,下面用计数器来举例。

StateProvider

在“Hello World”的基础上,做两点修改即可。

定义一个全局常量StateProvider

final StateProvider<int> counterProvider = StateProvider((_) => 0);
class StateProviderExample extends ConsumerWidget {

  
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: Text('StateProvider'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Consumer(
              builder: (context, ref, _) {
                /// 使用Consumer(ConsumerWidget的封装),控制刷新的范围。
                int count = ref.watch(stateProvider);
                return Text(
                  '$count',
                  style: Theme.of(context).textTheme.headline4,
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        /// 使用read获取counterProvider,操作state。
        onPressed: () => ref.read(stateProvider.notifier).state++,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }

}

ChangeNotifierProvider

这部分没啥说的,注意ChangeNotifier需要自己调用notifyListeners通知变更。

final ChangeNotifierProvider<Counter> _counterProvider = ChangeNotifierProvider((_) => Counter());

class Counter extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
  void decrement(){
    _count--;
    notifyListeners();
  }
}

class ChangeProviderNotifierExample extends ConsumerWidget {

  
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ChangeNotifierProvider'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Consumer(
              builder: (context, ref, _) {
                int count = ref.watch(changeNotifierProvider).count;
                return Text(
                  '$count',
                  style: Theme.of(context).textTheme.headline4,
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        /// 使用read获取counterProvider。
        onPressed: () => ref.read(changeNotifierProvider).increment(),
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }

}

FutureProvider

final FutureProvider<String> futureProvider = FutureProvider((_) async {
  /// 延时3s
  await Future.delayed(const Duration(seconds: 3));
  return 'Riverpod';
});

class FutureProviderExample extends StatelessWidget {

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('FutureProvider'),
      ),
      body: Center(
        child: Consumer(
          builder: (context, ref, _) {
            AsyncValue<String> futureProviderValue = ref.watch(futureProvider);
            return futureProviderValue.when(
              loading: () => CircularProgressIndicator(),
              error: (error, stack) => Text('Oops, something unexpected happened'),
              data: (value) => Text(
                'Hello $value',
                style: Theme.of(context).textTheme.headline4,
              ),
            );
          },
        ),
      ),
    );
  }
}

作者也提供了StreamProvider。用法大同小异,有兴趣的可以查看我的示例代码。

ProviderListener

如果你希望在Widget Tree上监听provider的状态变化,可以使用listen。用上面的计数器例子,当计数器为5时,触发监听。

final StateProvider<int> stateProvider = StateProvider((_) => 0);

class ProviderListenerExample extends ConsumerWidget {

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.listen(stateProvider, (previous, next) {
      if (next == 5) {
        print('当前计数器为5,触发监听。');
      }
    });
    ...
  }
}

ProviderScope

一般我们在实现一个列表的Item时,需要传入相应的index大致如下:

ListView.builder(
  itemCount: 50,
  itemBuilder: (context, index) {
    return ProductItem(index: index);
  },
)

如果使用 ProviderScope,就可以覆盖index的值,内部通过ref.watch获取,就不必从构造方法接收它。使用起来很简单,直接上代码:

final Provider<int> currentProductIndex = Provider<int>((_) => 0);

class ScopeProviderExample extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ScopedProvider'),
      ),
      body: ListView.builder(
        itemCount: 50,
        itemBuilder: (context, index) {
          return ProviderScope(
            overrides: [
              /// 修改value
              currentProductIndex.overrideWithValue(index),
            ],
            /// 使用'const'关键字实例化了“ProductItem”,但仍然可以在内部动态获取内容。
            child: const ProductItem(),
          );
        },
      ),
    );
  }
}

class ProductItem extends ConsumerWidget {

  const ProductItem({Key? key}): super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final index = ref.watch(currentProductIndex);
    return ListTile(title: Text('item $index'));
  }
}

4.修饰符

family

family的作用是可以在获取provider时可以添加一个参数。直接上例子,一看便知:

/// 使用family,可以在获取provider时传入city
final _weatherProvider = Provider.family<String, String>((ref, city) {
  return '$city (Sunny)';
});

class FamilyExample extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Family')),
      body: Center(
        child: Consumer(
          builder: (context, ref, _) {
            /// 这里可以传参“London”
            final String weather = ref.watch(_weatherProvider('London'));
            return Text('$weather',);
          },
        ),
      ),
    );
  }
}

注意: 使用family时传入的参数是有限制的。比如bool intdoubleString 、常量或是重写了==hashCode的不可变对象。

family的目的是根据外部参数获取对应的provider。例如接口传参:

final messagesFamily = FutureProvider.family<Message, String>((ref, id) async {
  return dio.get('http://my_api.dev/messages/$id');
});

autoDispose

前面我们的例子中,创建的provider因为保存在Widget Tree的根部。所以即使页面关闭,再次进入页面时会获取之前的状态。

这显然是有局限性的,那么这里就可以使用autoDispose,它可以在我们不再使用provider时,自动将其销毁。那么合理的使用它可以避免内存泄漏。

比如之前的计数器例子,只需加一个autoDispose就可以避免此类问题。

final stateProvider = StateProvider.autoDispose((_) => 0);

如果你需要自定义dispose事件,可以使用onDispose。比如你的provider中有网络请求(使用Dio):

final myProvider = FutureProvider.autoDispose((ref) async {
  
  final cancelToken = CancelToken();
  // 当provider被销毁时,取消http请求
  ref.onDispose(() => cancelToken.cancel());

  // http请求
  final response = await dio.get('path', cancelToken: cancelToken);
  // 如果请求成功完成,则保持该状态。
  ref.keepAlive();
  return response;
});

上面代码中出现了keepAlive方法。如果用户离开页面并且请求失败,下次则将再次执行该请求。但是,如果请求成功完成调用了keepAlivee,则将保留状态,下次重新进入页面时不会触发新的请求。

使用autoDispose可以达到限制provider是全局还是局部作用。这样一来,可以更方便的解决跨页面使用provider的问题。

5.进阶使用

Combining providers

1.如果创建的provider需要另一个provider的状态,这时就需要使用Refread方法。

下面的示例是,给予城市和国家的provider,当创建locationProvider时,获取城市和国家的状态。

final Provider<String> cityProvider = Provider<String>((ref) => 'London');
final Provider<String> countryProvider = Provider<String>((ref) => 'England');
final Provider<Location> locationProvider = Provider<Location>((ref) => Location(ref));

class Location {
  Location(this._ref);

  final Ref _ref;

  String get label {
    final city = _ref.read(cityProvider);
    final country = _ref.read(countryProvider);
    return '$city ($country)';
  }
}

使用Riverpod就可以提供多个相同类型的Provider,这也是相比Provider框架的一个优点。

2.如果获取的状态值会发生变化,我们需要监听它。可以使用Refwatch方法。

下面的示例是,当cityProvider变化时,weatherProvider也相应变化。

final StateProvider<String> cityProvider = StateProvider((ref) => 'London');
final StateProvider<String> weatherProvider = StateProvider((ref) {
  /// watch监听
  final String city = ref.watch(cityProvider).state;
  return '$city (Sunny)';
});

class CombiningProviderExample2 extends ConsumerWidget {

  
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(title: Text('CombiningProvider')),
      body: Center(
        child: Consumer(
          builder: (context, ref, _) {
            final String weather = ref.watch(weatherProvider);
            return Text('$weather',);
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          /// 修改状态
          String city = ref.read(cityProvider.notifier).state;
          if (city == 'London') {
            ref.read(cityProvider.notifier).state = "Xi'an";
          } else {
            ref.read(cityProvider.notifier).state = 'London';
          }
        },
        tooltip: 'Refresh',
        child: Icon(Icons.refresh),
      ),
    );
  }
}

refresh

强制provider立即刷新,重新返回创建的值。这种适合列表下拉刷新,或者请求数据错误时重试。


final FutureProvider<List<String>> productsProvider = FutureProvider((_) async {
  /// 延时3s
  await Future.delayed(const Duration(seconds: 3));
  return List.generate(50, (index) => 'Item $index');
});

class RefreshProviderExample extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('RefreshProvider'),
      ),
      body: Center(
        child: Consumer(
          builder: (context, ref, _) {
            AsyncValue<List<String>> productsProviderValue = ref.watch(productsProvider);
            return productsProviderValue.when(
              loading: () => CircularProgressIndicator(),
              error: (error, stack) => Text('Oops, something unexpected happened'),
              data: (list) => RefreshIndicator(
                onRefresh: () => ref.refresh(productsProvider.future), /// 刷新
                child: ListView(
                  children: [
                    for (final item in list) ListTile(title: Text(item)),
                  ],
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

select

当状态中某一个值发生变化时,相应Consumer下的builder就会执行,重建widget。如果使用select可以指定某一值更改时进行刷新,精准控制刷新范围,避免不必要的rebuild。

final ChangeNotifierProvider<Person> personProvider = ChangeNotifierProvider((_) => Person());

class Person extends ChangeNotifier {
  int _age = 0;
  int get age => _age;
  set age(int age) {
    _age = age;
    notifyListeners();
  }

  String _name = 'weilu';
  String get name => _name;
  set name(String name) {
    _name = name;
    notifyListeners();
  }
}

class SelectExample extends ConsumerWidget {

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Select Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Consumer(
              builder: (_, ref, __) {
                String name = ref.watch(personProvider.select((p) => p.name));
                return Text(
                  'name:$name',
                );
              },
            ),
            Consumer(
              builder: (_, ref, __) {
                int age = ref.watch(personProvider.select((p) => p.age));
                return Text(
                  'age:$age',
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        // 这里age变化时,只有对应的Text会变化。
        onPressed: () => ref.read(personProvider).age = Random.secure().nextInt(100),
        tooltip: 'Refresh',
        child: Icon(Icons.refresh),
      ),
    );
  }

}

补充

  • 上面的例子中,我们为了获取ref使用了ConsumerWidget。如果你使用的是StatelessWidget可以直接替换为ConsumerWidget。如果你需要使用StatefulWidget里面的一些方法(比如initState)。可以使用ConsumerStatefulWidget + ConsumerState替代。
class HomeView extends ConsumerStatefulWidget {
  const HomeView({Key? key}): super(key: key);

  
  HomeViewState createState() => HomeViewState();
}

class HomeViewState extends ConsumerState<HomeView> {
  
  void initState() {
    super.initState();
    // “ref”可用于StatefulWidget的所有生命周期。
    ref.read(counterProvider);
  }

  
  Widget build(BuildContext context) {
    final counter = ref.watch(counterProvider);
    return Text('$counter');
  }
}
  • 作者建议:最好使用ref.watch而不是ref.readref.listen来实现功能。通过依赖ref.watch,您的应用程序变得既被动又具有声明性,这使其更具可维护性。

ref.read方法是无需监听即可获得provider状态的方法,通常用于执行方法。例如上面的例子中当点击按钮时,计数加一。如果这里你使用ref.watch获取provider,整个widget都会刷新。

其他场景建议都使用ref.watch获取状态,即使目前可能不会更新(有时你可能因为优化性能的考虑使用read),但不能保证未来它不会变,一旦未来需要更改,修改为watch的过程中你可能会忘记一些情况,导致出错。为了更具可维护性,所以建议最好使用ref.watch。这样重构时遇到的问题会更少。

  • 对于上面的各种provider,有时很难理解何时使用这种provider类型而不是另一种provider类型。可以参考下面的表格:
类型创建方法使用场景
Provider返回任何类型单例/计算属性
StateProvider返回任何类型过滤器条件/简单状态对象
FutureProvider返回任何类型的FutureAPI调用的结果
StreamProvider返回任何类型的Stream来自API的结果流
StateNotifierProvider返回StateNotifier的子类复杂的状态对象,它是不可变的,除非通过接口
ChangeNotifierProvider返回ChangeNotifier的子类需要可变性的复杂状态对象

个人认为Riverpod是相对更轻松便捷的一种状态管理方式,待它稳定时应该能被更多的人喜爱。

以上这段话是我两年前的文中说到的,两年后的今天我的观点还是没变。或者说Riverpod是我更推荐的一种状态管理方式。别问我getx,我个人不喜欢把鸡蛋放到一个篮子里。

以上有关Riverpod的相关示例代码我已经上传至Github,有兴趣的可以看看,后面我也会持续维护。

参考

  • Riverpod文档
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Flutter状态管理之Riverpod 2.0 的相关文章

随机推荐

  • PHP源码目录说明

    1 build 和编译有关的目录 2 ext 扩展库代码 xff0c 例如 MySQL zlib iconv 等我们熟悉的扩展库 其中 ext standard 目录下是常用的标准函数集 3 main 主目录包含主要的 PHP 宏和定义 4
  • 终端程序定义示例

    interrupt 0 外部中断0 interrupt 1 T0中断 interrupt 2 外部中断1 interrupt 3 T1中断 interrupt 4 串口中断 我来告诉你实质 xff1a 单片机的中断处理是这样工作的 xff0
  • lisp学习资料

    中文LISP学习网站 CL HTTP franz com 217条消息 使用hunchentoot搭建Lisp web 服务器 keyboardOTA的博客 CSDN博客
  • GO调用C语言之字符串传递

    C xff1a hello h include lt stdlib h gt include lt stdio h gt include lt string h gt char abc int strnum char str C hello
  • C++-基础语法-cin.getline() 与 cin.get() 的区别,以及getline()函数使用方法

    参考声明 xff1a C 43 43 primer plus https blog csdn net best fiends zxh article details 53064771 https blog csdn net u0114216
  • Kotlin开发中的一些Tips(二)

    接着上一篇 xff0c 最近又整理了一些 1 作用域函数选择 目前有let run with apply 和 also五个作用域函数 官方文档有张表来说明它们之间的区别 xff1a 总结一下有几点区别 xff1a apply和also返回上
  • Jetpack Compose 从入门到入门(一)

    Jetpack Compose 是用于构建原生 Android 界面的新工具包 它使用更少的代码 强大的工具和直观的 Kotlin API xff0c 可以帮助您简化并加快 Android 界面开发 xff0c 打造生动而精彩的应用 它可让
  • Jetpack Compose 从入门到入门(二)

    开始布局部分 这部分我个人感觉没有必要每个组件 属性都详细说到 xff0c 否则篇幅会很长 建立起Compose中的组件与 Android Views的一个对应关系就够了 具体还是需要在实际的使用中去熟悉 1 Column 子元素按竖直顺序
  • Jetpack Compose 从入门到入门(三)

    本篇开始介绍Jetpack Compose 中的修饰符Modifier 修饰符可以用来执行以下操作 xff1a 更改可组合项的大小 布局 行为和外观 添加信息 xff0c 如无障碍标签 处理用户输入 添加高级互动 xff0c 如使元素可点击
  • 【操作系统】Linux系统中直接优化提升CPU性能(已解决)

    文章目录 问题 xff1a 服务器CPU没有调用最高性能 xff0c 导致跑算法的时候处理速度慢一 BIOS方法二 终端直接设置CPU调节器方法1 查看当前CPU调节器2 安装各种依赖库3 最后安装cpufrequtis工具包并设置CPU调
  • Jetpack Compose 从入门到入门(四)

    本篇开始介绍Jetpack Compose 中常用的组件 有一部分之前的文章中也出现过 xff0c 今天详细说明一下 1 Text 日常最常用的应该就是显示文字 xff0c 所以有必要说一下Text控件 首先源码如下 xff1a span
  • Jetpack Compose 从入门到入门(五)

    应用中的状态是指可以随时间变化的任何值 这是一个非常宽泛的定义 xff0c 从 Room 数据库到类的变量 xff0c 全部涵盖在内 由于Compose是声明式UI xff0c 会根据状态变化来更新UI xff0c 因此状态的处理至关重要
  • Jetpack Compose 从入门到入门(六)

    本篇说说Compose中的Canvas 1 Canvas span class token annotation builtin 64 Composable span span class token keyword fun span sp
  • Jetpack Compose 从入门到入门(七)

    本篇进入Compose 动画部分 1 动画预览 在本系列第一篇中我们提到过 xff0c 64 Preview可以帮我们实现UI的预览功能 xff0c 简单的交互和播放动画 在Android Studio Bumblebee xff08 大黄
  • Android 12 变更及适配攻略

    这几个月有点忙 xff0c 一年一篇的适配文章来的有点晚了 但其实也还好 xff0c 因为我们项目也是下半年才适配 我这边也是提前调研踩坑 xff0c 评估一下工作量 这个时间点也完全跟得上Google Play的审核要求 xff08 11
  • Jetpack Compose 从入门到入门(八)

    接着上一篇的动画部分 xff0c 本篇主要是自定义动画与Animatable AnimationSpec 上一篇中 xff0c 出现了多次animationSpec属性 xff0c 它是用来自定义动画规范的 例如 xff1a span cl
  • Jetpack Compose 从入门到入门(九)

    本篇是Compose的手势部分 点击 添加clickable修饰符就可以轻松实现元素的点击 此外它还提供无障碍功能 xff0c 并在点按时显示水波纹效果 span class token annotation builtin 64 Comp
  • 记参加 2022 Google开发者大会

    前几天有幸参加了2022年Google 开发者大会 Google Developer Summit xff0c 上一次参加Google开发者大会还是2019年 这期间因为众所周知的原因 xff0c 开发者大会都改为了线上举办 和上次相比可以
  • Jetpack Compose 从入门到入门(十)

    本篇介绍如何将Jetpack Compose 添加到已有应用中 xff0c 毕竟大多数情况都是在现有项目中使用 Jetpack Compose 旨在配合既有的基于 View 的界面构造方式一起使用 如果您要构建新应用 xff0c 最好的选择
  • Flutter状态管理之Riverpod 2.0

    两年前分享过一篇Flutter状态管理之Riverpod xff0c 当时riverpod的版本还是0 8 0 xff08 后来文章更新到0 14版本 xff09 当时提到过有一些不足之处 xff1a 毕竟诞生不久 xff0c 它还不能保证