概述
谷歌提供了官方的状态管理组件Provider,可以很方便地进行 全局/局部状态管理和参数传递,进行代码的MVVM分层管理。其实使用InheritedWiget也能实现类似效果。本篇将讲述Provider的基本用法,以及如何完全从0手写一个Provider,以加深对Provider原理的理解。
本文分为3个步骤:
- 引入
provider包,模拟常见的状态管理业务场景 - 去掉Provider包,使用
InheritedWidget实现完全类似的效果 - 对
InheritedWidget实现效果的代码进行泛型封装,实现与 provider完全一样的写法。
本文的主要代码,在 https://github.com/18598925736/FlutterProviderDemo.git 中,3个步骤分别对应Demo1,Demo2, Demo3 这3个目录。注意,Demo1要引入provider。由于只是单纯的dart代码实验,不涉及到原生,就不传完整工程了。建议新建Flutter工程,拷贝lib目录进行测试。
引入Provider
请clone demo工程,并拷贝Demo1运行。
先引入provider,用最典型的 provider使用方法来进行 状态管理。
这是一个很简单的案例,如图所示,中间一个FlutterLogo组件,下方一个控制面板,对FlutterLogo的阴影,大小和旋转角度进行控制。
关键的类有以下几个:
| 文件名 | 作用 |
|---|---|
| main.dart | 主要布局 |
| foo_model.dart | ViewModel 控制组件状态 |
| foo_widget.dart | 带FlutterLogo的组件 |
| foo_controller_widget.dart | FlutterLogo的控制面板组件 |
FooModel 实际上对 Foo(FlutterLogo) 和 FooControlPanel (控制面板) 都 进行了状态管理,只不过控制面板是主动控制,FlutterLogo 是被动展示,实际上他们的状态都受到了FooModel的制约。
注意观察其中几个要素:
状态提升
main.dart中,MaterialApp被ChangeNotifierProvider包裹,并且实现了一个 create参数,参数值为一个函数,函数的返回值为new 的 一个 FooModel对象。
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (BuildContext context) => FooModel(),
child: MaterialApp(
title: 'Provider Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const MyHomePage(title: 'Provider Demo')),
);
}
自定义ViewModel
FooModel本身是一个 ChangeNotifier 的实现类,其中包含了3个FlutterLogo需要用到的属性, 以及他们的get set方法,get方法直接返回字段本身,set方法在改变属性之后还调用了 notifyListeners:
class FooModel extends ChangeNotifier {
double _size = 20;
double _angle = 20;
bool _hasElevation = true;
double get size => _size;
set size(size) {
_size = size;
notifyListeners();
}
double get angle => _angle;
set angle(angle) {
_angle = angle;
notifyListeners();
}
bool get hasElevation => _hasElevation;
set hasElevation(hasElevation) {
_hasElevation = hasElevation;
notifyListeners();
}
}
状态监听
Foo 和 FooControlPanel中,都有context.watch方法,获得 FooModel对象,并且将他们用于UI构建。
class Foo extends StatelessWidget {
const Foo({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
FooModel model = context.watch<FooModel>();
return Card(
color: Colors.amber,
elevation: model.hasElevation ? 10 : 0,
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Transform.rotate(
angle: model.angle, child: FlutterLogo(size: model.size)),
));
}
}
// FooControlPanel太长就不贴上来了
纵观3个步骤,我们发现要对一个组件进行状态管理,需要3个步骤,
第一,定义ViewModel,就比如上面的FooModel,Flutter中的ViewModel继承 ChangeNotifier 就行了,写法参照上面。
第二,在要使用FooModel的组件的上级节点中,插入 ChangeNotifierProvider ,并实现create,传入FooModel的构建函数。
第三,在 要使用ViewModel的组件的build函数中,通过 context.watch<FooModel> 获得 viewModel对象,并且在 return的内容中使用model内的状态参数(angle角度,size大小等)。
再从表面上来看,所谓的状态管理就是,在 父Widget中提前预备好ViewModel,子组件获取ViewModel对象并且使用其中的参数。
那么,父容器是如何预备ViewModel的? 子组件又是如何获取viewModel的呢?
接下来开始解密。
使用 InheritedWidget 模拟Provider的效果
请 拷贝Demo2并运行,效果完全一样。注意移除 yaml中的provider依赖。
开始观察代码:
-
原本 main.dart中 ChangeNotifierProvider 现在替换成了 FooModelProvider,并提供一个fooModel 参数,返回一个 FooModel对象。
-
FooModel 完全没变化
-
原本 context.watch 的地方,现在换成了 FooModelProvider.of(context) ,同样是获取 的 FooModel。并且,特别注意:build函数中,现在多套了一层 AnimatedBuilder,并使用了 model对象作为 animation的值。 原本的 return Card现在被包裹在了 build参数值中。
class Foo extends StatelessWidget { const Foo({Key? key}) : super(key: key); @override Widget build(BuildContext context) { FooModel model = FooModelProvider.of(context); return AnimatedBuilder( animation: model, builder: (BuildContext context, Widget? child) => Card( color: Colors.amber, elevation: model.hasElevation ? 10 : 0, child: Padding( padding: const EdgeInsets.all(10.0), child: Transform.rotate( angle: model.angle, child: FlutterLogo(size: model.size)), )), ); } }
最大的改变在于:
- 新增了 FooModelProvider 这个类
它的作用,就相当于 原本 provider依赖中的 ChangeNotifierProvider ,属于将子组件的状态提升到父容器中的过程。代码如下:
class FooModelProvider extends InheritedWidget {
final FooModel fooModel;
static FooModel of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<FooModelProvider>()!
.fooModel;
}
const FooModelProvider(
{required Widget child, required this.fooModel, Key? key})
: super(child: child, key: key);
@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) {
return true;
}
}
- 它
extends了InheritedWidget,继承式组件,将 子组件要用到的ViewModel作为它的成员属性。并且要求重写updateShouldNotify。 - 提供了一个
of函数, 方便从外界获取FooModel对象。
这是InheritedWidget的典型用法,作为继承式组件,将状态向上提升,并且子组件要监听状态的变化。
InheritedWidget 的用法参考相关链接:
注意:此处的 updateShouldNotify 不会执行,这是因为 当且仅当 旧的 InheritedWidget 被替换为新的 InheritedWidget 的时候,才会去判断是否需要通知到 Model的监听者。如果压根就没发生 InheritedWidget 的替换动作,那么就不会有这个函数的执行。
bool updateShouldNotify(covariant InheritedWidget oldWidget) {
return true;
}
而 FooModelProvider 要被替换,至少必须是: MyApp 的build函数被重新调用。而 这个App是一个无状态的StatelessWidget,在app启动之后,它的build函数将永远不会再次执行。 子组件之所以还能响应 viewModel的变化,实际上还是因为 增加了 状态的监听者 AnimatedBuilder 。
完全复刻Provider
主要差距
InheritedWidget 虽然能实现与 Provider类似的效果,但是在易用性方面还是有一定差距。
-
使用provider时,子组件中 不需要写 AnimatedBuilder 去监听viewModel, 直接return想要的UI即可
-
子组件中 获得 FooModel,不再需要 FooModelProvider 这个类,而是直接
context.watch<FooModel>()
class Foo extends StatelessWidget {
const Foo({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
FooModel? model = context.watch<FooModel>();
if (model == null) {
return const SizedBox();
}
debugPrint("foo build");
return Card( // 直接返回UI,不必增加 AnimatedBuilder监听ViewModel
color: Colors.amber,
elevation: model.hasElevation ? 10 : 0,
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Transform.rotate(
angle: model.angle, child: FlutterLogo(size: model.size)),
));
}
}
- 使用provider时可以通过泛型 注定 ViewModel的类型,不再定死为FooModel :
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifyProvider<FooModel>( // 泛型指定vm类型
create: () => FooModel(),
child: MaterialApp(
title: 'Provider Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const MyHomePage(title: 'Provider Demo')),
);
}
}
逐渐复刻
请 拷贝Demo3的lib并运行app,注意删掉 yaml文件中的provider依赖并同步。
1. 让子组件更加简洁
上一章提到了 updateShouldNotify 不会被执行的原因,是因为InheritedWidget没有被重建。而上面的UI之所以还能跟随VM变化而变化,则是因为我们有AnimatedBuilder监听VM。要复刻Provider的用法,这个监听要去掉。并且让 updateShouldNotify 执行(为了方便,返回值固定为true,只要有变化,就通知),让InheritedWidget 之下的 UI都能响应VM的变化。而让updateShouldNotify 执行,最直接的办法,就是让 InheritedWidget所在的Widget 进行build函数的重新调用。
具体做法是:
- 去掉子组件中 AnimatedBuilder
- 使用 一个StatefulWidget 将InheritedWidget进行包裹,并且 InheritedWidget 外层要 进行 AnimatedBuilder监听 VM的变化
进行了这两步,子组件的写法更加简洁了。差距1 被抹平。
2. 泛型封装
ChangeNotifyProvider 类中,本来定死的 FooModel 这个VM,现在通过泛型类的传参,要求在创建 ChangeNotifyProvider 对象时,指定一个 Listenable 的子类作为泛型实参。
/// 定义
class ChangeNotifyProvider<T extends Listenable> extends StatefulWidget {
/// ...
}
/// 使用
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifyProvider<FooModel>(
create: () => FooModel(),
child: MaterialApp(
title: 'Provider Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const MyHomePage(title: 'Provider Demo')),
);
}
}
并且泛型参数T,要逐级传递到 InheritedWidget 的子类中去。
最终 我们提炼出的 ChangeNotifyProvider 如下:
foo_model_provider.dart
import 'package:flutter/cupertino.dart';
class ChangeNotifyProvider<T extends Listenable> extends StatefulWidget {
final T Function() create;
final Widget child;
const ChangeNotifyProvider({
Key? key,
required this.create,
required this.child,
}) : super(key: key);
@override
State<ChangeNotifyProvider> createState() {
return _FooModelProviderState<T>();
}
static T? of<T>(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<_InheritedWidget<T>>()
?.model;
}
}
class _FooModelProviderState<T extends Listenable>
extends State<ChangeNotifyProvider<T>> {
late T model;
@override
void initState() {
super.initState();
model = widget.create();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: model,
builder: (BuildContext context, Widget? child) => _InheritedWidget(
model: model,
child: widget.child,
),
);
}
}
class _InheritedWidget<T> extends InheritedWidget {
final T model;
const _InheritedWidget({
required Widget child,
required this.model,
Key? key,
}) : super(
child: child,
key: key,
);
///
/// 在 FooModelProvider 发生重建的前提下,是否通知到 使用到当前InheritedWidget实现类的 子Widget
///
/// 注意,这里有一个大前提,就是 它自身要发生重建,由新的替换旧的,所以这里有一个入参 oldWidget
///
@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) {
debugPrint(
"updateShouldNotify 执行 ${oldWidget.runtimeType} | ${oldWidget.hashCode} -> $hashCode");
return true;
}
}
extension ContextProviderExt on BuildContext {
T? watch<T extends Listenable>() => ChangeNotifyProvider.of<T>(this);
}
差距3被抹平。
3. 优化ViewModel的获取方式
context.watch的写法,其实是利用了dart的扩展函数的语法,它允许我们给一个系统类(或者某些我们无法直接插手的类)增加新的行为。
具体做法为:
extension ContextProviderExt on BuildContext {
T? watch<T extends Listenable>() => ChangeNotifyProvider.of<T>(this);
}
关键字 extension 它表示本函数为扩展函数, on 后面的 BuildContext为 被扩展的类, 中间的 ContextProviderExt 为本次定义的扩展名(如果扩展函数仅在本dart文件内部使用,则不需要加这个名字,如果需要在本dart文件外部使用,则必须加上这个 名字,取名字不重复即可)。
大括号中,则为 本次扩展出的新函数。 watch 为函数名,this 为 BuildContext本身,而 ChangeNotifyProvider.of<T>(this); 的实际执行代码为:ChangeNotifyProvider 类中的 静态方法:
static T? of<T>(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<_InheritedWidget<T>>()
?.model;
}
它的作用为,顺着自身的widget树结构向上查找,直到找到一个 类型为 _InheritedWidget<T> 的 InheritedWidget 为止。
到此,差距2 被抹平。
仔细观察Demo1和Demo3,就能发现,Demo3中只是多出了一个 foo_model_provider.dart 文件,其余代码完全相同。
思考
手写Provider,并达成类似的写法和UI操作效果,已达成。但是当我以为这就是 原provider包原理的时候,我发现事情并没有那么简单。
它并没有像我们这样 利用 StatefulWidget / State的build函数来导致InheritedWidget的 多次执行来使得 子组件响应viewModel的改变。而是继承了 ListenableProvider ,看着像是一个 可以监听某个ViewModel的Widget,当Vm发生变化时,child会发生重建。
原理上应该是差不多的,但是看了半天源码,本人表示,看不懂。。。但是从注释中,基本能作证我的判断:
/// Listens to a [ChangeNotifier], expose it to its descendants and rebuilds
/// dependents whenever [ChangeNotifier.notifyListeners] is called.
///
/// Depending on whether you want to **create** or **reuse** a [ChangeNotifier],
/// you will want to use different constructors.
翻译为: 监听一个 [ChangeNotifier],将其暴露给其子节点并在 [ChangeNotifier.notifyListeners] 被调用时重新构建依赖项。
所以说,本文基本上可以作为 Provider的原理向的解密文章,如有错误,欢迎指出。