Provider 5.0

  • 新的 create/update 回调函数是懒加载的, 也就是说他们在对应的值第一次被读取时才被调用, 而非provider首次被创建时.

    如果你不需要这个特性, 你可以通过将provider的lazy属性置为false, 来禁用懒加载

    FutureProvider(
      create: (_) async => doSomeHttpRequest(),
      lazy: false,
      child: ...
    )
    
  • ProviderNotFoundError 更名为 ProviderNotFoundException.

  • SingleChildCloneableWidget 接口被移除, 并被全新类型的组件 SingleChildWidget 所替代

    参考这个 issue 来获取迁移细节.

  • Selector 现在会将先后的集合类型的值进行深层对比

    如果你不需要这个特性, 你可以通过 shouldRebuild 参数来使其还原至旧有表现.

    Selector<Selected, Consumed>(
      shouldRebuild: (previous, next) => previous == next,
      builder: ...,
    )
    
  • DelegateWidget及其家族widget被移除, 现在想要自定义provider, 直接继承 InheritedProvider 或当前存在的provider.

使用

暴露一个值

暴露一个新的对象实例

Providers不仅允许暴露出一个值,也可以创建/监听/销毁它。

要暴露一个新创建的对象, 使用一个provider的默认构造函数. 如果你想创建一个对象, 不要使用 .value 构造函数, 否则可能会有你预期外的副作用。

查看该 StackOverflow Answer,来了解更多为什么不要使用.value构造函数创建值。

  • 在create内创建新对象

    Provider(
      create: (_) => MyModel(),
      child: ...
    )
    
  • 不要使用Provider.value创建对象

    ChangeNotifierProvider.value(
      value: MyModel(),
      child: ...
    )
    
  • 不要以可能随时间改变的变量创建对象

    在这种情况下,如果变量发生变化,你的对象将永远不会被更新

    int count;
    
    Provider(
      create: (_) => MyModel(count),
      child: ...
    )
    

    如果你想将随时间改变的变量传入给对象,请使用ProxyProvider:

    int count;
    
    ProxyProvider0(
      update: (_, __) => MyModel(count),
      child: ...
    )
    

注意:

在使用一个provider的create/update回调时,请注意回调函数默认是懒调用的。

也就是说, 除非这个值被读取了至少一次, 否则create/update函数不会被调用。

如果你想预先计算一些逻辑, 可以通过使用lazy参数来禁用这一行为。

MyProvider(
  create: (_) => Something(),
  lazy: false,
)

复用一个已存在的对象实例:

如果你已经拥有一个对象实例并且想暴露出它,你应当使用一个provider的.value构造函数。

如果你没有这么做,那么在你调用对象的 dispose 方法时, 这个对象可能仍然在被使用。

  • 使用ChangeNotifierProvider.value来提供一个当前已存在的 ChangeNotifier

    MyChangeNotifier variable;
    
    ChangeNotifierProvider.value(
      value: variable,
      child: ...
    )
    
  • 不要使用默认的构造函数来尝试复用一个已存在的 ChangeNotifier

    MyChangeNotifier variable;
    
    ChangeNotifierProvider(
      create: (_) => variable,
      child: ...
    )
    

读取一个值

读取一个值最简单的方式就是使用BuildContext上的扩展属性(由provider注入)。

  • context.watch<T>(), 一方法使得widget能够监听泛型T上发生的改变。
  • context.read<T>(),直接返回T,不会监听改变。
  • context.select<T, R>(R cb(T value)),允许widget只监听T上的一部分(R)。

或者使用 Provider.of<T>(context)这一静态方法,它的表现类似 watch ,而在你为 listen 参数传入 false 时(如 Provider.of<T>(context,listen: false) ),它的表现类似于 read

值得注意的是,context.read<T>() 方法不会在值变化时使得widget重新构建, 并且不能在 StatelessWidget.build/State.build 内调用. 换句话说, 它可以在除了这两个方法以外的任意之处调用。

上面列举的这些方法会与传入的 BuildContext 关联的widget开始查找widget树,并返回查找到的最近的类型T的变量(如果没有找到, 将抛出错误)。

值得注意是这一操作的复杂度是 O(1),它实际上并不涉及遍历整个组件树。

结合上面第一个向外暴露一个值的例子,这个widget会读取暴露出的String并渲染Hello World

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(
      // Don't forget to pass the type of the object you want to obtain to `watch`!
      context.watch<String>(),
    );
  }
}

或者不使用这些方法,我们也可以使用 ConsumerSelector

这些往往在性能优化以及当很难获取到provider的构建上下文后代(difficult to obtain a BuildContext descendant of the provider) 时是很有用的。

参见 FAQ 或关于ConsumerSelector 的文档部分了解更多.

MultiProvider

当在大型应用中注入较多状态时, Provider 很容易变得高度耦合:

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

使用MultiProvider:

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

以上两个例子的实际表现是一致的, MultiProvider唯一改变的就是代码书写方式.

ProxyProvider

从3.0.0开始, 我们提供了一种新的provider: ProxyProvider.

ProxyProvider能够将多个来自于其他的providers的值聚合为一个新对象,并且将结果传递给Provider

这个新对象会在其依赖的任一providers更新后被更新

下面的例子使用ProxyProvider,基于来自于另一个provider的counter值进行转化。

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

class Translations {
  const Translations(this._value);

  final int _value;

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

这个例子还有多种变化:

  • ProxyProvider vs ProxyProvider2 vs ProxyProvider3, ...

    类名后的数字是 ProxyProvider 依赖的其他providers的数量

  • ProxyProvider vs ChangeNotifierProxyProvider vs ListenableProxyProvider, ...

    它们工作的方式是相似的, 但 ChangeNotifierProxyProvider 会将它的值传递给ChangeNotifierProvider 而非 Provider

FAQ

我是否能查看(inspect)我的对象的内容?

Flutter提供的开发者工具能够展示特定时刻下的widget树。

既然providers同样是widget,他们同样能通过开发者工具进行查看。

img

点击一个provider, 即可查看它暴露出的值:

[图片上传失败...(image-6c27fc-1623978187784)]

以上的开发者工具截图来自于 /example 文件夹下的示例

开发者工具只显示"Instance of MyClass", 我能做什么?

默认情况下, 开发者工具基于toString,也就使得默认结果是 "Instance of MyClass"。

如果要得到更多信息,你有两种方式:

  • 使用Flutter提供的 Diagnosticable API

    在大多数情况下, 只需要在你的对象上使用 DiagnosticableTreeMixin 即可,以下是一个自定义 debugFillProperties 实现的例子:

    class MyClass with DiagnosticableTreeMixin {
      MyClass({this.a, this.b});
    
      final int a;
      final String b;
    
      @override
      void debugFillProperties(DiagnosticPropertiesBuilder properties) {
        super.debugFillProperties(properties);
        // list all the properties of your class here.
        // See the documentation of debugFillProperties for more information.
        properties.add(IntProperty('a', a));
        properties.add(StringProperty('b', b));
      }
    }
    
  • 重写toString方法

    如果你无法使用 DiagnosticableTreeMixin (比如你的类在一个不依赖于Flutter的包中), 那么你可以通过简单重写toString方法来达成效果。

    这比使用 DiagnosticableTreeMixin 要更简单,但能力也有着不足: 你无法 展开/折叠 来查看你的对象内部细节。

    class MyClass with DiagnosticableTreeMixin {
      MyClass({this.a, this.b});
    
      final int a;
      final String b;
    
      @override
      String toString() {
        return '$runtimeType(a: $a, b: $b)';
      }
    }
    

在获得initState内部的Providers时发生了异常, 该做什么?

这个异常的出现是因为你在尝试监听一个来自于永远不会再次被调用的生命周期的provider。

这意味着你要么使用另外一个生命周期(build),要么显式指定你并不在意后续更新。

也就是说,不应该这么做:

initState() {
  super.initState();
  print(context.watch<Foo>().value);
}

你可以这么做:

Value value;

Widget build(BuildContext context) {
  final value = context.watch<Foo>.value;
  if (value != this.value) {
    this.value = value;
    print(value);
  }
}

这会且只会在value变化时打印它。

或者你也可以这么做:

initState() {
  super.initState();
  print(context.read<Foo>().value);
}

这样只会打印一次value,并且会忽视后续的更新

如何控制我的对象上的热更新?

你可以使你提供的对象实现 ReassembleHandler 类:

class Example extends ChangeNotifier implements ReassembleHandler {
  @override
  void reassemble() {
    print('Did hot-reload');
  }
}

通常会和 provider 一同使用:

ChangeNotifierProvider(create: (_) => Example()),

使用ChangeNotifier时, 在更新后出现了异常, 发生了什么?

这通常是因为你在widget树正在构建时,从ChangeNotifier的某个后代更改了ChangeNotifier。

最典型的情况是在一个future被保存在notifier内部时发起http请求。

initState() {
  super.initState();
  context.read<MyNotifier>().fetchSomething();
}

这是不被允许的,因为更改会立即生效.

也就是说,一些widget可能在变更发生前构建,而有些则可能在变更后. 这可能造成UI不一致, 因此是被禁止的。

所以,你应该在一个整个widget树所受影响相同的位置执行变更:

  • 直接在你的model的 provider/constructor 的 create 方法内调用:

    class MyNotifier with ChangeNotifier {
      MyNotifier() {
        _fetchSomething();
      }
    
      Future<void> _fetchSomething() async {}
    }
    

    在不需要传入形参的情况下,这是相当有用的。

  • 在框架的末尾异步的执行(Future.microtask):

    initState() {
      super.initState();
      Future.microtask(() =>
        context.read<MyNotifier>(context).fetchSomething(someValue);
      );
    }
    

    这可能不是理想的使用方式,但它允许你向变更传递参数。

我必须为复杂状态使用 ChangeNotifier 吗?

不。

你可以使用任意对象来表示你的状态,举例来说,一个可选的架构方案是使用Provider.value配合StatefulWidget

这是一个使用这种架构的计数器示例:

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(context.watch<int>().toString());

并且这样来修改状态:

return FloatingActionButton(
  onPressed: () => context.read<ExampleState>().increment(),
  child: Icon(Icons.plus_one),
);

或者你还可以自定义provider.

我可以创建自己的Provider吗?

可以,provider暴露出了所有构建功能完备的provider所需的组件,它包含:

  • SingleChildStatelessWidget, 使任意widget能够与 MultiProvider 协作, 这个接口被暴露为包 package:provider/**single_child_widget 的一部分**
  • InheritedProvider,在使用 context.watch 时可获取的通用InheritedWidget

这里有个使用 ValueNotifier 作为状态的自定义provider例子:

https://gist.github.com/rrousselGit/4910f3125e41600df3c2577e26967c91

我的widget重构建太频繁了, 我能做什么?

你可以使用 context.select 而非 context.watch 来指定只监听对象的部分属性:

举例来说,你可以这么写:

Widget build(BuildContext context) {
  final person = context.watch<Person>();
  return Text(person.name);
}

这可能导致widget在 name 以外的属性发生变化时重构建。

你可以使用 context.select来 只监听name属性

Widget build(BuildContext context) {
  final name = context.select((Person p) => p.name);
  return Text(name);
}

这样,这widget间就不会在name以外的属性变化时进行不必要的重构建了。

同样,你也可以使用Consumer/Selector,可选的child参数使得widget树中只有所指定的一部分会重构建。

Foo(
  child: Consumer<A>(
    builder: (_, a, child) {
      return Bar(a: a, child: child);
    },
    child: Baz(),
  ),
)

在这个示例中, 只有Bar会在A更新时重构建,FooBaz不会进行不必要的重构建。

我能使用相同类型来获得两个不同的provider吗?

不。 当你有两个持有相同类型的不同provider时,一个widget只会获取其中之一: 最近的一个

你必须显式为两个provider提供不同类型,而不是:

Provider<String>(
  create: (_) => 'England',
  child: Provider<String>(
    create: (_) => 'London',
    child: ...,
  ),
),

推荐的写法:

Provider<Country>(
  create: (_) => Country('England'),
  child: Provider<City>(
    create: (_) => City('London'),
    child: ...,
  ),
),

我能消费一个接口并且提供一个实现吗?

能,类型提示(type hint)必须被提供给编译器,来指定将要被消费的接口,同时需要在craete中提供具体实现:

abstract class ProviderInterface with ChangeNotifier {
  ...
}

class ProviderImplementation with ChangeNotifier implements ProviderInterface {
  ...
}

class Foo extends StatelessWidget {
  @override
  build(context) {
    final provider = Provider.of<ProviderInterface>(context);
    return ...
  }
}

ChangeNotifierProvider<ProviderInterface>(
  create: (_) => ProviderImplementation(),
  child: Foo(),
),

现有的providers

provider中提供了几种不同类型的"provider",供不同类型的对象使用。

完整的可用列表参见 provider-library

name description
Provider 最基础的provider组成,接收一个值并暴露它, 无论值是什么。
ListenableProvider 供可监听对象使用的特殊provider,ListenableProvider会监听对象,并在监听器被调用时更新依赖此对象的widgets。
ChangeNotifierProvider 为ChangeNotifier提供的ListenableProvider规范,会在需要时自动调用ChangeNotifier.dispose
ValueListenableProvider 监听ValueListenable,并且只暴露出ValueListenable.value
StreamProvider 监听流,并暴露出当前的最新值。
FutureProvider 接收一个Future,并在其进入complete状态时更新依赖它的组件。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容