Flutter

Flutter中Provider 的基本使用

2020-03-08  本文已影响0人  二两陈皮_

背景

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。

如何使用

## 1,在pubspe.yaml中添加依赖 
##2,执行flutter packages get
## 3,在需要使用的文件中导包import 'package:provider/provider.dart'
## 4,创建数据共享类
这里的数据共享类实际上就是我们的状态,它不仅储存了我们的数据模型,而且还包含了更改数据的方法,并暴露出它想要暴露出的数据。
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 来更改状态。

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 组件中进行使用。

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)。
context: context 就是 build 方法传进来的 BuildContext 。
T:T也很简单,就是获取到的最近一个祖先节点中的数据模型。
child:它用来构建那些与 Model 无关的部分,在多次运行 builder 中,child 不会进行重建。
然后它会返回一个通过这三个参数映射的 Widget 用于构建自身。
这里讲的 Consumer 获取的只有一个范型<T>数据,如果获取两个数据,则可以使用Consumer2<A,B>。使用方式基本上和 Consumer<T>一致,只不过范型改为了两个,并且 builder 方法也变成了 Function(BuildContext context, A value, B value2, Widget child)。
从源码里面可以看到,作者只为提供到了 Consumer6。如果还要要求更多就只有合并数据源了。
两者区别
先看下 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 则没有。
在 MyHeader 中的 build 方法中添加一个LogUtil.v("WeatherInfoPage MyHeader build");
然后在 MyContent 中的 build 方法中添加一个 LogUtil.v("WeatherInfoPage MyContent build");
点击MyFloatingActionButton浮动按钮,那么会在控制台看到这句输出WeatherInfoPage MyHeader build。

这证明了 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;
}

d等价于

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

总结

使用Provider.of(context),简单易用,但是要数据发生变化时,会进行页面级别rebuild,相当于StatefulWidget,此方法将从BuildContext关联的小部件树中查找,它将返回找到的最近的类型变量T

Provider.of<T>( BuildContext context,
{bool listen = true}//listen:默认true监听状态变化,false为不监听状态改变
)

也可以使用Consumer组件获取,Consumer可用在没有context的地方,还可以优化性能


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

状态管理包裹在MaterialApp()外面作用域是全局,其他作用域在本页面或本页的子Widget中
MultiProvider()可以提供多个状态
ChangeNotifierProvider.value()与ChangeNotifierProvider()区别是ChangeNotifierProvider()通过构造器绑定数据并进行监听,当从 Widget Tree 中删除时, 会自动调用dispose 进行销毁释放资源,在需要使用多个状态值时可以使用ChangeNotifierProvider
无论使用那种 .value 方式,均建议在 dispose 中进行 listener 的关闭;
状态管理包裹在MaterialApp()外面作用域是全局,其他作用域在本页面或本页的子Widget中
MultiProvider()可以提供多个状态
ChangeNotifierProvider.value()与ChangeNotifierProvider()区别是ChangeNotifierProvider()通过构造器绑定数据并进行监听,当从 Widget Tree 中删除时, 会自动调用dispose 进行销毁释放资源,在需要使用多个状态值时可以使用ChangeNotifierProvider
无论使用那种 .value 方式,均建议在 dispose 中进行 listener 的关闭;

@override
void dispose() {
    super.dispose();
    _homepageNotifier.dispose();
}
上一篇下一篇

猜你喜欢

热点阅读