Flutter状态管理:Provider4 入门教程(一)
背景
很久之前,在我们的QQ群里有位朋友一直想让我出个[Provider](https://github.com/rrousselGit/provider)
教程,但是我一直没有允诺。因为我觉得如果写入门级的教程,已经有官方文档了,已经有人写了,如果要深入一些呢,我又不会。但最近不太一样了,因为要水文了。
状态管理
说到Flutter
,我们很难回避状态管理。对于React
的开发者来说,状态管理并不陌生;但对于我们这种纯原生开发者来说,还是有些陌生的。
Flutter
是声明式的,这意味着Flutter
是通过更新UI来反映当前app的状态:

Flutter
中,如果我们想更新我们的控件,最基本的方式应该是setState()
了。如果说我们一个页面里的组件不多,直接使用setState()
并没有什么问题,但是实际工作中,我们的页面布局还是足够复杂的。
一种情况是我们在一个页面中:

Widget
都写到一个类里,这个类一定会是个200多斤的胖子,而且很容易陷入{{{{}}}}
旋涡。这时我们会想到Widget
进行拆分,但这个时候如果仅仅依靠setState()
,你会发现这将十分痛苦,因为setState()
的作用域仅限于当作Widget
,也就是说如果你仅仅在最底层的Widget
里调用setState
并不会更新顶层的Widget
,这就意味你要通过回调实现,而且在这个过程中你会发现一些Widget
类里的变量又必须是不可变的,这又会引起其他的麻烦事,不谈。
而通常来说,实际开发中,很可能有跨页面共享数据的可能:

上图为大家展示了一个物车功能,当用户点击Add
时,会将商品添加到购物车,点击购物车时,我们可以看到刚刚的商品。想想如果不使用状态管理,我们应该如何实现呢?
说了这么多,无非就是想说使用状态管理的更要性。简单来说就是如何方便快捷地在Widget
之间共享数据并将数据展示在页面上。
Flutter
的状态管理方式包括但不限于Provider
,Bloc
,Redux
以及Fish-Redux
。
Bloc
准确地来说是一种理念,也是我使用的第一个状态管理,现在也有对应的实现库,一般来说是基于响应式编程的。Redux
对于React
开发者来说并不陌生,毕竟Flutter
这块也是借鉴了React
。Fish-Redux
脱胎于Redux
,阿里出品,总体来说比较复杂,适合中大型项目。现在社会也有生成Fish-Redux
模板代码的工具Provider
是Google
推荐的状态管理,也是我使用的第二种状态管理,相对来说比较简单省心。
接下来,我将简单地介绍一下Provider
的使用。
初识Provider
Provider
其实是对InheritedWidget的封装。相比于直接使用InheritedWidget
,使用Provider
有很多好处,比如说简化资源的分配与处置,支持懒加载等等。
Provider
为我们提供了一些不同类型的Provider
。要查看所有类型的provider
可以点击这里
name | description |
---|---|
Provider | 最基础的provider。它携带一个值并将这个值暴露,无论这个值是什么。 |
ListenableProvider | 为Listenable 对象而创建的provider 。ListenableProvider 会监听对象的变化,只要ListenableProvider 的listner被调用,ListenableProvider 就会重新构建依赖于该provider的控件。 |
ChangeNotifierProvider | ChangeNotifierProvider 是一种特殊的ListenableProvider ,它基于ChangeNotifier ,并且在有需要的时候,它会自动调用ChangeNotifier.dispose 。 |
ValueListenableProvider | 监听ValueListenable 并只会暴露ValueListenable.value . |
StreamProvider | 监听一个Stream 并且对外暴露最新提交的值。 |
FutureProvider | 携带一个 Future ,当Future 完成时,它会更新依赖于它的控件。 |
鉴于本人才疏学浅,本文并不会逐一讲述如何使用各种Provider
,所以本文挑选了我用的最多的ChangeNotifierProvider
来讲解,希望可以抛砖引玉。
创建一个Proivder
一般来说创建Provider
有两种方式:
- 默认构造方法
.value
构造方法
当我们要新创建一个对象,我们要使用默认构造方法而不是使用.value
构造方法,因为如果我们通过.value
创建一个对象可能会引起内存泄漏或产生一些意想不到的问题。
这里简单解释一下为什么不能使用value
创建一个对象,英文好的可以看StackOverflow原文。因为Flutter
中的build
方法应该是纯净无副作用的,很多外部因素会触发rebuild
,比如说:
- 路由的pop/push
- 屏幕大小重新调整,通常来说是因为键盘变化或者屏幕方向变化
- 父控件重绘子控件
- 依赖于
InheritedWidget
的控件(Class.of(context) 部分)发生了变化
所以说,使用.value
创建对象的问题在于会使得build
变得不纯粹或者说具有副作用,会使来自外部的build
调用变得很麻烦。
这个问题到此为止,喜欢研究的朋友可以自行探索。
- 要 使用
Provider
的create
中创建对象。
Provider(
create: (_) => MyModel(),
child: ...
)
- 不要 使用
Provider.value
创建对象。
ChangeNotifierProvider.value(
value: MyModel(),
child: ...
)
- 不要 从可以随时间变化而变化的变量中创建对象。 因为在这种情况中,即使引用的变量发生了变化,我们创建的对象也不会被更新。
int count;
Provider(
create: (_) => MyModel(count),
child: ...
)
如果想将随时间变化而变化的变量传递到我们的对象中,可以考虑使用ProxyProvider
:
int count;
ProxyProvider0(
update: (_, __) => MyModel(count),
child: ...
)
笔记:当使用
Provider
的create/update
回调时,我们要注意的是,默认情况下,create/update
的调用是懒式调用的。这就意味着,只有我们Provider
中的数据至少被请求一次,create/update
才会被调用。如果我们想做一些预处理,我们可以使用lazy
参数来禁止这一特性:
MyProvider(
create: (_) => Something(),
lazy: false,
)
读取Provider中的数据
最简单的读取数据的方式是使用BuildContext
的扩展方法:
- context.watch(), 该方法会使用对应的控件监听T的变化。
- context.read(), 该方法直接返回T,并不会监听的变化。
- context.select<T, R>(R cb(T value)), 该方法会使对应的控件只监听一小部分T的变,从名字上看我们就知道这是一个筛选器。
当然了我们也可以使用静态方法Provider.of<T>(context)
,它和watch/read
的行为很像,这也是在上面扩展方法出现之前,我们获取数据的方式。
这些方法会从控件树中进行查找,并且是从与传递过来的BuildContext
相关的控件开始,最终返会找到并返回与类型T的最近变量(如果未找到,则抛出)。
值得注意的是,这个操作的复杂度为O(1)。 实际上,这并不会在控件树中游走。
说到现在,无非还是对文档的翻译,现在让我们走码上任吧~~~
Show me the code
故事还是要从Flutter
的计数器说起,因为新创建的Flutter
项目模板就是这个计数器了,现在我们要用ChangeNotifierProvider
来简单改造一下这个项目。
- 将
MyHomePage
由StatefulWidget
改成StatelessWidget
。 - 使用
ChangeNotifierProvider
来更新页面
第一个版本
首先,我们要创建一个ChangeNotifier
:
class MyChangeNotifier extends ChangeNotifier {
int _counter = 0;
int get counter => _counter;
incrementCounter() {
_counter++;
notifyListeners();//要更新UI记得调用这个方法
}
}
当前我们点击FloatingActionButton
时会调用MyChangeNotifier
的incrementCounter
方法,要注意的是当我们处理完业务时,如果需要更新UI需要调用notifyListeners
来通知Provider
更新UI。
接下来我们实现我们的UI。
首先,我们要创建MyHomePage
, UI布局直接使用的是example里的布局,不同的是我们使用的是StatelessWidget
。然后我们通过BuildContext
取出MyChangeNotifier
实例。要注意到,当我们点击FloatingActionButton
,我们并没有调用setState
(废话,StatelessWidget
也不能setState
),但我们的UI依然会被更新。代码如下:
class MyHomePage extends StatelessWidget {
final String title;
MyHomePage({Key key, this.title}) : super(key: key);
@override
Widget build(BuildContext context) {
MyChangeNotifier notifier =
Provider.of(context); //通过Provider.of(context)获取MyChangeNotifier
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${context.watch<MyChangeNotifier>().counter}',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: notifier.incrementCounter,//点击时我们期望输出点击次数
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
现在我们要用ChangeNotifierProvider
包裹MyHomePage
,这样可以保证在MyHomePage
中可以通过BuildContext
取到MyChangeNotifier
实例。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: ChangeNotifierProvider(
create: (_) => MyChangeNotifier(),
child: MyHomePage(title: 'Flutter Demo Home Page')),
);
}
}
到此为止,代码已经写完了,运行下,效果是不是和example一模一样呢?
当然了,我们可以直接在
MyChangeNotifier
中直接定义一个字段叫outputMessage
,然后直接在MyHomePage
中直接给Text
赋值。
Text(
context.watch<MyChangeNotifier>().outputMessage,
style: Theme.of(context).textTheme.headline4,
),
第二个版本
现在看来,我们已经学会了ChangeNotifierProvider
的基本用法,那么我们现在要对上面的代码简单改造一下。
- 将
ChangeNotifierProvider
移动到MyHomePage
。
很简单了,代码如下:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatelessWidget {
final String title;
final MyChangeNotifier notifier = MyChangeNotifier();
MyHomePage({Key key, this.title}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => notifier,
child: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${context.watch<MyChangeNotifier>().counter}',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: notifier.incrementCounter,//点击时我们期望输出点击次数
tooltip: 'Increment',
child: Icon(Icons.add),
),
),
);
}
}
当我们高高兴兴的运行上面的代码时却遇到了一些问题:

Consumer
。
Consumer
的使用
Consumer
本身没有魔法,也没有什么花里胡哨的实现。只不过是在一个新的控件中使用Provider.of
,然后将这个控件的build
方法委托给参数里的builder
。这个builder
会被调用多次。就是这么简单。
Consumer
的设计初衷有两个
- 当我们的
BuildContext
中不存在指定的Provider
时,Consumer
允许我们从Provider
中的获取数据。 - 通过提供更多细小的重绘达到性能的优化。
我们现在遇到的就是第一种情况,至于第二种情况,读者们可自行探讨。
所以,我们可以通过加一个Consumer
来解决上面的ProviderNotFoundException
问题:
class MyHomePage extends StatelessWidget {
final String title;
final MyChangeNotifier notifier = MyChangeNotifier();
MyHomePage({Key key, this.title}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => notifier,
child: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Consumer<MyChangeNotifier>(
builder: (_, localNotifier, __) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${localNotifier.counter}',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: notifier.incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
),
);
}
}
再次运行,是不是很完美?
暂时性总结
时间有限,本来想一口气写完,但是互联网时代不玩玩饥饿营销怎么好意思说自己混过互联网。。。
作为Provider
入门第一篇,本文还是十分简单的,毕竟只是改下了一下Flutter example。在接下来的文章中,我会介绍更多的Provider
用法与问题,也包含更复杂的demo。
未完待续。。。 期待不期待你说了算。
转载自:https://juejin.cn/post/6844904179014582286