provider: ^6.0.4(来自于文档)

provider 6.0.4

InheritedWidget 组件的上层封装,使其更易用,更易复用。

使用 provider 而非手动书写 InheritedWidget,有以下的优势:

  • 简化的资源分配与处置
  • 懒加载
  • 创建新类时减少大量的模板代码
  • 支持 DevTools
  • 更通用的调用 InheritedWidget 的方式(参考 Provider.of/Consumer/Selector
  • 提升类的可扩展性,整体的监听架构时间复杂度以指数级增长(如 ChangeNotifier, 其复杂度为 O(N))

想了解更多 provider 相关内容,请参考
文档

更多内容:

自 4.x.x 版本升级到 5.0.0-nullsafety

  • FutureProviderStreamProvider 现在要求声明 initialData

    待迁移的旧代码:

    FutureProvider<int>(
      create: (context) => Future.value(42),
      child: MyApp(),
    )
    
    Widget build(BuildContext context) {
      final value = context.watch<int>();
      return Text('$value');
    }
    

    迁移后:

    FutureProvider<int?>(
      initialValue: null,
      create: (context) => Future.value(42),
      child: MyApp(),
    )
    
    Widget build(BuildContext context) {
      // be sure to specify the ? in watch<int?>
      final value = context.watch<int?>();
      return Text('$value');
    }
    
  • ValueListenableProvider 已被移除

    你只需要将 ValueListenableBuilderProvider 结合使用即可:

    ValueListenableBuilder<int>(
      valueListenable: myValueListenable,
      builder: (context, value, _) {
        return Provider<int>.value(
          value: value,
          child: MyApp(),
        );
      }
    )
    

使用

暴露一个值

暴露一个新的对象实例

Provider 不仅可以暴露出一个值,同时也可以创建、监听和销毁它。

要暴露一个新创建的对象,你可以使用这个 provider 的默认构造。
而如果你想在开始监听时再 创建 一个对象,
不推荐使用 .value 构造函数,否则可能会有你预期外的副作用。

你可以阅读在 StackOverflow 上的回答
以了解为什么不推荐使用 .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 的 createupdate 回调时,回调函数默认是延迟调用的。

也就是说,变量被读取时,createupdate 函数才会被调用。

如果你想预先计算一些对象内的逻辑,可以使用 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 类型的 provider 发生的改变。
  • context.read<T>(),直接返回 T,不会监听改变。
  • context.select<T,R>(R cb(T value)),允许 widget 只监听 T 上的一部分内容的改变。

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

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

上面列举的这些方法会从传入的 BuildContext 关联的 widget 开始,向上查找 widget 树,
并返回查找到的层级最近的 T 类型的 provider(未找到时将抛出错误)。

值得一提的是,该操作的复杂度是 O(1),它实际上并不会遍历整个组件树。

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

class Home extends StatelessWidget {
  
  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

它们往往在一些需要 性能优化 的场景,
以及当 widget 很难获取到 provider 所在层级以下的 BuildContext 时非常有用。

查阅 FAQ
或关于 ConsumerSelector 的文档了解更多。

依赖可能不存在的 Provider

某些情况下,我们可能需要支持 provider 不存在的查询。
例如一个可能在 provider 以外使用的封装复用的 widget。

此时你可以将 context.watchcontext.read 对应的 T 声明为可空的类型。

假设原有的代码为:

context.watch<Model>()

会在找不到 provider 时抛出 ProviderNotFoundException,而按以下方法修改后:

context.watch<Model?>()

在查询时会尝试找到匹配 provider,未找到时返回 null 而不会抛出异常。

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 能够将多个 provider 的值聚合为一个新对象,将结果传递给 Provider

这个新对象会在其依赖的任意一个 provider 更新后同步更新。

下面的例子使用了 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';
}

它还有其他不同的形式:

  • ProxyProviderProxyProvider2ProxyProvider3

    类名后的数字是 ProxyProvider 依赖的 provider 的数量。

  • ProxyProviderChangeNotifierProxyProviderListenableProxyProvider

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

常见问题

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

Flutter 提供的 DevTools 能够展示特定时刻下的 widget 树。

既然 provider 也同样是 widget,那么它们同样能通过 DevTools 进行查看。

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

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

DevTools 只显示了「Instance of MyClass」,我应该怎么做?

默认情况下,DevTools 基于 toString,也就使得默认结果是「Instance of MyClass」。

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

  • 使用 Flutter 提供的 Diagnosticable API

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

    class MyClass with DiagnosticableTreeMixin {
      MyClass({this.a, this.b});
    
      final int a;
      final String b;
    
      
      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 的 package 中),
    那么你可以通过重写 toString 方法来达成效果。

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

    class MyClass with DiagnosticableTreeMixin {
      MyClass({this.a, this.b});
    
      final int a;
      final String b;
    
      
      String toString() {
        return '$runtimeType(a: $a, b: $b)';
      }
    }
    
initState 方法里调用 provider 的获取方法时发生了异常,我应该怎么做?

异常的出现是因为你尝试在一个 永远不会再次被调用的生命周期 监听 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);
  }
}

此时 print 只会在 value 变化时执行。

或者你也可以这么做:

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

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

如何在热更新 (hot-reload) 时处理我的对象?

你可以将你的 provider 对象实现 ReassembleHandler 类:

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

它们通常会和 provider 一同使用:

ChangeNotifierProvider(create: (_) => Example()),
使用 ChangeNotifier 时,在更新后出现了异常,发生了什么?

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

最典型的情况是一个发起 Http 请求的 Future 被保存在 notifier 内部:

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

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

也就是说,一些 widget 可能在 变更发生前(获取到旧的值)构建,
而另一些则可能在 变更后(获取到新的值)构建。
这可能造成 UI 不一致,因此是被禁止的。

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

  • 在你的 model 的构造方法内直接调用:

    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;

  
  ExampleState createState() => ExampleState();
}

class ExampleState extends State<Example> {
  int _count;

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

  
  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 重新 build 太频繁了,我应该怎么做?

你可以使用 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),来指定将要被消费的接口,
同时你需要在 create 中提供具体实现:

abstract class ProviderInterface with ChangeNotifier {
  ...
}

class ProviderImplementation with ChangeNotifier implements ProviderInterface {
  ...
}

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

ChangeNotifierProvider<ProviderInterface>(
  create: (_) => ProviderImplementation(),
  child: Foo(),
),
现有的 providers

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

完整的可用列表参见 此处

名称描述
Provider最基础的 provider 组成,接收一个任意值并暴露它。
ListenableProvider供可监听对象使用的特殊 provider。ListenableProvider 会监听对象,并在监听器被调用时更新依赖此对象的 widgets。
ChangeNotifierProvider为 ChangeNotifier 提供的 ListenableProvider 规范,会在需要时自动调用 ChangeNotifier.dispose
ValueListenableProvider监听 ValueListenable,并且只暴露出 ValueListenable.value
StreamProvider监听流,并暴露出当前的最新值。
FutureProvider接收一个 Future,并在其进入 complete 状态时更新依赖它的组件。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值