Flutter系列十:Flutter状态管理之Provider的使用和架构分析

状态管理在Flutter中非常重要,但是它包含的内容又非常的广泛。

本文我们首先了解下什么是状态状态管理呢?然后我们来了解官方的状态管理库Provider的使用,最后分析下Provider背后的秘密。

Provider

状态管理

状态

Flutter是声明式编程,Widget定义的UI都是在build()函数中实现的,这个函数的功能就是将状态转换成UI

UI = f(state)

官方对状态的定义如下:

whatever data you need in order to rebuild your UI at any moment in time

翻译过来就是:状态就是任何时间任何场景下重构UI所需要的数据。

这里面至少可以看到两层含义:

  1. 状态就是数据;
  2. 状态的改变驱动了UI的改变。

状态的分类

我们可以把状态分为局部状态全局状态

局部状态就是Widget内部持有的状态,典型代表就是StatefuleWidget和它对应的State局部状态只会影响单个Widget的UI呈现。

当某个状态需要在多个Widget使用,或者在整个APP中使用,那它就是全局状态了。全局状态的典型代表就是InheritedWidget

我们在InheritedWidget的使用和源码分析这篇文章中已经详细介绍过了InheritedWidget的相关内容,当然我们也提到过它的一些不是太完善的地方。

状态管理库

我们这里所说的状态管理库主要是指对全局状态的一些处理库,除了InheritedWidget外,还有一些最近非常流行的库:

它目前是评分最高的库,适合大型的项目。但是它有一个缺点就是理解起来比较困难,编写代码方式也很独特,需要编写一些重复的代码模板。

它是Flutter官方团队共同维护的一个项目,由于有官方背景,所以不用担心后期的维护升级问题。

getx是目前上升趋势最快的一个库,使用非常简单,代码也很简介,功能很多。

当然还有其他一些库,譬如mobx,flutter_redux等,当然你很大可能也不会用到。

我们将会对Providergetx这两个库的使用和源码进行介绍。

Provider的使用

和介绍InheritedWidget时使用的案例类似,本文讲解Provider的时候使用是一个简单的计数器案例:有一个number全局状态,有三个Widget会使用到它,点击FloatingActionButton可以将number的值加1。效果如下:

Demo

当然复杂的多界面逻辑的实现方法使用的方法是一样的。譬如实现下面的功能:


官方的示例

基本使用

使用前得先引入库:

dependencies:
  provider: ^5.0.0

接下来我们分三步来了解它的使用:

  1. number封装到ChangeNotifier中,创建需要共享的状态
class NumberModel extends ChangeNotifier {
  int _number = 0;

  int get number => _number;

  set number(int value) {
    _number = value;
    notifyListeners();
  }
}

ChangeNotifierFlutter Framework的基础类,不是Provider库中的类。ChangeNotifier继承自Listenable,也就是ChangeNotifier可以通知观察者值的改变(实现了观察者模式)。

NumberModel有一个_number状态,然后提供了获取的方法get和设置set的方法。

  1. 在应用程序的顶层添加ChangeNotifierProvider
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (ctx) => NumberModel(),
      child: MyApp(),
    ),
  );
}

将应用的顶层设置为ChangeNotifierProvider, 然后将MyApp()变为它的子Widget

ChangeNotifierProvidercreate函数需要返回ChangeNotifier

  1. 其它Widget使用共享的状态

有四个地方需要使用到共享的状态,三个显示文字的Text WidgetFloatingActionButton

  • Provider.of
class NumberWidget1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 获取NumberModel的number
    int number = Provider.of<NumberModel>(context).number;
    return Container(
      child: Text(
        "点击次数: $number",
        style: TextStyle(fontSize: 30),
      ),
    );
  }
}

我们将Text Widget封装成了NumberWidget1, 通过int number = Provider.of<NumberModel>(context).number;获取到NumberModelnumber值,然后就可以显示了。

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 1 获取NumberModel
    NumberModel model = Provider.of<NumberModel>(context);

    return Scaffold(
        appBar: AppBar(
          title: Text("Provider"),
        ),
        body: Center(
          child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            // 2 修改number值
            model.number++;
          },
          child: Icon(Icons.add),
        ));
  }
}

FloatingActionButton也需要通过Provider.of<NumberModel>(context)方法先拿到NumberModel,然后调用set方法改变number的值。

全部代码:

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (ctx) => NumberModel(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
          primarySwatch: Colors.blue, splashColor: Colors.transparent),
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    
    NumberModel model = Provider.of<NumberModel>(context);

    return Scaffold(
        appBar: AppBar(
          title: Text("Provider"),
        ),
        body: Center(
          child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            model.number++;
          },
          child: Icon(Icons.add),
        ));
  }
}

class NumberWidget1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    int number = Provider.of<NumberModel>(context).number;
    return Container(
      child: Text(
        "点击次数: $number",
        style: TextStyle(fontSize: 30),
      ),
    );
  }
}

class NumberModel extends ChangeNotifier {
  int _number = 0;

  int get number => _number;

  set number(int value) {
    _number = value;
    notifyListeners();
  }
}
  • Consumer

问题:Provider.of有一个问题,就是当状态值发生变化后,Provider.of所在的Widget整个build方法都会重新构建。

上面的例子中,FloatingActionButton会引起Scaffold的重构,所以对性能的影响是最大的。

Consumer<NumberModel>(
    builder: (context, value, child) {
        return FloatingActionButton(
            onPressed: () {
                value.number++;
            },
        child: Icon(Icons.add),
        );
    },
)

我们将FloatingActionButtonConsumer包裹,builder中的value参数就是我们需要的NumberModel了。

这里我们可以进一步优化一下,对child进行复用。

Consumer<NumberModel>(
    builder: (context, value, child) {
        return FloatingActionButton(
            onPressed: () {
                value.number++;
            },
            child: child,
        );
        },
    child: Icon(Icons.add),
));

我们将child传入Consumer的构造函数就能实现复用了。

child复用的逻辑我们在前一篇关于动画源码的文章中有解释,如果需要可以回头参阅。

差异部分的代码如下:

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Provider"),
        ),
        body: Center(
          child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
        ),
        floatingActionButton: Consumer<NumberModel>(
          builder: (context, value, child) {
            return FloatingActionButton(
              onPressed: () {
                value.number++;
              },
              child: child,
            );
          },
          child: Icon(Icons.add),
        ));
  }
}
  • Consumer

问题:Consumer总归还是需要重构的,其实我们使用FloatingActionButton的时候只是用到了NumberModel的设置方法,根本没有用到它的_number属性,所以即使_number改变了,我们也是可以不需要重构的。

如果不需要重构,我们可以使用Selector

Selector<NumberModel, NumberModel>(
    selector: (ctx, numberModel) => numberModel,
    shouldRebuild: (previous, next) => false,
    builder: (context, value, child) {
        return FloatingActionButton(
            onPressed: () {
                value.number++;
            },
        child: child,
        );
    },
    child: Icon(Icons.add),
)

代码解释:

  1. Selector的泛型中有两个参数类型,第一个是原始类型,第二个是转换后的类型,也就是说Selector多了一个对数据进行转换的功能;
  2. selector是进行数据类型转换的函数;
  3. shouldRebuild是确实是否需要重构,我们明显是不需要的,所以传false;
  4. builderConsumer的功能就是类似的了。

差异部分的代码如下:

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Provider"),
        ),
        body: Center(
          child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
        ),
        floatingActionButton: Selector<NumberModel, NumberModel>(
          selector: (ctx, numberModel) => numberModel,
          shouldRebuild: (previous, next) => false,
          builder: (context, value, child) {
            return FloatingActionButton(
              onPressed: () {
                value.number++;
              },
              child: child,
            );
          },
          child: Icon(Icons.add),
        ));
  }
}

多个状态的使用

有时候某个Widget可能需要使用多个状态,我们接下来就介绍这种情况的使用方法。

  1. 创建多个需要共享的状态
class RandomNumberModel extends ChangeNotifier {
  int _randomNumber = Random().nextInt(100);

  int get randomNumber => _randomNumber;

  void resetRandomNumber() {
    _randomNumber = Random().nextInt(100);
    notifyListeners();
  }
}

我们再创建一个RandomNumberModel,里面有一个随机的数值_randomNumber, 并且设置获取方法get和设置方法resetRandomNumber

  1. 将应用程序的顶层改为MultiProvider
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider<NumberModel>(
          create: (ctx) => NumberModel(),
        ),
        ChangeNotifierProvider<RandomNumberModel>(
          create: (ctx) => RandomNumberModel(),
        ),
      ],
      child: MyApp(),
    ),
  );
}

MultiProviderproviders放置的是共享的多个Provider

  1. 其它Widget使用共享的状态
  • Provider.of
class NumberWidget1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 读取
    int number = Provider.of<NumberModel>(context).number;
    // 读取
    int randomNumber = Provider.of<RandomNumberModel>(context).randomNumber;
    return Container(
      // 使用
      child: Text(
        "点击次数: $number 随机数: $randomNumber",
        style: TextStyle(fontSize: 30),
      ),
    );
  }
}

我们可以通过Provider.of分别取到NumberModelRandomNumberModel,然后读取到相应的值。

  • Consumer2
class NumberWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Consumer2<NumberModel, RandomNumberModel>(
        builder: (context, value, value2, child) {
          return Text("点击次数: ${value.number}  随机数: ${value2.randomNumber}",
              style: TextStyle(fontSize: 30));
        },
      ),
    );
  }
}

Consumer2中两个泛型代表使用的哪两个数据,build方法中的value就是NumberModel,value2就是RandomNumberModel,然后读取到相应的值。

  • Selector2
class NumberWidget3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Selector2<NumberModel, RandomNumberModel, Tuple2<int, int>>(
        selector: (ctx, value1, value2) => Tuple2(value1.number, value2.randomNumber),
        builder: (context, value, child) {
          return Text("点击次数: ${value.item1}  随机数: ${value.item2}",
              style: TextStyle(fontSize: 30));
        },
        shouldRebuild: (previous, next) => previous != next,
      )
    );
  }
}
  1. Selector2有三个泛型参数:NumberModelRandomNumberModel代表使用的两个数据类型,第三个参数表示由前两个数据转换成的新的数据类型,我们需要使用两个int值。

使用Tuple2需要引入三方库 tuple: ^2.0.0。使用它的优点是它内置了==比较操作符,不需要我们去自己比较元素是否相等了。

  1. selector的三个参数为:BuildContextNumberModelRandomNumberModel, 返回值就是转换后的数据。

builder方法中就可以直接使用value.item1value.item2了。

  1. shouldRebuild方法的previousnext的类型是Tuple2<int, int>,可以直接比较。如果相同就不重构了。

多个状态使用的补充

Consumer2还有几个好兄弟:,Consumer3Consumer4Consumer5Consumer6

Selector2也有几个好兄弟:,Selector3Selector4Selector5Selector6

通过名字可以知道,他们分别可以组合对应的多个数据。

全部代码:

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider<NumberModel>(
          create: (ctx) => NumberModel(),
        ),
        ChangeNotifierProvider<RandomNumberModel>(
          create: (ctx) => RandomNumberModel(),
        ),
      ],
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
          primarySwatch: Colors.blue, splashColor: Colors.transparent),
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Provider"),
      ),
      body: Center(
        child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [NumberWidget1(), NumberWidget2(), NumberWidget3()]),
      ),
      floatingActionButton: Consumer2<NumberModel, RandomNumberModel>(
        child: Icon(Icons.add),
        builder: (context, value, value2, child) {
          return FloatingActionButton(
            onPressed: () {
              value.number++;
              value2.resetRandomNumber();
            },
            child: child,
          );
        },
      ),
    );
  }
}

class NumberWidget1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    int number = Provider.of<NumberModel>(context).number;
    int randomNumber = Provider.of<RandomNumberModel>(context).randomNumber;
    return Container(
      child: Text(
        "点击次数: $number 随机数: $randomNumber",
        style: TextStyle(fontSize: 30),
      ),
    );
  }
}

class NumberWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Consumer2<NumberModel, RandomNumberModel>(
        builder: (context, value, value2, child) {
          return Text("点击次数: ${value.number}  随机数: ${value2.randomNumber}",
              style: TextStyle(fontSize: 30));
        },
      ),
    );
  }
}

class NumberWidget3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Selector2<NumberModel, RandomNumberModel, Tuple2<int, int>>(
        selector: (ctx, value1, value2) => Tuple2(value1.number, value2.randomNumber),
        builder: (context, value, child) {
          return Text("点击次数: ${value.item1}  随机数: ${value.item2}",
              style: TextStyle(fontSize: 30));
        },
        shouldRebuild: (previous, next) => previous != next,
      )
    );
  }
}

class NumberModel extends ChangeNotifier {
  int _number = 0;

  int get number => _number;

  set number(int value) {
    _number = value;
    notifyListeners();
  }
}

class RandomNumberModel extends ChangeNotifier {
  int _randomNumber = Random().nextInt(100);

  int get randomNumber => _randomNumber;

  void resetRandomNumber() {
    _randomNumber = Random().nextInt(100);
    notifyListeners();
  }
}

Provider源码解析

  • Provider的基本架构如下:
Provider架构
  1. 所有的Provider都继承自InheritedProvider
  2. InheritedProvider持有一个_CreateInheritedProvider对象_delegate, _delegate持有_ValueInheritedProviderState对象,_ValueInheritedProviderState对象通过createState()方法调用了InheritedProvidercreate()方法生成了_value_value也就是开发者提供的可监测对象ChangeNotifier;

create()只有在需要使用_value时候才会调用,并不是InheritedProvider插入Widget Tree时候就调用,属于懒加载的实现。

  1. InheritedProvider有一个InheritedWidget子Widget _InheritedProviderScope。_InheritedProviderScope持有上面提到的_value的值;

也就是说Provider依赖于InheritedWidget,找到对应的InheritedWidget就能获取对应的_value的值。

  1. Widget重构的时候如果调用Provider.of方法,会找到_value的值并且监听它的变化。
  • Provider的局部刷新逻辑如下:
Provider的局部刷新
  1. _value值发生变化,会通知监听者刷新。其中会调用_InheritedProviderScope的markNeedsNotifyDependents方法,调用依赖WidgetdidChangeDependencies, 这两个方法都会调用markNeedsBuild(),进行重构;
  2. Widget重构的时候会调用Provider.of方法,更新对_value的监听,为下次重构做准备。
  • ConsumerSelector的优化逻辑:
Consumer

ConsumerSelector只是封装了一层SingleChildStatefulWidget,重构的范围限定在ConsumerSelector内部,内部调用的还是Provider.of方法。

  • MultiProvider的逻辑:
多个Provider

MultiProvider就是嵌套了多个Provider,其他和单个Provider没有什么差别。

总结

其实Provider库还提供了其他的几个ProviderListenableProvider,ValueListenableProvider,StreamProviderFutureProvider,它们都是我们开发中的可选项。

至此,我们将Provider库的使用方式和底层的逻辑解释完了。

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

推荐阅读更多精彩内容