Flutter Provider使用指南

前言

  使用一种语言编写各种应用的时候,横亘在开发者面前的第一个问题就是如何进行状态管理。在前端领域,我们习惯使用框架或者各种辅助库来进行状态管理。例如,开发者经常使用react自带的context,或者mobx/redux等工具来管理组件间状态。在大热的跨端框架flutter中,笔者将对社区中使用广泛的provider框架进行介绍。

准备工作

安装与引入

provider pub链接
官方文档宣称(本文基于4.0版本),provider是一个依赖注入和状态管理的混合工具,通过组件来构建组件。
provider有以下三个特点:

  1. 可维护性,provider强制使用单向数据流
  2. 易测性/可组合性,provider可以很方便地模拟或者复写数据
  3. 鲁棒性,provider会在合适的时候更新组件或者模型的状态,降低错误率

在pubspec.yaml文件中加入如下内容:

dependencies:
  provider: ^4.0.0

然后执行命令flutter pub get,安装到本地。
使用时只需在文件头部加上如下内容:

import 'package:provider/provider.dart';

暴露一个值

如果我们想让某个变量能够被一个widget及其子widget所引用,我们需要将其暴露出来,典型写法如下:

Provider(
  create: (_) => new MyModel(),
  child: ...
)

读取一个值

如果要使用先前暴露的对象,可以这样操作

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    MyModel yourValue = Provider.of<MyModel>(context)
    return ...
  }
}

暴露和使用多个值(MultiProvider)

Provider的构造方法可以嵌套使用

Provider<Something>(
  create: (_) => Something(),
  child: Provider<SomethingElse>(
    create: (_) => SomethingElse(),
    child: Provider<AnotherThing>(
      create: (_) => AnotherThing(),
      child: someWidget,
    ),
  ),
),

上述代码看起来过于繁琐,走入了嵌套地狱,好在provider给了更加优雅的实现

MultiProvider(
  providers: [
    Provider<Something>(create: (_) => Something()),
    Provider<SomethingElse>(create: (_) => SomethingElse()),
    Provider<AnotherThing>(create: (_) => AnotherThing()),
  ],
  child: someWidget,
)

代理provider(ProxyProvider)

在3.0版本之后,有一种新的代理provider可供使用,ProxyProvider能够将不同provider中的多个值整合成一个对象,并将其发送给外层provider,当所依赖的多个provider中的任意一个发生变化时,这个新的对象都会更新。下面的例子使用ProxyProvider来构建了一个依赖其他provider提供的计数器的例子

Widget build(BuildContext context) {
  return MultiProvider(
    providers: [
      ChangeNotifierProvider(create: (_) => Counter()),
      ProxyProvider<Counter, Translations>(
        create: (_, counter, __) => Translations(counter.value),
      ),
    ],
    child: Foo(),
  );
}

class Translations {
  const Translations(this._value);

  final int _value;

  String get title => 'You clicked $_value times';
}

各种provider

可以通过各种不同的provider来应对具体的需求

  • Provider 最基础的provider,它会获取一个值并将它暴露出来
  • ListenableProvider 用来暴露可监听的对象,该provider将会监听对象的改变以便及时更新组件状态
  • ChangeNotifierProvider ListerableProvider依托于ChangeNotifier的一个实现,它将会在需要的时候自动调用ChangeNotifier.dispose方法
  • ValueListenableProvider 监听一个可被监听的值,并且只暴露ValueListenable.value方法
  • StreamProvider 监听一个流,并且暴露出其最近发送的值
  • FutureProvider 接受一个Future作为参数,在这个Future完成的时候更新依赖

项目实战

接下来笔者将以自己项目来举例provider的用法
首先定义一个基类,完成一些UI更新等通用工作

import 'package:provider/provider.dart';

class ProfileChangeNotifier extends ChangeNotifier {
  Profile get _profile => Global.profile;

  @override
  void notifyListeners() {
    Global.saveProfile(); //保存Profile变更
    super.notifyListeners();
  }
}

之后定义自己的数据类

class UserModle extends ProfileChangeNotifier {
  String get user => _profile.user;
  set user(String user) {
    _profile.user = user;
    notifyListeners();
  }

  bool get isLogin => _profile.isLogin;
  set isLogin(bool value) {
    _profile.isLogin = value;
    notifyListeners();
  }

  String get avatar => _profile.avatar;
  set avatar(String value) {
    _profile.avatar = value;
    notifyListeners();
  }

这里通过setget方法劫持对数据的获取和修改,在有相关改动发生时通知组件树同步状态。
在主文件中,使用provider

class MyApp extends StatelessWidget with CommonInterface {

  MyApp({Key key, this.info}) : super(key: key);
  final info;
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    UserModle newUserModel = new UserModle();
    return MultiProvider(
      providers: [
        //  用户信息
        ListenableProvider<UserModle>.value(value: newUserModel),
      ],
      child: ListenContainer(),
    );
  }
}

接下来,在所有的子组件中,如果需要使用用户的名字,只需Provider.of<UserModle>(context).user即可,但是这样的写法看上去不够精简,每次调用时都需要写很长的一段开头Provider.of<xxx>(context).XXX很是繁琐,故而这里我们可以简单封装一个抽象类:

abstract class CommonInterface {
  String cUser(BuildContext context) {
    return Provider.of<UserModle>(context).user;
  }
}

在子组件声明时,使用with,来简化代码

class MyApp extends StatelessWidget with CommonInterface {
  ......
}

在使用时只需cUser(context)即可。

class _FriendListState extends State<FriendList> with CommonInterface {
  @override
  Widget build(BuildContext context) {
    return Text(cUser(context));
  }
}

项目完整代码详见本人仓库

其他相关细节和常见问题(来自官方文档)

  1. 为什么在initState中获取Provider会报错?
    不要在只会调用一次的组件生命周期中调用Provider,比如如下的使用方法是错误的
initState() {
  super.initState();
  print(Provider.of<Foo>(context).value);
}

要解决这个问题,要么使用其他生命周期方法(didChangeDependencies/build)

didChangeDependencies() {
  super.didChangeDependencies();
  final value = Provider.of<Foo>(context).value;
  if (value != this.value) {
    this.value = value;
    print(value);
  }
}

或者指明你不在意这个值的更新,比如

initState() {
  super.initState();
  print(Provider.of<Foo>(context, listen: false).value);
}
  1. 我在使用ChangeNotifier的过程中,如果更新变量的值就会报出异常?
    这个很有可能因为你在改变某个子组件的ChangeNotifier时,整个渲染树还处在创建过程中。
    比较典型的使用场景是notifier中存在http请求
initState() {
  super.initState();
  Provider.of<Foo>(context).fetchSomething();
}

这是不允许的,因为组件的更新是即时生效的。
换句话来说如果某些组件在异步过程之前构建,某些组件在异步过程之后构建,这很有可能触发你应用中的UI表现不一致,这是不允许的。
为了解决这个问题,需要把你的异步过程放在能够等效的影响组件树的地方

  • 直接在你provider模型的构造函数中进行异步过程
class MyNotifier with ChangeNotifier {
  MyNotifier() {
    _fetchSomething();
  }

  Future<void> _fetchSomething() async {}
}
  • 或者直接添加异步行为
initState() {
  super.initState();
  Future.microtask(() =>
    Provider.of<Foo>(context).fetchSomething(someValue);
  );
}
  1. 为了同步复杂的状态,我必须使用ChangeNotifier吗?
    并不是,你可以使用一个对象来表示你的状态,例如把Provider.value()StatefulWidget结合起来使用,达到即刷新状态又同步UI的目的.
class Example extends StatefulWidget {
  const Example({Key key, this.child}) : super(key: key);

  final Widget child;

  @override
  ExampleState createState() => ExampleState();
}

class ExampleState extends State<Example> {
  int _count;

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

  @override
  Widget build(BuildContext context) {
    return Provider.value(
      value: _count,
      child: Provider.value(
        value: this,
        child: widget.child,
      ),
    );
  }
}

当需要读取状态时:

return Text(Provider.of<int>(context).toString());

当需要改变状态时:

return FloatingActionButton(
  onPressed: Provider.of<ExampleState>(context).increment,
  child: Icon(Icons.plus_one),
);
  1. 我可以封装我自己的Provider么?
    可以,provider暴露了许多细节api以便使用者封装自己的provider,它们包括:SingleChildCloneableWidgetInheritedProviderDelegateWidgetBuilderDelegateValueDelegate
  2. 我的组件重建得过于频繁,这是为什么?
    可以使用Provider.of来替代Consumer/Selector.
    可以使用可选的child参数来保证组件树只会重建某个特定的部分
Foo(
  child: Consumer<A>(
    builder: (_, a, child) {
      return Bar(a: a, child: child);
    },
    child: Baz(),
  ),
)

在以上例子中,当A改变时,只有Bar会重新渲染,FooBaz并不会进行不必要的重建。
为了更精细地控制,我们还可以使用Selector来忽略某些不会影响组件数的改变。

Selector<List, int>(
  selector: (_, list) => list.length,
  builder: (_, length, __) {
    return Text('$length');
  }
);

在这个例子中,组件只会在list的长度发生改变时才会重新渲染,其内部元素改变时并不会触发重绘。

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

推荐阅读更多精彩内容