flutter 专题二 Flutter状态管理之Riverpod 0.8.4

一 、flutter 有哪些状态管理方式

Flutter的状态管理方式有很多,Redux、 Bloc、 MobX、Provider等等。单单一个Provider,我也见到了各种组合,例如ChangeNotifier + Provider / StateNotifier + Provider( + freezed)。各种方式各有千秋,我们根据自己的习惯和项目的情况去选择就好,这里不做讨论;

二 、Riverpod 介绍

Riverpod和Provider师出同门,都来自作者Remi,Riverpod可以被认为是Provider的重写,来实现原本不可能的功能。就像它的名字一样,字母与Provider相同,但是又不相同。

你可以理解Riverpod是Provider的升级版,解决了Provider的一些痛点:

Provider是InheritedWidget的封装,所以在读取状态时需要BuildContext。这导致了许多的限制,许多新手在不理解InheritedWidget和BuildContext时,跨页面获取状态经常会ProviderNotFoundException。而Riverpod不再依赖Flutter,也就是没有使用InheritedWidget,所以也不需要BuildContext。

读取对象是编译安全的。没有那么多的运行时异常。

能够有多个相同类型的provider。

provider可以是私有的。

当不再使用provider的状态时,将其自动回收。

当然目前Riverpod也有一些不足(0.14.0+3版本):

  • 毕竟诞生不久,它还不能保证是完全稳定的。
  • 可能后期会有API的破坏性改动。(比如在0.7.0就有不少Breaking,导致我之前写的部分示例内容就报错了。)
  • 目前生产环境中使用需要谨慎。

三、Riverpod的三种使用方式

Riverpod 提供了三种使用方式,如下图,可以根据自己的实际项目选择适合自己的方式

本篇不引入flutter_hooks相关内容,这里我就选择flutter_riverpod 。那么将它添加到pubspec.yaml

flutter_riverpod: ^0.14.0+3

四 、flutter 基础使用

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 {@overrideWidget 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 {@overrideWidget build(BuildContext context, ScopedReader watch) {final String value = watch(helloWorldProvider);return Scaffold(appBar: AppBar(title: Text('Provider Example')),body: Center(child: Text(value),),);}
}

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

StateProvider

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

  1. 定义一个全局常量StateProvider

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

2、 

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

如果你的状态比较复杂可以使用ChangeNotifierProvider,如果习惯使用StateNotifier,可以使用StateNotifierProvider 。其实StateProvider的内部是StateController,也还是StateNotifier。源码如下;

class StateProvider<T>extends AlwaysAliveProviderBase<StateController<T>, StateController<T>> {StateProvider(Create<T, ProviderReference> create, {String name,}) : super((ref) => StateController(create(ref)), name);...
}class StateController<T> extends StateNotifier<T> {StateController(T state) : super(state);@overrideT get state => super.state;@overrideset state(T value) => super.state = value;
}

StateNotifierProvider的用法与StateProvider基本一致,这里就不贴出来了,有兴趣的可以点击这里查看。

2021-04-19更新:

0.14.0对StateNotifierProvider的语法有破坏性变化,避免了StateNotifierProvider的错误使用。具体见文档。

ChangeNotifierProvider
这部分没啥说的,注意ChangeNotifier与StateNotifier的区别,需要自己调用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 StatelessWidget {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('ChangeNotifierProvider Example'),),body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[Text('You have pushed the button this many times:',),Consumer(builder: (context, watch, _) {int count = watch(_counterProvider).count;return Text('$count',style: Theme.of(context).textTheme.headline4,);},),],),),floatingActionButton: FloatingActionButton(/// 使用read获取counterProvider。onPressed: () => context.read(_counterProvider).increment(),tooltip: 'Increment',child: Icon(Icons.add),),);}
}

FutureProvider

final FutureProvider<String> futureProvider = FutureProvider((_) async {/// 延时3sawait Future.delayed(const Duration(seconds: 3));return 'Riverpod';
});class FutureProviderExample extends StatelessWidget {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('FutureProvider Example'),),body: Center(child: Consumer(builder: (context, watch, _) {AsyncValue<String> futureProviderValue = 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的状态变化,可以使用ProviderListener。用上面的计数器例子,当计数器为5时,触发监听。

ProviderListener<StateController<int>>(provider: counterProvider,onChange: (_, counter) {if (counter.state == 5) {print('当前计数器为5,触发监听。');}},child: Consumer(builder: (context, watch, _) {int count = watch(counterProvider).state;return Text('$count',style: Theme.of(context).textTheme.headline4,);},),
),

ScopeProvider

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

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

如果使用ScopedProvider并结合 ProviderScope,就可以简单的获取index,不必从构造方法接收它。使用起来很简单,直接上代码:

/// 定义ScopedProvider
final ScopedProvider<int> currentProductIndex = ScopedProvider<int>(null);class ScopeProviderExample extends StatelessWidget {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('ScopedProvider'),),body: ListView.builder(itemCount: 50,itemBuilder: (context, index) {return ProviderScope(overrides: [/// 修改valuecurrentProductIndex.overrideWithValue(index),],/// 使用'const'关键字实例化了“ProductItem”,/// 但仍然可以在内部动态获取内容。child: const ProductItem(),);},),);}
}class ProductItem extends ConsumerWidget {const ProductItem({Key key}): super(key: key);@overrideWidget build(BuildContext context, ScopedReader watch) {/// 获取相应indexfinal index = 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 {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('Family')),body: Center(child: Consumer(builder: (context, watch, _) {/// 这里可以传参“London”final String weather = watch(_weatherProvider('London'));return Text('$weather',);},),),);}
}

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

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.maintainState = true;return response;
});

上面代码中出现了ref.maintainState,这个参数默认为false。如果用户离开页面并且请求失败,下次则将再次执行该请求。但是,如果请求成功完成(maintainState为true),则将保留状态,下次重新进入页面时不会触发新的请求。

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

5.进阶使用
Combining providers
1.如果创建的provider需要另一个provider的状态,这时就需要使用ProviderReference的read方法。

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

final Provider<String> cityProvider = Provider((ref) => 'London');
final Provider<String> countryProvider = Provider((ref) => 'England');
final Provider<Location> locationProvider = Provider((ref) => Location(ref));class Location {Location(this._ref);final ProviderReference _ref;String get label {/// read 获取final city = _ref.read(cityProvider);final country = _ref.read(countryProvider);return '$city ($country)';}
}

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

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

下面的示例是,给予城市provider,当城市变化时,天气也相应变化。

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 StatelessWidget {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('CombiningProvider')),body: Center(child: Consumer(builder: (context, watch, _) {final String weather = watch(weatherProvider).state;return Text('$weather',);},),),floatingActionButton: FloatingActionButton(onPressed: () {String city = context.read(cityProvider).state;/// 修改状态if (city == 'London') {context.read(cityProvider).state = "Xi'an";} else {context.read(cityProvider).state = 'London';}},tooltip: 'Refresh',child: Icon(Icons.refresh),),);}
}

refresh

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


final FutureProvider<List<String>> productsProvider = FutureProvider((_) async {/// 延时3sawait Future.delayed(const Duration(seconds: 3));return List.generate(50, (index) => 'Item $index');
});class RefreshProviderExample extends StatelessWidget {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('RefreshProvider'),),body: Center(child: Consumer(builder: (context, watch, _) {AsyncValue<List<String>> productsProviderValue = watch(productsProvider);return productsProviderValue.when(loading: () => CircularProgressIndicator(),error: (error, stack) => Text('Oops, something unexpected happened'),data: (list) => RefreshIndicator(onRefresh: () => context.refresh(productsProvider), /// 刷新child: ListView(children: [for (final item in list) ListTile(title: Text(item)),],),),);},),),);}
}

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

不过目前(0.14.0+3版本),select这种局部监听只支持使用hooks_riverpod包的useProvider。所以这里需要引用hooks_riverpod。

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 StatelessWidget {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('Select Example'),),body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[HookBuilder(builder: (_) {String name = useProvider(personProvider.select((p) => p.name));/// 如果使用下面的方式,则age变化时,这里的Text也会刷新。
//                String name = useProvider(personProvider).name;return Text('name:$name',);},),HookBuilder(builder: (_) {int age = useProvider(personProvider.select((p) => p.age));return Text('age:$age',);},),],),),floatingActionButton: FloatingActionButton(// 这里age变化时,只有对应的Text会变化。onPressed: () => context.read(personProvider).age = Random.secure().nextInt(255),tooltip: 'Refresh',child: Icon(Icons.refresh),),);}
}

其他
细心的你会发现,在使用read获取provider时还是使用了context。一开始不是说,没有使用InheritedWidget,所以也不需要BuildContext吗?

其实Riverpod本身确实如此,但是在Flutter的应用中,为了便于高效(时间复杂度O(1))的在Widget Tree中获取ProviderContainer(在ProviderScope中隐式创建,用来储存provider),需要在根部使用InheritedWidget,便于最终获取provider。

read、refresh、Consumer、ProviderListener等方法和Widget的内部其实都调用了ProviderScope.containerOf(context, listen = xx);,不同的是listen的值。

static ProviderContainer containerOf(BuildContext context, {bool listen = true,}) {UncontrolledProviderScope scope;if (listen) {scope = context //.dependOnInheritedWidgetOfExactType<UncontrolledProviderScope>();} else {scope = context.getElementForInheritedWidgetOfExactType<UncontrolledProviderScope>().widget as UncontrolledProviderScope;}return scope.container;}

比如read中listen的值为false,使用getElementForInheritedWidgetOfExactType方法,这样在数据发生变化时就不会掉用didChangeDependencies,避免不必要的rebuild。相对的,Consumer、ProviderListener中listen的值为ture,会实现我们需要的widget重建。

我们可以还可以通过Flutter Inspector检查已有的状态,所有状态汇总在ProviderScope下面,这也是Riverpod的一个优点。如下图所示:

发布本篇时,有关Riverpod的资料与讨论很少。本篇也是我在实践完官网文档后的理解,如有错误,欢迎指出!

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

Riverpod的相关示例代码我已经上传至Github,有兴趣的可以看看。后面如果Riverpod有变动时,我也会及时更新。大家可以收藏起来,多多点赞支持一下,给我点更新动力!

5、参考

https://riverpod.dev/docs

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/59015.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

海风里的青春:海滨学院班级回忆录开发

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…

ts:用加减乘除方法配合展示类的继承(extends)

ts&#xff1a;用加减乘除方法配合展示类的继承&#xff08;extends&#xff09; 1 主要内容说明2 例子2.1 基类的创建2.1.1 源码1 &#xff08;基类的创建&#xff09;2.1.2 源码1运行效果 2.2 继承基类的参数和方法2.2.1 源码2&#xff08;继承基类的参数和方法&#xff09;2…

Java中的容器及其区别

|Collection |  ├List |  │-├LinkedList |  │-├ArrayList |  │-└Vector |  │ └Stack |  ├Set |  │├HashSet |  │├TreeSet |  │└LinkedSet | |Map   ├Hashtable   ├HashMap   └WeakHashMap 1、Java中的容器 Java容器类…

论文阅读- --DeepI2P:通过深度分类进行图像到点云配准

目前存在的问题&#xff1a; 单模态配准具有局限性&#xff0c;多模态研究很少跨模态图像到点云配准问题是求解相机坐标系与点云之间的旋转矩阵R ∈ SO(3)和平移向量t ∈ R3。 这个问题很困难&#xff0c;因为由于缺乏点到像素的对应关系&#xff0c;无法使用 ICP、PnP 和捆绑调…

Air780E如何发送SMS?一文详解!

今天一起来学习使用合宙低功耗4G模组Air780E发送SMS短消息&#xff1a; 一、SMS简介 SMS&#xff08;短消息服务&#xff0c;ShortMessageService&#xff09;功能主要用于在蜂窝网络中传输短消息。 在4G网络中&#xff0c;短信可以在数据传输的同时进行&#xff0c;不会因数…

Centos 7系统一键安装宝塔教程

服务器推荐青鸟云服务器&#xff0c;2H2G低至16元/月 官网地址&#xff1a; 所有产品_香港轻量云 2核 2G-A型_青鸟云 推荐Finalshell软件连接至服务器&#xff0c;下载地址&#xff1a; https://dl.hostbuf.com/finalshell3/finalshell_windows_x64.exe 下载完成后连接服务…

知识吾爱纯净版小程序系统 leibiao SQL注入漏洞复现(XVE-2024-30663)

0x01 产品简介 知识吾爱纯净版小程序系统是一款基于微信小程序平台开发的知识付费应用,旨在帮助用户快速建立自己的知识付费平台,实现支付变现和流量主收益。它提供了简洁明了的用户界面和良好的用户体验,同时注重用户隐私保护,确保用户信息的安全存储和传输。 0x02 漏洞…

HTML前端页面设计静态网站

浅浅分享一下前端作业&#xff0c;大佬轻喷~ <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>一个网…

各大自媒体平台的感受 —— 小红书

如果你有些内容&#xff0c;想发布到自媒体平台上&#xff0c;目前有很多平台可用选择。 发布自媒体在获得关注的同时&#xff0c;同时也会期望有些收益。 我们的内容也发布了不少时间了&#xff0c;那就分别扒下各大自媒体平台的吃相&#xff0c;这篇文章说的是小红书。 收…

openEuler下配置openGauss环境图解

一、在openEuler中创建用户&#xff0c;并授予权限 # 创建用户 sudo adduser omm# 授予权限 chown omm /opt# 切换用户 su - omm 二、在openGauss官网找到openGauss极简版的软件包 openGauss软件 | openGauss下载 | openGauss软件包 | openGauss社区 右键立即下载&#xff0…

RabbitMQ 的集群

大家好&#xff0c;我是锋哥。今天分享关于【RabbitMQ 的集群】面试题&#xff1f;希望对大家有帮助&#xff1b; RabbitMQ 的集群 RabbitMQ 是一种流行的开源消息代理&#xff0c;广泛用于构建分布式系统中的消息队列。随着应用程序规模的扩大&#xff0c;单一的 RabbitMQ 实…

[ 应急响应基础篇 ] Windows 写入任务计划程序详解--任务计划程序窗口写入 命令行写入

&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 _PowerShell &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 &#x1f389;点赞➕评论➕收藏 养成习…

使用Git LFS管理大型文件

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 使用Git LFS管理大型文件 引言 Git LFS 简介 安装 Git LFS 安装 Git 安装 Git LFS 配置 Git LFS 初始化 Git 仓库 指定需要使用…

【Clikhouse 探秘】ClickHouse 物化视图:加速大数据分析的新利器

&#x1f449;博主介绍&#xff1a; 博主从事应用安全和大数据领域&#xff0c;有8年研发经验&#xff0c;5年面试官经验&#xff0c;Java技术专家&#xff0c;WEB架构师&#xff0c;阿里云专家博主&#xff0c;华为云云享专家&#xff0c;51CTO 专家博主 ⛪️ 个人社区&#x…

【Android】Java开发语言规范

Java语言规范 命名风格 **类名&#xff1a;**使用 UpperCamelCase 风格&#xff0c;必须遵从驼峰形式&#xff0c;但以下情形例外&#xff1a;DO / BO / DTO / VO / AO&#xff0c;所有单词的首字母大写**方法名、参数名、成员变量、局部变量&#xff1a;**统一使用 lowerCam…

一些硬件知识【2024/11/2】

当需要提供功率型的输出信号的时候&#xff0c;可以在信号发生器外接功率放大器&#xff0c;这样可以提高输出功率 信号的调幅&#xff08;AM&#xff09;、调频&#xff08;FM&#xff09;与调相&#xff08;PM&#xff09;&#xff1a; 调制信号&#xff1a;控制高频振荡的低…

WPF自定义日历控件Calendar 的方法

推荐下载地址 https://www.haolizi.net/example/view_2107.html <UserControl.Resources><local1:DayConverter x:Key"DayConverter"/><!--导入转换器--><Style x:Key"CalendarStyle1"TargetType"{x:Type Calendar}">&…

Qt第三课 ----------输入类的控件属性

作者前言 &#x1f382; ✨✨✨✨✨✨&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f382; ​&#x1f382; 作者介绍&#xff1a; &#x1f382;&#x1f382; &#x1f382; &#x1f389;&#x1f389;&#x1f389…

LabVIEW 离心泵机组故障诊断系统

开发了一套基于LabVIEW图形化编程语言设计的离心泵机组故障诊断系统。系统利用先进的数据采集技术和故障诊断方法&#xff0c;通过远程在线监测与分析&#xff0c;有效提升了离心泵的预测性维护能力&#xff0c;保证了石油化工生产的连续性和安全性。 项目背景及意义 离心泵作…

typescript的简介

简介 tsc npm install -g typescripttsc -v作用 检查类型和语法错误&#xff0c;提前纠错 ts的类型 如何穿件带有ts的vue工程 作用 常见类型 用法 编写一个ts文件 let username:string "John";let age:number 25;let isUpdated:boolean true;let data:any &q…