Flutter 状态管理 Provider 使用小结

背景

  2019 Google I/O 大会上重磅消息出了支持 flutter_web 之外,另一个便是弃用之前的状态管理 Provide,转而推荐相似的库 Provider;虽然只有一个字母之差使用方式差别却很大,自此,Provider 代替 Provide 成为官方推荐的状态管理方式之一。

  Provider也是借助了InheritedWidget,它允许在小部件树中传递数据,允许我们更加灵活地处理数据类型和数据。

为什么需要状态管理

  在进行项目的开发时,往往需要管理不同页面之间的数据共享,在页面功能复杂,状态可能会达到几十个甚至上百个,这时将难以清楚的维护这些数据状态。而且还会有多个页面共享同一个状态,例如当你进入一个文章点赞,退出到外部缩略展示的时候,外部也需要显示点赞数,这时候就需要同步这两个状态。

  Flutter 实际上在一开始就提供了一种状态管理方式 — StatefulWidget。然而发现它仅适合用于在单个 Widget 内部维护其状态。当需要使用跨组件的状态时,StatefulWidget 将不再是一个好的选择。在StatefulWidget中的 State 属于某一个特定的 Widget,在多个 Widget 之间进行通信的时候,虽然可以使用 callback 解决,但当嵌套足够深的话,很容易增大代码的耦合度。

  这时候,就迫切的需要一个架构来帮助理清这些关系,状态管理框架应运而生。

为什么选择Provider

  Provider 从名字上就很容易理解,它就是用于提供数据,无论是在单个页面还是在整个 app 都有它自己的解决方案,我们可以很方便的管理状态,并在合适的时机释放资源。可以说,Provider 的目标就是来替代StatefulWidget。

如何使用

添加依赖

170dd382915684b2.jpg

执行

flutter packages get

在需要使用的页面引入

import 'package:provider/provider.dart'

创建数据共享类

这里的数据共享类实际上就是我们的状态,它不仅储存了我们的数据模型,而且还包含了更改数据的方法,并暴露出它想要暴露出的数据。

import 'package:flutter/material.dart’;

class WeatherInfo with ChangeNotifier {
  String _tempType = “celcius”;
  int _temperatureVal = 25;

  String get temperatureType => _tempType;

  set temperatureType(String value) {
    _tempType = value;
    notifyListeners();
  }

  int get temperatureVal => _temperatureVal;

  set temperatureVal(int value) {
    _temperatureVal = value;
    notifyListeners();
  }
}

这个类意图很清晰,数据就是一个 int 类型的 _temperatureVal、string 类型的_tempType,下划线代表私有。通过 get value 把 _temperatureVal 、_tempType值暴露出来。并提供 set 方法用于更改数据。

这里使用了 mixin 混入了 ChangeNotifier,这个类能够帮助我们自动管理所有听众。

当调用 notifyListeners() 时,它会通知所有听众进行刷新。

提供数据共享

在 build 方法提供初始化数据

class WeatherInfoPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    LogUtil.v("WeatherInfoPage build”);
    return ChangeNotifierProvider<WeatherInfo>(
      builder: (context) => WeatherInfo(),
      child: Scaffold(
        appBar: AppBar(
          title: Text("Provider demo${Random().nextInt(100)}”),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Padding(
                padding: EdgeInsets.all(8.0),
                child: Text(
                    "我在ChangeNotifierProvider中,看我变不变${Random().nextInt(100)}”),
              ),
              MyHeader(),
              MyHeaderListenerFalse(),
              MyContent(),
              Padding(
                padding: EdgeInsets.all(8.0),
                child: RaisedButton(
                  onPressed: () {
                    Navigator.push(context,
                        MaterialPageRoute(builder: (context) {
                      return WeatherInfoPage2();
                    }));
                  },
                  child: Text("consumer 例子”),
                ),
              ),
            ],
          ),
        ),
        floatingActionButton: MyFloatingActionButton(),
      ),
    );
   
  }
}

ChangeNotifierProvider不仅能够提供数据供子孙节点使用,还可以在数据改变的时候通知所有听众刷新。(通过之前我们说过的 notifyListeners)

此处的 <T>范型可省略。但是建议大家还是进行声明,这会使你的应用更加健壮。

ChangeNotifierProvider 绑定数据有两种方式:

ChangeNotifierProvider({Key key, @required ValueBuilder builder, Widget child })
ChangeNotifierProvider.value({Key key, @required T notifier, Widget child })

使用基本差不多,区别在于Provider()提供dispose参数,可以传递一个方法销毁的时候被调用,方便StatelessWidget释放资源,并且通过构造器绑定数据并进行监听,当从 Widget Tree 中删除时, 会自动调用dispose 进行销毁;

获取状态

这里有两个子控件 MyHeader,MyContent获取状态,MyFloatingActionButton 来更改状态。

  1. Provider.of(context)
class MyHeader extends StatelessWidget {
  Color decideColor(WeatherInfo info) {
    return "celcius" == info.temperatureType ? Colors.green : Colors.deepOrange;
  }

  @override
  Widget build(BuildContext context) {
    LogUtil.v("WeatherInfoPage MyHeader build”);

    WeatherInfo weatherInfo = Provider.of<WeatherInfo>(context);

    return Padding(
      padding: EdgeInsets.all(8.0),
      child: Text(
        "我是类中的temperatureType=${weatherInfo.temperatureType}”,
        style: TextStyle(
          color: decideColor(weatherInfo),
          fontSize: 20,
        ),
      ),
    );
  }
}

获取顶层数据最简单的方法就是 Provider.of<T>(context);这里的范型<T>指定了获取 MyHeader向上寻找最近的储存了 T 的祖先节点的数据。通过这个方法获取了顶层的 WeatherInfo。并在 Text 组件中进行使用。

  1. Consumer
class MyContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    LogUtil.v("WeatherInfoPage MyContent build”);

    return Consumer<WeatherInfo>(
      builder: (BuildContext context, WeatherInfo weatherInfo, Widget child) {
        return Padding(
          padding: EdgeInsets.all(8.0),
          child: Text("我是类中的temperatureVal=${weatherInfo.temperatureVal}”),
        );
      },
    );
  }
}


这里获取顶层数据使用Consumer 获取数据。这里的范型 <T>指定了获取 MyContent向上寻找最近的储存了 T 的祖先节点的数据。

Consumer 使用了 Builder模式,收到更新通知就会通过 builder 重新构建。Consumer<T>代表了它要获取哪一个祖先中的 Model。
Consumer 的 builder 实际上就是一个 Function,它接收三个参数 (BuildContext context, T model, Widget child)。

  • context: context 就是 build 方法传进来的 BuildContext 。
  • T:T也很简单,就是获取到的最近一个祖先节点中的数据模型。
  • child:它用来构建那些与 Model 无关的部分,在多次运行 builder 中,child 不会进行重建。

然后它会返回一个通过这三个参数映射的 Widget 用于构建自身。
这里讲的 Consumer 获取的只有一个范型<T>数据,如果获取两个数据,则可以使用Consumer2<A,B>。使用方式基本上和 Consumer<T>一致,只不过范型改为了两个,并且 builder 方法也变成了 Function(BuildContext context, A value, B value2, Widget child)。

从源码里面可以看到,作者只为提供到了 Consumer6。如果还要要求更多就只有合并数据源了。

  1. 两者区别

先看下 Consumer 内部实现

@override
Widget build(BuildContext context) {
  return builder(
    context,
    Provider.of<T>(context),
    child,
  );
}

可以发现,Consumer就是通过Provider.of<T>(context)来实现的。但是从实现来讲Provider.of<T>(context)比 Consumer简单好用太多,为什么要搞得那么复杂呢?

实际上 Consumer非常有用,它的经典之处在于能够在复杂项目中,极大地缩小你的控件刷新范围。Provider.of<T>(context)将会把调用了该方法的context作为听众,并在notifyListeners的时候通知其刷新。

举个例子来说,我们在 MyHeader使用了Provider.of<T>(context)来获取数据,MyContent 则没有。

  • 在 MyHeader 中的 build 方法中添加一个LogUtil.v("WeatherInfoPage MyHeader build”);
  • 然后在 MyContent 中的 build 方法中添加一个 LogUtil.v("WeatherInfoPage MyContent build”);
  • 点击MyFloatingActionButton浮动按钮,那么会在控制台看到这句输出WeatherInfoPage MyHeader build。

这证明了 Provider.of<T>(context)会导致调用的 context 页面范围的刷新。

那么MyContent页面刷新没有呢? 刷新了,但是只刷新了 Consumer的部分。

假如在应用的页面级别的Widget中,使用了Provider.of<T>(context)。会导致什么后果已经显而易见了,每当其状态改变的时候,都会重新刷新整个页面。虽然有Flutter的自动优化算法给你撑腰,但肯定无法获得最好的性能。

所以在这里建议各位尽量使用 Consumer而不是 Provider.of<T>(context)获取顶层数据。

多个 Provider

从源码中可以看出,作者提供了 MultiProvider 供提供多种状态使用。

MultiProvider(
    providers:[
        AProvider,
        BProvider,
        CProvider,
    ],
    child: child,
)

MultiProvider 实际上就是通过每一个 provider 都实现了的 cloneWithChild 方法,用循环把自己一层一层包裹起来。

@override
Widget build(BuildContext context) {
  var tree = child;
  for (final provider in providers.reversed) {
    tree = provider.cloneWithChild(tree);
  }
  return tree;
}

等价于

AProvider(
    child: BProvider(
        child: CProvider(
            child: child,
        ),
    ),
)

总结

  • 使用Provider.of(context),简单易用,但是要数据发生变化时,会进行页面级别rebuild,相当于statefulWidget,此方法将从BuildContext关联的小部件树中查找,它将返回找到的最近的类型变量T
    Provider.of<T>( BuildContext context,
    {bool listen = true}//listen:默认true监听状态变化,false为不监听状态改变
    )

  • 也可以使用Consumer组件获取,Consumer可用在没有context的地方,还可以优化性能

Consumer<T>({
@required this.builder,//这边写布局
this.child,//可以控制刷新性能优化,当数据数据发生改变,不会重新build,
})
  • 状态管理包裹在MaterialApp()外面作用域是全局,其他作用域在本页面或本页的子Widget中
  • MultiProvider()可以提供多个状态
  • ChangeNotifierProvider.value()与ChangeNotifierProvider()区别是ChangeNotifierProvider()通过构造器绑定数据并进行监听,当从 Widget Tree 中删除时, 会自动调用dispose 进行销毁释放资源,在需要使用多个状态值时可以使用ChangeNotifierProvider
  • 无论使用那种 .value 方式,均建议在 dispose 中进行 listener 的关闭;
@override
void dispose() {
    super.dispose();
    _homepageNotifier.dispose();
}

最后

  如果在使用过程遇到问题,欢迎下方留言交流。

学习资料

请大家不吝点赞!因为您的点赞是对我最大的鼓励,谢谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容