likes
comments
collection
share

Flutter状态管理之Provider的使用

作者站长头像
站长
· 阅读数 10

Flutter状态管理之Provider的使用

当App的复杂性发展到一定程度,经常会出现一个页面中不同深度的子Widget需要共享访问同一个数据状态,甚至不同页面要共享同一个状态。这时我们就会想到InheritedWidget。InheritedWidget是 Flutter 中非常重要的一个功能型组件,它提供了一种在 widget 树中从上到下共享数据的方式,比如我们在应用的根 widget 中通过InheritedWidget共享了一个数据,那么我们便可以在任意子widget 中来获取该共享的数据。而Provider就是对InheritedWidget组件的上层封装,使其更易用,更易复用。

Provider的类型

为我们提供了多个类型便于不同的场景下使用:

名称描述
Provider最基础的 provider 组成,接收一个任意值并暴露它。由于它接收任何类型,所以它并不会自动帮你对暴露的数据进行addListener,所以数据变化后,也不会自动触发相关的通知和UI更新。
ListenableProvider供可监听对象使用的特殊 provider。ListenableProvider 会监听对象,并在监听器被调用时更新依赖此对象的 widgets。
ChangeNotifierProvider为 ChangeNotifier 提供的 ListenableProvider 规范,会在需要时自动调用 ChangeNotifier.dispose。
ValueListenableProvider监听 ValueListenable,并且只暴露出 ValueListenable.value。
StreamProvider监听流,并暴露出当前的最新值。
FutureProvider接收一个 Future,并在其进入 complete 状态时更新依赖它的组件。

可以看到,Provider支持的共享数据类型涉及到多个类,在这里也梳理一下:

Listenable:抽象类。子类需实现addListener和removeListener。

ValueListenable:Listenable的子类,同样是个抽象类,增加了一个属性value,用于存放值。目的在于当这个值改变时,执行所有的listener回调。注意,它是个抽象类,所以并不能直接工作,需要其他类来实现。

ChangeNotifier:Listenable的一个实现,也就是给出了addListener和removeListener的具体实现,并且多提供了一个notifyListeners的方法,用于通知listener。但是它本身没有数据,所以如果你想保存数据,需要自己写一个类保存数据,混入ChangeNotifier接口拥有它的能力,然后编写代码来确定在何时通知所有的listener。

例如:

class Counter with ChangeNotifier {
  int _count = 0;
  int get count => _count;
  void increment() {
    _count++;
    notifyListeners();
  }
}

ValueNotifier:继承ChangeNotifier,并且实现了ValueListenable,也就是说它可以存放value,又不需要自己实现addListener、notifyListeners之类的方法。所以这个类我们会经常看到。这个类自己的实现里最主要的工作就是将value的变化和notifyListeners联系起来。

Provider的使用

Provider放置的位置一般是在相应的widget的外层,也就是数据状态的共享都是在该层widget内部进行。需要使用多个Provider的话,可以选择MultiProvider来完成放置。以下代码将所有的providers放置到了app的最外层:

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => Counter()),
      ],
      child: const MyApp(),
    ),
  );
}

至于如何在代码中使用共享状态,使用 BuildContext 上的扩展属性。也可以直接使用Provider.of<T>(context)来代替watch和read,因为他们就是对Provider.of<T>(context)的封装,通过传入不同的参数值来告知Provider是否需要监听数据的变化:static T of<T>(BuildContext context, {bool listen = true})

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

以实现一个简单的计数器为例,下面给出相关的实现:

// 实现一个基于ChangeNotifier的类,内部维护一个计数,当计数有变化时,同时通知所有监听者
class Counter with ChangeNotifier {
  int _count = 0;
  int get count => _count;
  // String _description = 'Counter';
  // String get description => _description;
  void increment() {
    _count++;
    notifyListeners();
  }
}

// 显示计数器的次数
class Count extends StatelessWidget {
  const Count({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Text(
        /// Calls `context.watch` to make [Count] rebuild when [Counter] changes.
        '${context.watch<Counter>().count}',
        key: const Key('counterState'),
        style: Theme.of(context).textTheme.headline4);
  }
}

由于我们需要实时刷新计数器的数据,所以上面的Count类使用了context.watch。之后实现一个简单的页面显示计数器的数值,并通过一个按钮的点击来增加计数。由于按钮点击时,并不关心计数变化的值,所以使用context.read。需要注意,context.read不能在 StatelessWidget.buildState.build 内调用。

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Example'),
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          mainAxisAlignment: MainAxisAlignment.center,
          children: const <Widget>[
            Text('You have pushed the button this many times:'),
            Count(),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        key: const Key('increment_floatingActionButton'),
        onPressed: () => context.read<Counter>().increment(),
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

什么是Consumer和Selector

从上面的例子可以看到,我们在Column中放了两个child,一个Text,以及一个Count实例。可以看到Count这个类非常简单,那么可不可以在使用Count()的地方直接替换为Text('${context.watch<Counter>().count}')呢?从最终的实验效果看,肯定是可以的。但是使用Count这个类将对计数器的值的变化的监听封装到自己的build方法中,会有一个重要的优点,即使用Count()后,每次计数器变化时,前面的Text('You have pushed the button this many times:')不会每次重新build,所以在一些需要性能优化的场景,把对共享状态的使用和监听封装到一个新的widget中,会很有用。

此外,这种写法还有一个好处,即当 widget 很难获取到 provider 所在层级以下的 BuildContext 时,可以通过封装,轻松拿到buildContext。来看一个例子:

@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
    create: (_) => Foo(),
    child: Text(Provider.of<Foo>(context).value),
  );
}

在上面的例子中,通过context调用Provider.of会找不到Provider,因为这个context在ChangeNotifierProvider的层级之上。

Consumer其实就是类似Count的类,将对共享数据的监听和使用进行了封装,使用Consumer来重新实现上面的例子:

@override
Widget build(BuildContext context) {
  return ChangeNotifierProvider(
    create: (_) => Foo(),
    child: Consumer<Foo>(
      builder: (_, foo, __) => Text(foo.value),
    ),
  );
}

总结一下Consumer,Consumer简单封装了[Provider.of],Consumer 有两个优点:

1.有时我们无法拿到合适的BuildContext,比如provider是在buildContext之下构建的,需要Consumer帮我们拿到它。

2.Consumer可提升性能,避免不需要rebuild的Widget更新。

最后提一下,如果指向监听共享数据的部分变化,可以使用Selector,用法和Consumer类似,不过要指定具体想监听的是哪部分的变化:

Selector<Counter, String>(
        builder: (_, value, __) => Text(value),
        selector: (_, counter) => counter.description);

相关内容

1.数据共享(InheritedWidget):对InheritedWidget的原理和使用方法进行介绍。

book.flutterchina.club/chapter7/in…

2.跨组件状态共享:通过实现一个简单的Provider来为大家提供一个跨组件实现状态共享的思路。

book.flutterchina.club/chapter7/pr…

3.Provider官方发布页:提供Provider的更新内容、使用说明以及注意事项。

pub.dev/packages/pr…