Flutter 知识梳理 (状态管理) - Provider 之各种 XXProvider 的使用姿势

一、前言

Provider是目前Google推荐的状态管理方式之一,建议大家可以先看一下 Provider 的 Github 地址 了解基本的用法。

网上大多数介绍Provider的文章讲的都是ChangeNotifierProvider,看完之后确实知道它是干什么的,以及怎么用。

然而其实还有其它的Provider供我们使用,那么它们之间的区别和联系是什么呢,官方文档对它们的使用也没有详细的Demo,这篇文章就来总结一下它们的用法和区别。

Provider的分类有如下几种:

  • Provider
  • ListenableProvider
  • ChangeNotifierProvider
  • ValueListenableProvider
  • StreamProvider

二、Provider

2.1 构造函数

Provider的构造函数如下:

  Provider({
    Key key,
    @required ValueBuilder<T> builder,
    Disposer<T> dispose,
    Widget child,
  })
  • builderT Function(BuildContext context),返回要共享的数据Model
  • disposevoid Function(BuildContext context, T value),在回调中释放资源。

2.2 优点 & 缺点

Provider的优点:

  • 数据共享:通过Provider.of<T>(context)方法,可以在以Provider为根节点的子树中获取到T的对象。
  • 通过dispose参数,可以进行资源的释放。

但是Provider也有一个明显的缺点:

  • 在共享数据发生改变时,不能通知它的监听者

2.3 计数器例子

下面我们用一个经典的计数器Demo演示一下Provider的使用,为了方便对比,后面在介绍其它Provider时,也使用该例子。

根据Provider的两个特点,我们可以用它来实现BLocsink的获取,以及最后资源的释放。

  • 首先我们定义一个简单的Bloc模型。
import 'dart:async';

class ProviderBloc {

  int _count = 0;
  var _countController = StreamController<int>.broadcast();

  Stream<int> get stream => _countController.stream;
  int get count => _count;

  increment() {
    _countController.sink.add(++_count);
  }

  dispose() {
    _countController.close();
  }
}
  • 接下来编写主界面。
    • 通过Provider.of<ProviderBloc>(context)我们可以在任意的地方获取到ProviderBloc对象,获得sink来更改数据。
    • 使用StreamBuilder监听Stream中数据的变化,刷新界面。
    • 在原始的Bloc模型上,顶层的Provider提供了dispose回调,用于资源的释放。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'provider_bloc.dart';

void main() => runApp(_ProviderApp());

class _ProviderApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Provider<ProviderBloc>(
      builder: (context) => ProviderBloc(),
      dispose: (context, bloc) => bloc.dispose(),
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text('Provider Demo')),
          body: CounterLabel(),
          floatingActionButton: CounterButton(),
        ),
      ),
    );
  }
}

class CounterButton extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: Provider.of<ProviderBloc>(context).increment,
      child: const Icon(Icons.add),
    );
  }

}

class CounterLabel extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Center(
      child: StreamBuilder<int>(
        builder: (context, snapshot) {
          return Text('you have push ${snapshot.data} times');
        },
        initialData: 0,
        stream: Provider.of<ProviderBloc>(context).stream,
      ),
    );
  }
}

三、ChangeNotifierProvider

ChangeNotifierProvider应该是大家见的最多的,大多数介绍Provider的文章都是以它为例子,和 Provider 相比,它最大的优点就是解决了数据改变后无法监听的问题

3.1 构造函数

ChangeNotifierProvider的构造函数为如下:

  ChangeNotifierProvider({
    Key key,
    @required ValueBuilder<T> builder,
    Widget child,
  })
  • builderT Function(BuildContext context),返回要共享的数据Model

3.2 ChangeNotifier

使用ChangeNotifierProvider时,它要求builder返回的数据Model必须是ChangeNotifier的子类。

  • 在改变数据后,调用notifyListener()方法。
  • 通过重写它的dispose方法,可以完成和Provider一样的资源释放工作。
class Counter with ChangeNotifier {

  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }

  @override
  void dispose() {
    super.dispose();
  }
}

3.3 Provider.of<T>(context) & Consumer

在介绍Provider的文章中,Provider.of<T>(context)Consumer都会被拿来对比,一般都会推荐使用Consumer因为它会将数据发生变化后,把监听者的 Widget 重建的范围限制地更小

项目中Provider的使用,可以分为两个角色,数据改变的 触发者监听者

  • 触发者:如果只是需要获取到数据model,不需要监听变化(例如点击按钮),推荐使用Provider.of<Counter>(context, listen: false)来获取数据model
  • 监听者:推荐使用Consumer

3.4 例子

3.4.1 定义数据模型

  • 首先,定义数据模型:
    • 在改变数据后,调用notifyListeners方法。
    • 如果需要释放资源,那么需要重写dispose方法。
import 'package:flutter/foundation.dart';

class Counter with ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
  
  @override
  void dispose() {
    super.dispose();
  }
}

3.4.2 主文件

import 'counter_model.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() => runApp(_ProviderApp());

class _ProviderApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<Counter>(
      builder: (context) => Counter(),
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text('Provider Demo')),
          body: CounterLabel(),
          floatingActionButton: CounterButton(),
        ),
      )
    );
  }
}

class CounterButton extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: Provider.of<Counter>(context, listen : false).increment,
      child: const Icon(Icons.add),
    );
  }

}

class CounterLabel extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Consumer<Counter>(
          builder: (BuildContext context, Counter counter, Widget child) {
            return Text('you have push ${counter.count} times');
          }),
    );
  }
}

四、ListenableProvider

4.1 构造函数

ListenableProvider的构造函数为:

  ListenableProvider({
    Key key,
    @required ValueBuilder<T> builder,
    Disposer<T> dispose,
    Widget child,
  })
  • builderT Function(BuildContext context),返回要共享的数据Model
  • disposevoid Function(BuildContext context, T value),在回调中释放资源。

4.2 和 ChangeNotifierProvider 对比

先来一下ChangeNotifierProvider的源码:

class ChangeNotifierProvider<T extends ChangeNotifier>
    extends ListenableProvider<T> implements SingleChildCloneableWidget {
  
  static void _disposer(BuildContext context, ChangeNotifier notifier) =>
      notifier?.dispose();

  ChangeNotifierProvider({
    Key key,
    @required ValueBuilder<T> builder,
    Widget child,
  }) : super(key: key, builder: builder, dispose: _disposer, child: child);

}

从源码上可以看出,ListenableProviderChangeNotifierProvider其实是 父与子的关系ChangeNotifierProvider在它的基础上:

  • 限制数据model的上限,要求必须是ChangeNotifier的子类。
  • 通过重写ChangeNotifier.dispose()来完成资源的释放,不需要传入dispose参数给ChangeNotifierProvider

4.3 例子

使用ListenableProvider时,假如我们没有将数据模型定义成ChangeNotifier的子类,那么需要自己进行监听者的管理,为了方便,我们还是继续使用ChangeNotifier,其它地方的使用和ChangeNotifierProvider都是一样的。

import 'counter_model.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() => runApp(_ProviderApp());

class _ProviderApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return ListenableProvider<Counter>(
        builder: (context) => Counter(),
        dispose: (context, counter) => counter.dispose(),
        child: MaterialApp(
          home: Scaffold(
            appBar: AppBar(title: Text('Provider Demo')),
            body: CounterLabel(),
            floatingActionButton: CounterButton(),
          ),
        )
    );
  }
}

class CounterButton extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: Provider.of<Counter>(context, listen: false).increment,
      child: const Icon(Icons.add),
    );
  }

}

class CounterLabel extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Consumer<Counter>(
          builder: (BuildContext context, Counter counter, Widget child) {
            return Text('you have push ${counter.count} times');
          }),
    );
  }
}

五、ValueListenableProvider

5.1 构造函数

  ValueListenableProvider({
    Key key,
    @required ValueBuilder<ValueNotifier<T>> builder,
    UpdateShouldNotify<T> updateShouldNotify,
    Widget child,
  })
  • builderT Function(BuildContext context),返回要共享的数据Model
  • disposevoid Function(BuildContext context, T value),在回调中释放资源。

5.2 ValueNotifier

ValueListenableProvider要求builder返回的对象必须是ValueNotifier<T>的子类,T是需要共享的数据类型。

ValueNotifier的定义如下:

class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {

  ValueNotifier(this._value);

  @override
  T get value => _value;
  T _value;
  set value(T newValue) {
    if (_value == newValue)
      return;
    _value = newValue;
    notifyListeners();
  }

  @override
  String toString() => '${describeIdentity(this)}($value)';
}

ValueNotifier是我们前面多次提到的ChangeNotifier的子类,在改变_value时,自动调用了notifyListeners方法,那么就参照之前的方法来使用它。

5.3 例子

5.3.1 定义 ValueNotifier 的子类

import 'package:flutter/foundation.dart';

class CounterModel {

  int count;
  CounterNotifier wrapper;

  CounterModel(this.count);

}

class CounterNotifier extends ValueNotifier<CounterModel> {

  CounterNotifier(CounterModel value) : super(value) {
    value.wrapper = this;
  }

  @override
  void dispose() {
    super.dispose();
  }

}

5.3.2 主文件

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter_notifier.dart';

void main() => runApp(_ProviderApp());

class _ProviderApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return ValueListenableProvider<CounterModel>(
        builder: (context) => CounterNotifier(CounterModel(0)),
        updateShouldNotify: (model1, model2) {
          print('updateShouldNotify');
          return model1.count != model2.count;
        },
        child: MaterialApp(
          home: Scaffold(
            appBar: AppBar(title: Text('Provider Demo')),
            body: _CounterLabel(),
            floatingActionButton: _CounterButton(),
          ),
        )
    );
  }
}

class _CounterButton extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: () {
        CounterModel oldModel = Provider.of<CounterModel>(context, listen: false);
        CounterModel newModel = CounterModel(oldModel.count + 1);
        newModel.notifier = oldModel.notifier;
        oldModel.notifier.value = newModel;
        return;
      },
      child: const Icon(Icons.add),
    );
  }

}

class _CounterLabel extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Consumer<CounterModel>(
          builder: (BuildContext context, CounterModel model, Widget child) {
            return Text('you have push ${model.count} times');
          }),
    );
  }
}

5.4 疑问

这里有一个问题困扰了我很久:就是使用ValueNotifier<T>时必须要改变_value才会触发notifyListeners()方法,而通过Provider.of<T>(context, listen: false)拿到的对象是_value,因此还需要在它里面保存ValueNotifier<T>的引用(或者将ValueNotifier定义成单例的模式),再设置一次达到触发的效果,感觉用起来很奇怪,不知道是不是我的使用方式有问题,网上也没有找到相关的例子。

六、StreamProvider

StreamProvider用来结合Bloc使用。

6.1 构造函数

6.1.1 使用 Stream 构造

  StreamProvider({
    Key key,
    @required ValueBuilder<Stream<T>> builder,
    T initialData,
    ErrorBuilder<T> catchError,
    UpdateShouldNotify<T> updateShouldNotify,
    Widget child,
  })
  • builder:返回Bloc中的Stream
  • initialData:初始数据。
  • catchError:发生错误时候的回调。

6.1.2 使用 StreamController 构造

  StreamProvider.controller({
    Key key,
    @required ValueBuilder<StreamController<T>> builder,
    T initialData,
    ErrorBuilder<T> catchError,
    UpdateShouldNotify<T> updateShouldNotify,
    Widget child,
  })
  • builder:返回Bloc中的StreamController

6.2 例子

6.2.1 定义单例模式

import 'dart:async';

class ProviderBloc {

  static ProviderBloc _instance;

  ProviderBloc._internal() {
    print("_internal");
  }

  static ProviderBloc _getInstance() {
    if (_instance == null) {
      _instance = ProviderBloc._internal();
    }
    return _instance;
  }

  factory ProviderBloc() => _getInstance();
  static ProviderBloc get instance => _getInstance();

  int _count = 0;
  var _countController = StreamController<int>.broadcast();

  Stream<int> get stream => _countController.stream;
  int get count => _count;

  increment() {
    _countController.sink.add(++_count);
  }

  dispose() {
    _countController.close();
  }

}

6.2.2 主文件

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'provider_bloc.dart';

void main() => runApp(_ProviderApp());

class _ProviderApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return StreamProvider<int> (
        builder: (context) {
          return ProviderBloc().stream;
        },
        catchError: (BuildContext context, Object error) {},
        initialData: 0,
        child: MaterialApp(
          home: Scaffold(
            appBar: AppBar(title: Text('Provider Demo')),
            body: CounterLabel(),
            floatingActionButton: CounterButton(),
          ),
        )
    );
  }
}

class CounterButton extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: ProviderBloc().increment,
      child: const Icon(Icons.add),
    );
  }

}

class CounterLabel extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Consumer<int>(
          builder: (BuildContext context, int value, Widget child) {
            return Text('you have push $value times');
          }),
    );
  }
}

七、FutureProvider

7.1 构造函数

  FutureProvider({
    Key key,
    @required ValueBuilder<Future<T>> builder,
    T initialData,
    ErrorBuilder<T> catchError,
    UpdateShouldNotify<T> updateShouldNotify,
    Widget child,
  })
  • builder:返回一个Future<T>对象,异步任务的返回结果,在结果返回后,会触发Consumer的重建。
  • initialData:初始数据。
  • catchError:发生错误的回调。

7.2 和 FutureBuilder 的区别

FutureProvider和我们之前讨论的Provider场景不太一样,它和FutureBuilder比较类似,就是在数据返回之前加载一个组件,等待数据返回值后,重绘返回另一个组件。

它和FutureBuilder的区别在于:

  • FutureBuilder的数据请求和展示都是在一个组件当中,而FutureProvider是数据的请求者,Consumer是展示者。
  • FutureBuilder的请求者和展示者是一对一的关系,而FutureProvider可以是一对多的关系。

7.3 例子

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() => runApp(_ProviderApp());

class _ProviderApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return FutureProvider<int>(
        builder: (context) => _request(),
        initialData: 0,
        child: MaterialApp(
          home: Scaffold(
            appBar: AppBar(title: Text('Provider Demo')),
            body: CounterLabel(),
          ),
        )
    );
  }

  Future<int> _request() async {
    return await Future<int>.delayed(Duration(milliseconds: 3000)).then((int value) {
        return 300;
    });
  }
}

class CounterLabel extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(children: <Widget>[
        Consumer<int>(
            builder: (BuildContext context, int count, Widget child) {
              return Text('Observer1=$count');
            }),
        Consumer<int>(
            builder: (BuildContext context, int count, Widget child) {
              return Text('Observer2=$count');
            }),
      ],)
    );
  }
}

七、小结

对比以上几种Provider的使用方式:还是ChangeNotifierProviderStreamProvider比较符合我们平时的使用场景。

而其它三种:

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