Flutter状态管理:Provider4 入门教程(一)

背景

好久以前,在咱们的QQ群里有位朋友一直想让我出个[Provider](https://github.com/rrousselGit/provider)教程,可是我一直没有允诺。由于我以为若是写入门级的教程,已经有官方文档了,已经有人写了,若是要深刻一些呢,我又不会。但最近不太同样了,由于要水文了。html

状态管理

说到Flutter,咱们很难回避状态管理。对于React的开发者来讲,状态管理并不陌生;但对于咱们这种纯原生开发者来讲,仍是有些陌生的。 Flutter是声明式的,这意味着Flutter是经过更新UI来反映当前app的状态:git

简单来讲,在 Flutter中,若是咱们想更新咱们的控件,最基本的方式应该是 setState()了。若是说咱们一个页面里的组件很少,直接使用 setState()并无什么问题,可是实际工做中,咱们的页面布局仍是足够复杂的。

一种状况是咱们在一个页面中: github

若是咱们把全部的 Widget都写到一个类里,这个类必定会是个200多斤的胖子,并且很容易陷入 {{{{}}}}旋涡。这时咱们会想到 Widget进行拆分,但这个时候若是仅仅依靠 setState(),你会发现这将十分痛苦,由于 setState()的做用域仅限于看成 Widget,也就是说若是你仅仅在最底层的 Widget里调用 setState并不会更新顶层的 Widget,这就意味你要经过回调实现,并且在这个过程当中你会发现一些 Widget类里的变量又必须是不可变的,这又会引发其余的麻烦事,不谈。

而一般来讲,实际开发中,极可能有跨页面共享数据的可能: 编程

上图为你们展现了一个物车功能,当用户点击Add时,会将商品添加到购物车,点击购物车时,咱们能够看到刚刚的商品。想一想若是不使用状态管理,咱们应该如何实现呢?api

说了这么多,无非就是想说使用状态管理的更要性。简单来讲就是如何方便快捷地在Widget之间共享数据并将数据展现在页面上。app

Flutter的状态管理方式包括但不限于ProviderBlocRedux以及Fish-Reduxless

  • Bloc准确地来讲是一种理念,也是我使用的第一个状态管理,如今也有对应的实现库,通常来讲是基于响应式编程的。
  • Redux对于React开发者来讲并不陌生,毕竟Flutter这块也是借鉴了React
  • Fish-Redux脱胎于Redux,阿里出品,整体来讲比较复杂,适合中大型项目。如今社会也有生成Fish-Redux模板代码的工具
  • ProviderGoogle推荐的状态管理,也是我使用的第二种状态管理,相对来讲比较简单省心。

接下来,我将简单地介绍一下Provider的使用。ide

初识Provider

Provider实际上是对InheritedWidget的封装。相比于直接使用InheritedWidget,使用Provider有不少好处,好比说简化资源的分配与处置,支持懒加载等等。工具

Provider 为咱们提供了一些不一样类型的Provider。要查看全部类型的provider能够点击这里布局

name description
Provider 最基础的provider。它携带一个值并将这个值暴露,不管这个值是什么。
ListenableProvider Listenable对象而建立的providerListenableProvider会监听对象的变化,只要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调用变得很麻烦。 这个问题到此为止,喜欢研究的朋友能够自行探索。

  • 使用Providercreate中建立对象。
Provider(
  create: (_) => MyModel(),
  child: ...
)
复制代码
  • 不要 使用Provider.value建立对象。
ChangeNotifierProvider.value(
 value: MyModel(),
 child: ...
)
复制代码
  • 不要 从能够随时间变化而变化的变量中建立对象。 由于在这种状况中,即便引用的变量发生了变化,咱们建立的对象也不会被更新。
int count;

Provider(
  create: (_) => MyModel(count),
  child: ...
)
复制代码

If you want to pass variables that can change over time to your object, consider using ProxyProvider: 若是想将随时间变化而变化的变量传递到咱们的对象中,能够考虑使用ProxyProvider

int count;

ProxyProvider0(
  update: (_, __) => MyModel(count),
  child: ...
)
复制代码

笔记:当使用Providercreate/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的行为很像,这也是在上面扩展方法出现以前,咱们获取数据的方式。

These methods will look up in the widget tree starting from the widget associated with the BuildContext passed, and will return the nearest variable of type T found (or throw if nothing is found).

It's worth noting that this operation is O(1). It doesn't involve actually walking in the widget tree. 这些方法会从控件树中进行查找,而且是从与传递过来的BuildContext相关的控件开始,最终返会找到并返回与类型T的最近变量(若是未找到,则抛出)。

值得注意的是,这个操做的复杂度为O(1)。 实际上,这并不会在控件树中游走。

说到如今,无非仍是对文档的翻译,如今让咱们走码上任吧~~~

Show me the code

故事仍是要从Flutter的计数器提及,由于新建立的Flutter项目模板就是这个计数器了,如今咱们要用ChangeNotifierProvider来简单改造一下这个项目。

  • MyHomePageStatefulWidget改为StatelessWidget
  • 使用ChangeNotifierProvider来更新页面

第一个版本

首先,咱们要建立一个ChangeNotifier

class MyChangeNotifier extends ChangeNotifier {
  int _counter = 0;

  int get counter => _counter;

  incrementCounter() {
    _counter++;
    notifyListeners();//要更新UI记得调用这个方法
  }
}
复制代码

当前咱们点击FloatingActionButton时会调用MyChangeNotifierincrementCounter方法,要注意的是当咱们处理完业务时,若是须要更新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),
        ),
      ),
    );
  }
}

复制代码

当咱们高高兴兴的运行上面的代码时却遇到了一些问题: Simulator Screen Shot - iPhone 11 Pro Max - 2020-06-02 at 19.18.24.png

当我第一次遇到这个错误的时候,我不禁自主的说了一句以F开头以U结尾的话。可是话说了也不能不解决问题,这个时候,咱们可能须要 Consumer

Consumer的使用

Consumer自己没有魔法,也没有什么花里胡哨的实现。只不过是在一个新的控件中使用Provider.of,而后将这个控件的build方法委托给lamda里的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。

未完待续。。。 期待不期待你说了算。

相关文章
相关标签/搜索