Flutter 状态管理 - 通俗易懂 Provider

2,801 阅读3分钟

本文主要讲解了 Flutter 状态管理框架 Provider 的发展过程、设计目的以及如何使用,保证读过之后可以快速上手 Provider。

InheritedWidget

InheritedWidget 是 Flutter 提供可以在 WidgetTree 上进行数据共享的 Widget,它解决了依赖注入的问题。 如果不使用它,subWidget 如果想获取 parentWidget 的数据就需要从 parentWidget 创建 subWidget 的时候一层层的传递 Data:

通过构造注入的方式会导致代码难以维护,所以我们可以在 WidgetTree 某个合适的位置放置一个 InheritedWidget 用来管理全部数据,它的 subWidget 都可以通过 InheritedWidget 来获取数据:

自定义 InheritedWidget

使用 InheritedWidget 可以分为三个步骤:

  • 继承 InheratedWidget
  • 为实现类添加 data field
  • 添加 of 方法让 child 可以方便的获取 InheritedWidget

class CounterInheritedWidget extends InheritedWidget {
  final int count;

  CounterInheritedWidget({this.count, Widget child})
      : super(child: child);

  static CounterInheritedWidget of(BuildContext context) {
  /// 通过 BuildContext 可以在 WigetTree 上找到最近的 CounterInheritedWidget
    return context.inheritFromWidgetOfExactType(CounterInheritedWidget);
  }

  @override
  bool updateShouldNotify(CounterInheritedWidget oldWidget) {
    return count != oldWidget.count;
  }
}

SubWiget 获取数据
class MyHomePage extends StatefulWidget {
  ...
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  ...
  @override
  Widget build(BuildContext context) {
    return CountInheritedWidget(
      count:0,
      child: SubChildWidget()
    );
  }
}

class SubChildWidget extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return Text("value:${CountInheritedWidget.of(context).count}");
  }
}

InheritedWidget 只提供了数据共享的功能,没有提供 Widget 之间通信的功能,比如点击一个按钮使 count++ 后刷新界面,所以 Fultter 又提供了 ChangeNotifier,ChangeNotifier 配合 InheritedWidget 可以做到组件间通信。

ChangeNotifier

ChangeNotifier 使用了观察者模式,可以向 ChangeNotifier 添加 Listener,在数据变化的时候 ChangeNotifier 会通知它的观察者。这里我们使用 ValueNotifier(ChangeNotifier 的一种子类)来展示如何和 InheritedWidget 配合实现通信。 原理其实很简单,InheritedWidget 可以将它的内容分享给子树,那么我们在 InheritedWidget 声明一个 ChangeNotifier 让它的子树自由访问就好了.

为 InheritedWidget 添加 ValueNotifier
class CountInheritedWidget extends InheritedWidget {
  int count = 1;
  final ValueNotifier<int> countNotifier;

  CountInheritedWidget(this.countNotifier, Widget child) : super(child: child);

  @override
  bool updateShouldNotify(CountInheritedWidget oldWidget) {
    return count != oldWidget.count;
  }

  void increase() {
    count++;
    countNotifier.value = count;
  }

  static CountInheritedWidget of(BuildContext context) {
    // ignore: deprecated_member_use
    return context.inheritFromWidgetOfExactType(CountInheritedWidget);
  }
}
SubWidget 使用 ChangeNotifier
class CounterInheritedPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return CounterInheritedPageState();
  }
}

class CounterInheritedPageState extends State {
  @override
  void initState() {
    super.initState();
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    /// 添加监听
    CountInheritedWidget.of(context).countNotifier.addListener(() {
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text("count:${CountInheritedWidget.of(context).count}"),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          /// 改变 value
          CountInheritedWidget.of(context).increase();
        },
      ),
    );
  }
}

为了不显示的 addListener,我们也可以使用 ValueListenableBuilder:

class SubChildWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      valueListenable: CountInheritedWidget.of(context).countNotifier,
      builder: (context, value, _) {
        return Scaffold(
          body: Center(
            child: Text("count:$value"),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () {
              CountInheritedWidget.of(context).increase();
            },
          ),
        );
      },
    );
  }
}

ChangeNotifierProvider

为了应对更加复杂的业务场景,开源社区基于 InheritedWidget 封装了更加强大的 Provider 框架,Provider 中的 ChangeNotifierProvider 配合 ChangeNotifier 可以进行复杂的状态管理。

作用

官网上有这一段话:

ChangeNotifierProvider is the widget that provides an instance of a ChangeNotifier to its descendants.

意思是 ChangeNotifierProvider 可以向它的子类提供 ChangeNotifier 的实例,这和我们上面的代码是不是很类似。

使用

先定义一个 ChangeNotifier:

class CounterModel with ChangeNotifier{
  int count = 10;
  void increase(){
    count++;
    notifyListeners();
  }
}

然后在根 Widget 下添加监听:

void main() => runApp(MyApp());
...
class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
        create: (context) => CounterModel(),
        child: CounterProviderSimplePage());
  }
}

最后在 SubWidget 下通过 Provider.of 来获取 CounterModel 进行数据的获取和修改:

class CounterProviderSimplePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text("counter1: ${Provider.of<CounterModel>(context).count}")
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Provider.of<CounterModel>(context, listen: false).increase();
        },
      ),
    );
  }
}
子类获取数据的两种方式
Provider.of

第一种方式就是上面的代码采用的 Provider 的 of 静态方法,通过设置的类型 T,Provider 会使用 BuilderContext 找到最近的父类 T 从而获取到数据。

Consumer

第二种方式就是通过 Consumer:

class CounterProviderSimplePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<CounterModel>(
      builder: (context,counterModel,_){
        return Container(
          child: Center(
            child: Text("counter: ${counterModel.count}"),
          ),
        );
      },
    );
  }
}

Consumer 需要传入一个 builder:

  /// context: 上下文
  /// value:所监听的 ChangeNotifier 类型
  /// child:不跟随 T 变化而重新 build 的 child Widget
  final Widget Function(BuildContext context, T value, Widget child) builder;

两种方式的区别

其实两种方式区别不大,因为 Consumer 内部也是采用的 Provider.of :

class Consumer<T> extends SingleChildStatelessWidget {
  ...
  final Widget Function(BuildContext context, T value, Widget child) builder;

  @override
  Widget buildWithChild(BuildContext context, Widget child) {
    return builder(
      context,
      Provider.of<T>(context),
      child,
    );
  }
}

之所以提供 Consumer 是为了让粒度更细从而提高性能,比如:

  @override
  Widget build(BuildContext context) {
   return FooWidget(
     child: BarWidget(
       bar: Provider.of<Bar>(context),
     ),
    );
  }

我们只想刷新 BarWidget,但是会造成 FooWidget 不必要的刷新,接下来我们使用 Consumer 来细化粒度:

@override
Widget build(BuildContext context) {
  return FooWidget(
    child: Consumer<Bar>(
      builder: (_, bar, __) => BarWidget(bar: bar),
    ),
  );
}

这样就不会造成 FooWidget 不必要的刷新了。

MultiProvider

当一个复杂的页面有个多个数据源的时候,为了提高刷新效率,我们可能设置多个 ChangeNotifier,这时候就需要用到 MultiProvider:

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
        providers: [
          ChangeNotifierProvider<RedCounterModel>(
            create: (_) => RedCounterModel(),
        ),
          ChangeNotifierProvider<BlueCounterModel>(
            create: (_) => BlueCounterModel(),
        ),
    ], 
        child: CounterProviderSimplePage()
    );
  }
}

MultiProvider 只是让代码更加易读而已,上面的代码等价于:

Widget build(BuildContext context) {
  return ChangeNotifierProvider<RedCounterModel>(
          create: (_) => RedCounterModel(),
          child: ChangeNotifierProvider<BlueCounterModel>(
            create: (_) => BlueCounterModel(),
            child: CounterProviderSimplePage(),
          ),
        );
}

所以 ChangeNotifierProvider 其实是根据我们声明的顺序嵌套了,这一点可以在 FlutterInspect 中验证:

Selector

我们可以通过多个 ChangeNotifier + Consumer 来细分粒度,提高刷新性能,但是多个 ChangeNotifier 会有嵌套问题,这个时候如果想用一个 ChangeNotifier 来控制不同区域进行刷新就可以使用 Selector,两者的区别如下图:

我们将用一个有两个 Counter 的 demo 来讲解一下 Selector 的使用,首先我们先不用 Selector 实现一下:

Non-Selector DoubleCounter
  • Model
class DoubleCounterModel with ChangeNotifier {
  var countRed = 0;
  var countBlue = 0;

  void increaseRed() {
    countRed++;
    notifyListeners();
  }

  void increaseBlue() {
    countBlue++;
    notifyListeners();
  }
}

DoubleCounterModel 的 countRed 用来控制 RedCounter, countBlue 用来控制 BlueCounter。

  • Widget 然后声明 RedCounter 和 BlueCounter
class RedCounter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<DoubleCounterModel>(
      builder: (context, counterModel, child) {
        print("red counter build");
        return Row(
          children: <Widget>[
            Container(
              height: 50,
              width: 200,
              color: Colors.red,
              child: Center(child: Text("counter: ${counterModel.countRed}")),
            ),
            child
          ],
        );
      },
      child: FloatingActionButton(
        backgroundColor: Colors.red,
        onPressed: () {
          Provider.of<DoubleCounterModel>(context, listen: false).increaseRed();
        },
        child: Icon(Icons.add),
      ),
    );
  }
}
class BlueCounter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<DoubleCounterModel>(
      builder: (context, counterModel, child) {
        print("blue counter build");
        return Row(
          children: <Widget>[
            Container(
              height: 50,
              width: 200,
              color: Colors.blue,
              child: Center(child: Text("counter: ${counterModel.countBlue}")),
            ),
            child
          ],
        );
      },
      child: FloatingActionButton(
        backgroundColor: Colors.blue,
        onPressed: () {
          Provider.of<DoubleCounterModel>(context, listen: false).increaseBlue();
        },
        child: Icon(Icons.add),
      ),
    );
  }
}
  • 使用
void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  ...
}

class MyHomePage extends StatefulWidget {
  ...
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(providers: [
      ChangeNotifierProvider<DoubleCounterModel>(
          create: (_) => DoubleCounterModel())
    ], child: DoubleCounterPage());
  }
}
class DoubleCounterPage extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          RedCounter(),
          BlueCounter()
        ],
      ),
    );
  }
}
  • 效果 此时我们发现虽然只修改了 BlueCounter 但是 RedCounter 也会重新 build,这不是最佳的刷新策略,那么让我们来看一下如何用 Seletor 提高刷新效率

Selector

Selector 的构造主要包含以下四个参数:

 Selector<A,S>(
        selector: S Function(BuildContext, A),
        shouldRebuild: bool Function(T previous, T next),
        builder: Widget Function(BuildContext context, T value, Widget child)
        child:Widget
    )
  • A 代表传入的数据源,比如 DoubleCounterModel, S 代表想要监听的 A 的某个属性,比如 DoubleCounterModel 的 countRed
  • builder 和 child 的用法同 Consumer 一样
  • selector 负责从 A 中筛选出我们需要监听的 S,然后将 S 传给 builder 进行刷新
  • shouldRebuild 用来覆盖默认的对比算法,可以不设置 比较数据是否发生改变的源码如下:
class _Selector0State<T> extends SingleChildState<Selector0<T>> {
  T value;
  Widget cache;
  Widget oldWidget;

  @override
  Widget buildWithChild(BuildContext context, Widget child) {
    final selected = widget.selector(context);

    var shouldInvalidateCache = oldWidget != widget ||
    /// 如果 shouldRebuild 不为空就是用 shouldRebuild
        (widget._shouldRebuild != null && widget._shouldRebuild.call(value, selected)) ||
    /// 如果 shouldRebuild 就使用默认的方法比较  
        (widget._shouldRebuild == null && !const DeepCollectionEquality().equals(value, selected));
    /// 如果数据发生了改变就 rebuild    
    if (shouldInvalidateCache) {
      value = selected;
      oldWidget = widget;
      cache = widget.builder(
        context,
        selected,
        child,
      );
    }
    /// 如果数据没有发生改变就返回旧的 widget
    return cache;
  }
}
Selector DoubleCounter

明白了 Selector 的定义使用起来就非常简单,只需要通过 selector 方法筛选出感兴趣的数据即可:

  • 声明 Selector
class RedCounter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Selector<DoubleCounterModel, int>(
      builder: (context, count, child) {
        print("red counter build");
        return Row(
          children: <Widget>[
            Container(
              height: 50,
              width: 200,
              color: Colors.red,
              child: Center(child: Text("counter: $count")),
            ),
            child
          ],
        );
      },
      child: FloatingActionButton(
        backgroundColor: Colors.red,
        onPressed: () {
          Provider.of<DoubleCounterModel>(context, listen: false).increaseRed();
        },
        child: Icon(Icons.add),
      ),
      selector: (context, counterModel) {
        return counterModel.countRed;
      },
    );
  }
}

class BlueCounter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Selector<DoubleCounterModel, int>(
      builder: (context, count, child) {
        print("blue counter build");
        return Row(
          children: <Widget>[
            Container(
              height: 50,
              width: 200,
              color: Colors.blue,
              child: Center(child: Text("counter: $count}")),
            ),
            child
          ],
        );
      },
      child: FloatingActionButton(
        backgroundColor: Colors.red,
        onPressed: () {
          Provider.of<DoubleCounterModel>(context, listen: false).increaseBlue();
        },
        child: Icon(Icons.add),
      ),
      selector: (context, counterModel) {
        return counterModel.countBlue;
      },
    );
  }
}
  • 效果 此时再看下效果就可以发现两个 Widget 不再相互影响了:

更轻量级的 select

Provider 在 4.0.0 添加了和 Consumer 同等级的 Selector,又在 4.1.0 添加了 select 方法,它作用类似于 Selector,但是更加轻量级。 可以参考Provider 的 changeLog。 With Selector:

Widget build(BuildContext context) {
  return Selector<Person, String>(
    selector: (_, p) => p.name,
    builder: (_, name, __) {
      return Text(name);
    },
  ),
}

VS with the new select extension:

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

需要注意的是 4.10.0 要求 Flutter SDK 的版本>=1.15.17。

为项目添加 Provider

在 pubspec.yaml 添加依赖即可:

dependencies:
  flutter:
    sdk: flutter
  provider: ^4.0.0

多类型的 Consumer/Selector

上面所讲的 Consumer,Selector<A,S> 都只能监听一种类型的数据,官方提供了可以监听多种类型数据的类,比如:

  • Consumer2<A, B>,Consumer3<A, B, C> ...
  • Selector2<A, B, S>,Selector3<A, B, C, S> ...

其他

官方文档上还有很多规范,比如 ChangeNotifierProvider 添加 notifier 的两种方式:

/// 添加新的 Model 使用 create
ChangeNotifierProvider(
  create: (_) => MyModel(),
  child: ...
)
/// 添加已经存在的 Model 使用 value
MyModel model = MyModel();
ChangeNotifierProvider.value(
  value: model,
  child: ...
)

其他注意点请参考 github.com/rrousselGit…

参考

Flutter | 状态管理指南篇——Provider

官方文档

API Reference