flutter

Flutter 状态管理 Provider 使用小结

2020-03-17  本文已影响0人  Cheney2006

背景

  2019 Google I/O 大会上重磅消息出了支持 flutter_web 之外,另一个便是弃用之前的状态管理 Provide,转而推荐相似的库 Provider;虽然只有一个字母之差使用方式差别却很大,自此,Provider 代替 Provide 成为官方推荐的状态管理方式之一。

  Provider也是借助了InheritedWidget,它允许在小部件树中传递数据,允许我们更加灵活地处理数据类型和数据。

为什么需要状态管理

  在进行项目的开发时,往往需要管理不同页面之间的数据共享,在页面功能复杂,状态可能会达到几十个甚至上百个,这时将难以清楚的维护这些数据状态。而且还会有多个页面共享同一个状态,例如当你进入一个文章点赞,退出到外部缩略展示的时候,外部也需要显示点赞数,这时候就需要同步这两个状态。

  Flutter 实际上在一开始就提供了一种状态管理方式 — StatefulWidget。然而发现它仅适合用于在单个 Widget 内部维护其状态。当需要使用跨组件的状态时,StatefulWidget 将不再是一个好的选择。在StatefulWidget中的 State 属于某一个特定的 Widget,在多个 Widget 之间进行通信的时候,虽然可以使用 callback 解决,但当嵌套足够深的话,很容易增大代码的耦合度。

  这时候,就迫切的需要一个架构来帮助理清这些关系,状态管理框架应运而生。

为什么选择Provider

  Provider 从名字上就很容易理解,它就是用于提供数据,无论是在单个页面还是在整个 app 都有它自己的解决方案,我们可以很方便的管理状态,并在合适的时机释放资源。可以说,Provider 的目标就是来替代StatefulWidget。

如何使用

添加依赖

170dd382915684b2.jpg

执行

flutter packages get

在需要使用的页面引入

import 'package:provider/provider.dart'

创建数据共享类

这里的数据共享类实际上就是我们的状态,它不仅储存了我们的数据模型,而且还包含了更改数据的方法,并暴露出它想要暴露出的数据。

import 'package:flutter/material.dart’;

class WeatherInfo with ChangeNotifier {
  String _tempType = “celcius”;
  int _temperatureVal = 25;

  String get temperatureType => _tempType;

  set temperatureType(String value) {
    _tempType = value;
    notifyListeners();
  }

  int get temperatureVal => _temperatureVal;

  set temperatureVal(int value) {
    _temperatureVal = value;
    notifyListeners();
  }
}

这个类意图很清晰,数据就是一个 int 类型的 _temperatureVal、string 类型的_tempType,下划线代表私有。通过 get value 把 _temperatureVal 、_tempType值暴露出来。并提供 set 方法用于更改数据。

这里使用了 mixin 混入了 ChangeNotifier,这个类能够帮助我们自动管理所有听众。

当调用 notifyListeners() 时,它会通知所有听众进行刷新。

提供数据共享

在 build 方法提供初始化数据

class WeatherInfoPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    LogUtil.v("WeatherInfoPage build”);
    return ChangeNotifierProvider<WeatherInfo>(
      builder: (context) => WeatherInfo(),
      child: Scaffold(
        appBar: AppBar(
          title: Text("Provider demo${Random().nextInt(100)}”),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Padding(
                padding: EdgeInsets.all(8.0),
                child: Text(
                    "我在ChangeNotifierProvider中,看我变不变${Random().nextInt(100)}”),
              ),
              MyHeader(),
              MyHeaderListenerFalse(),
              MyContent(),
              Padding(
                padding: EdgeInsets.all(8.0),
                child: RaisedButton(
                  onPressed: () {
                    Navigator.push(context,
                        MaterialPageRoute(builder: (context) {
                      return WeatherInfoPage2();
                    }));
                  },
                  child: Text("consumer 例子”),
                ),
              ),
            ],
          ),
        ),
        floatingActionButton: MyFloatingActionButton(),
      ),
    );
   
  }
}

ChangeNotifierProvider不仅能够提供数据供子孙节点使用,还可以在数据改变的时候通知所有听众刷新。(通过之前我们说过的 notifyListeners)

此处的 <T>范型可省略。但是建议大家还是进行声明,这会使你的应用更加健壮。

ChangeNotifierProvider 绑定数据有两种方式:

ChangeNotifierProvider({Key key, @required ValueBuilder builder, Widget child })
ChangeNotifierProvider.value({Key key, @required T notifier, Widget child })

使用基本差不多,区别在于Provider()提供dispose参数,可以传递一个方法销毁的时候被调用,方便StatelessWidget释放资源,并且通过构造器绑定数据并进行监听,当从 Widget Tree 中删除时, 会自动调用dispose 进行销毁;

获取状态

这里有两个子控件 MyHeader,MyContent获取状态,MyFloatingActionButton 来更改状态。

  1. Provider.of(context)
class MyHeader extends StatelessWidget {
  Color decideColor(WeatherInfo info) {
    return "celcius" == info.temperatureType ? Colors.green : Colors.deepOrange;
  }

  @override
  Widget build(BuildContext context) {
    LogUtil.v("WeatherInfoPage MyHeader build”);

    WeatherInfo weatherInfo = Provider.of<WeatherInfo>(context);

    return Padding(
      padding: EdgeInsets.all(8.0),
      child: Text(
        "我是类中的temperatureType=${weatherInfo.temperatureType}”,
        style: TextStyle(
          color: decideColor(weatherInfo),
          fontSize: 20,
        ),
      ),
    );
  }
}

获取顶层数据最简单的方法就是 Provider.of<T>(context);这里的范型<T>指定了获取 MyHeader向上寻找最近的储存了 T 的祖先节点的数据。通过这个方法获取了顶层的 WeatherInfo。并在 Text 组件中进行使用。

  1. Consumer
class MyContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    LogUtil.v("WeatherInfoPage MyContent build”);

    return Consumer<WeatherInfo>(
      builder: (BuildContext context, WeatherInfo weatherInfo, Widget child) {
        return Padding(
          padding: EdgeInsets.all(8.0),
          child: Text("我是类中的temperatureVal=${weatherInfo.temperatureVal}”),
        );
      },
    );
  }
}


这里获取顶层数据使用Consumer 获取数据。这里的范型 <T>指定了获取 MyContent向上寻找最近的储存了 T 的祖先节点的数据。

Consumer 使用了 Builder模式,收到更新通知就会通过 builder 重新构建。Consumer<T>代表了它要获取哪一个祖先中的 Model。
Consumer 的 builder 实际上就是一个 Function,它接收三个参数 (BuildContext context, T model, Widget child)。

然后它会返回一个通过这三个参数映射的 Widget 用于构建自身。
这里讲的 Consumer 获取的只有一个范型<T>数据,如果获取两个数据,则可以使用Consumer2<A,B>。使用方式基本上和 Consumer<T>一致,只不过范型改为了两个,并且 builder 方法也变成了 Function(BuildContext context, A value, B value2, Widget child)。

从源码里面可以看到,作者只为提供到了 Consumer6。如果还要要求更多就只有合并数据源了。

  1. 两者区别

先看下 Consumer 内部实现

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

可以发现,Consumer就是通过Provider.of<T>(context)来实现的。但是从实现来讲Provider.of<T>(context)比 Consumer简单好用太多,为什么要搞得那么复杂呢?

实际上 Consumer非常有用,它的经典之处在于能够在复杂项目中,极大地缩小你的控件刷新范围。Provider.of<T>(context)将会把调用了该方法的context作为听众,并在notifyListeners的时候通知其刷新。

举个例子来说,我们在 MyHeader使用了Provider.of<T>(context)来获取数据,MyContent 则没有。

这证明了 Provider.of<T>(context)会导致调用的 context 页面范围的刷新。

那么MyContent页面刷新没有呢? 刷新了,但是只刷新了 Consumer的部分。

假如在应用的页面级别的Widget中,使用了Provider.of<T>(context)。会导致什么后果已经显而易见了,每当其状态改变的时候,都会重新刷新整个页面。虽然有Flutter的自动优化算法给你撑腰,但肯定无法获得最好的性能。

所以在这里建议各位尽量使用 Consumer而不是 Provider.of<T>(context)获取顶层数据。

多个 Provider

从源码中可以看出,作者提供了 MultiProvider 供提供多种状态使用。

MultiProvider(
    providers:[
        AProvider,
        BProvider,
        CProvider,
    ],
    child: child,
)

MultiProvider 实际上就是通过每一个 provider 都实现了的 cloneWithChild 方法,用循环把自己一层一层包裹起来。

@override
Widget build(BuildContext context) {
  var tree = child;
  for (final provider in providers.reversed) {
    tree = provider.cloneWithChild(tree);
  }
  return tree;
}

等价于

AProvider(
    child: BProvider(
        child: CProvider(
            child: child,
        ),
    ),
)

总结

Consumer<T>({
@required this.builder,//这边写布局
this.child,//可以控制刷新性能优化,当数据数据发生改变,不会重新build,
})
@override
void dispose() {
    super.dispose();
    _homepageNotifier.dispose();
}

最后

  如果在使用过程遇到问题,欢迎下方留言交流。

学习资料

请大家不吝点赞!因为您的点赞是对我最大的鼓励,谢谢!

上一篇下一篇

猜你喜欢

热点阅读