Flutter状态管理

Flutter状态管理

状态管理是声明式编程很是重要的一个概念,咱们在前面介绍过Flutter是声明式编程的,也区分声明式编程和命令式编程的区别。vue

这里,咱们就来系统的学习一下Flutter声明式编程中很是重要的状态管理git

一. 为何须要状态管理?

1.1. 认识状态管理

不少从命令式编程框架(Android或iOS原生开发者)转成声明式编程(Flutter、Vue、React等)刚开始并不适应,由于须要一个新的角度来考虑APP的开发模式。github

Flutter做为一个现代的框架,是声明式编程的:web

Flutter构建应用过程
Flutter构建应用过程

在编写一个应用的过程当中,咱们有大量的State须要来进行管理,而正是对这些State的改变,来更新界面的刷新:算法

状态管理流程
状态管理流程

1.2. 不一样状态管理分类

1.2.1. 短时状态Ephemeral state

某些状态只须要在本身的Widget中使用便可编程

  • 好比咱们以前作的简单计数器counter
  • 好比一个PageView组件记录当前的页面
  • 好比一个动画记录当前的进度
  • 好比一个BottomNavigationBar中当前被选中的tab

这种状态咱们只须要使用StatefulWidget对应的State类本身管理便可,Widget树中的其它部分并不须要访问这个状态。redux

这种方式在以前的学习中,咱们已经应用过很是屡次了。数据结构

1.2.2. 应用状态App state

开发中也有很是多的状态须要在多个部分进行共享app

  • 好比用户一个个性化选项
  • 好比用户的登陆状态信息
  • 好比一个电商应用的购物车
  • 好比一个新闻应用的已读消息或者未读消息

这种状态咱们若是在Widget之间传递来、传递去,那么是无穷尽的,而且代码的耦合度会变得很是高,牵一发而动全身,不管是代码编写质量、后期维护、可扩展性都很是差。框架

这个时候咱们能够选择全局状态管理的方式,来对状态进行统一的管理和应用。

1.2.3. 如何选择不一样的管理方式

开发中,没有明确的规则去区分哪些状态是短时状态,哪些状态是应用状态。

  • 某些短时状态可能在以后的开发维护中须要升级为应用状态。

可是咱们能够简单遵照下面这幅流程图的规则:

状态管理选择
状态管理选择

针对React使用setState仍是Redux中的Store来管理状态哪一个更好的问题,Redux的issue上,Redux的做者Dan Abramov,它这样回答的:

The rule of thumb is: Do whatever is less awkward

经验原则就是:选择可以减小麻烦的方式。

选择可以减小麻烦的方式
选择可以减小麻烦的方式

二. 共享状态管理

2.1. InheritedWidget

InheritedWidget和React中的context功能相似,能够实现跨组件数据的传递。

定义一个共享数据的InheritedWidget,须要继承自InheritedWidget

  • 这里定义了一个of方法,该方法经过context开始去查找祖先的HYDataWidget(能够查看源码查找过程)
  • updateShouldNotify方法是对比新旧HYDataWidget,是否须要对更新相关依赖的Widget
class HYDataWidget extends InheritedWidget {
 final int counter;   HYDataWidget({this.counter, Widget child}): super(child: child);   static HYDataWidget of(BuildContext context) {  return context.dependOnInheritedWidgetOfExactType();  }   @override  bool updateShouldNotify(HYDataWidget oldWidget) {  return this.counter != oldWidget.counter;  } } 复制代码

建立HYDataWidget,而且传入数据(这里点击按钮会修改数据,而且从新build)

class HYHomePage extends StatefulWidget {
 @override  _HYHomePageState createState() => _HYHomePageState(); }  class _HYHomePageState extends State<HYHomePage> {  int data = 100;   @override  Widget build(BuildContext context) {  return Scaffold(  appBar: AppBar(  title: Text("InheritedWidget"),  ),  body: HYDataWidget(  counter: data,  child: Center(  child: Column(  mainAxisAlignment: MainAxisAlignment.center,  children: <Widget>[  HYShowData()  ],  ),  ),  ),  floatingActionButton: FloatingActionButton(  child: Icon(Icons.add),  onPressed: () {  setState(() {  data++;  });  },  ),  );  } } 复制代码

在某个Widget中使用共享的数据,而且监听

2.2. Provider

Provider是目前官方推荐的全局状态管理工具,由社区做者Remi Rousselet 和 Flutter Team共同编写。

使用以前,咱们须要先引入对它的依赖,截止这篇文章,Provider的最新版本为4.0.4

dependencies:
 provider: ^4.0.4 复制代码

2.2.1. Provider的基本使用

在使用Provider的时候,咱们主要关心三个概念:

  • ChangeNotifier:真正数据(状态)存放的地方
  • ChangeNotifierProvider:Widget树中提供数据(状态)的地方,会在其中建立对应的ChangeNotifier
  • Consumer:Widget树中须要使用数据(状态)的地方

咱们先来完成一个简单的案例,将官方计数器案例使用Provider来实现:

第一步:建立本身的ChangeNotifier

咱们须要一个ChangeNotifier来保存咱们的状态,因此建立它

  • 这里咱们可使用继承自ChangeNotifier,也可使用混入,这取决于几率是否须要继承自其它的类
  • 咱们使用一个私有的_counter,而且提供了getter和setter
  • 在setter中咱们监听到_counter的改变,就调用notifyListeners方法,通知全部的Consumer进行更新
class CounterProvider extends ChangeNotifier {
 int _counter = 100;  int get counter {  return _counter;  }  set counter(int value) {  _counter = value;  notifyListeners();  } } 复制代码

第二步:在Widget Tree中插入ChangeNotifierProvider

咱们须要在Widget Tree中插入ChangeNotifierProvider,以便Consumer能够获取到数据:

  • 将ChangeNotifierProvider放到了顶层,这样方便在整个应用的任何地方可使用CounterProvider
void main() {
 runApp(ChangeNotifierProvider(  create: (context) => CounterProvider(),  child: MyApp(),  )); } 复制代码

第三步:在首页中使用Consumer引入和修改状态

  • 引入位置一:在body中使用Consumer,Consumer须要传入一个builder回调函数,当数据发生变化时,就会通知依赖数据的Consumer从新调用builder方法来构建;
  • 引入位置二:在floatingActionButton中使用Consumer,当点击按钮时,修改CounterNotifier中的counter数据;
class HYHomePage extends StatelessWidget {
 @override  Widget build(BuildContext context) {  return Scaffold(  appBar: AppBar(  title: Text("列表测试"),  ),  body: Center(  child: Consumer<CounterProvider>(  builder: (ctx, counterPro, child) {  return Text("当前计数:${counterPro.counter}", style: TextStyle(fontSize: 20, color: Colors.red),);  }  ),  ),  floatingActionButton: Consumer<CounterProvider>(  builder: (ctx, counterPro, child) {  return FloatingActionButton(  child: child,  onPressed: () {  counterPro.counter += 1;  },  );  },  child: Icon(Icons.add),  ),  );  } } 复制代码

Consumer的builder方法解析:

  • 参数一:context,每一个build方法都会有上下文,目的是知道当前树的位置
  • 参数二:ChangeNotifier对应的实例,也是咱们在builder函数中主要使用的对象
  • 参数三:child,目的是进行优化,若是builder下面有一颗庞大的子树,当模型发生改变的时候,咱们并不但愿从新build这颗子树,那么就能够将这颗子树放到Consumer的child中,在这里直接引入便可(注意我案例中的Icon所放的位置)
案例效果
案例效果

步骤四:建立一个新的页面,在新的页面中修改数据

class SecondPage extends StatelessWidget {
 @override  Widget build(BuildContext context) {  return Scaffold(  appBar: AppBar(  title: Text("第二个页面"),  ),  floatingActionButton: Consumer<CounterProvider>(  builder: (ctx, counterPro, child) {  return FloatingActionButton(  child: child,  onPressed: () {  counterPro.counter += 1;  },  );  },  child: Icon(Icons.add),  ),  );  } } 复制代码
第二个页面修改数据
第二个页面修改数据

2.2.2. Provider.of的弊端

事实上,由于Provider是基于InheritedWidget,因此咱们在使用ChangeNotifier中的数据时,咱们能够经过Provider.of的方式来使用,好比下面的代码:

Text("当前计数:${Provider.of<CounterProvider>(context).counter}",
 style: TextStyle(fontSize: 30, color: Colors.purple), ), 复制代码

咱们会发现很明显上面的代码会更加简洁,那么开发中是否要选择上面这种方式了?

  • 答案是否认的,更多时候咱们仍是要选择Consumer的方式。

为何呢?由于Consumer在刷新整个Widget树时,会尽量少的rebuild Widget。

方式一:Provider.of的方式完整的代码:

  • 当咱们点击了floatingActionButton时,HYHomePage的build方法会被从新调用。
  • 这意味着整个HYHomePage的Widget都须要从新build
class HYHomePage extends StatelessWidget {
 @override  Widget build(BuildContext context) {  print("调用了HYHomePage的build方法");  return Scaffold(  appBar: AppBar(  title: Text("Provider"),  ),  body: Center(  child: Column(  mainAxisAlignment: MainAxisAlignment.center,  children: <Widget>[  Text("当前计数:${Provider.of<CounterProvider>(context).counter}",  style: TextStyle(fontSize: 30, color: Colors.purple),  )  ],  ),  ),  floatingActionButton: Consumer<CounterProvider>(  builder: (ctx, counterPro, child) {  return FloatingActionButton(  child: child,  onPressed: () {  counterPro.counter += 1;  },  );  },  child: Icon(Icons.add),  ),  );  } } 复制代码

方式二:将Text中的内容采用Consumer的方式修改以下:

  • 你会发现HYHomePage的build方法不会被从新调用;
  • 设置若是咱们有对应的child widget,能够采用上面案例中的方式来组织,性能更高;
Consumer<CounterProvider>(builder: (ctx, counterPro, child) {
 print("调用Consumer的builder");  return Text(  "当前计数:${counterPro.counter}",  style: TextStyle(fontSize: 30, color: Colors.red),  ); }), 复制代码

2.2.3. Selector的选择

Consumer是不是最好的选择呢?并非,它也会存在弊端

  • 好比当点击了floatingActionButton时,咱们在代码的两处分别打印它们的builder是否会从新调用;
  • 咱们会发现只要点击了floatingActionButton,两个位置都会被从新builder;
  • 可是floatingActionButton的位置有从新build的必要吗?没有,由于它是否在操做数据,并无展现;
  • 如何能够作到让它不要从新build了?使用Selector来代替Consumer
Select的弊端
Select的弊端

咱们先直接实现代码,在解释其中的含义:

floatingActionButton: Selector<CounterProvider, CounterProvider>(
 selector: (ctx, provider) => provider,  shouldRebuild: (pre, next) => false,  builder: (ctx, counterPro, child) {  print("floatingActionButton展现的位置builder被调用");  return FloatingActionButton(  child: child,  onPressed: () {  counterPro.counter += 1;  },  );  },  child: Icon(Icons.add), ), 复制代码

Selector和Consumer对比,不一样之处主要是三个关键点:

  • 关键点1:泛型参数是两个
    • 泛型参数一:咱们此次要使用的Provider
    • 泛型参数二:转换以后的数据类型,好比我这里转换以后依然是使用CounterProvider,那么他们两个就是同样的类型
  • 关键点2:selector回调函数
    • 转换的回调函数,你但愿如何进行转换
    • S Function(BuildContext, A) selector
    • 我这里没有进行转换,因此直接将A实例返回便可
  • 关键点3:是否但愿从新rebuild
    • 这里也是一个回调函数,咱们能够拿到转换先后的两个实例;
    • bool Function(T previous, T next);
    • 由于这里我不但愿它从新rebuild,不管数据如何变化,因此这里我直接return false;
Selector的使用
Selector的使用

这个时候,咱们从新测试点击floatingActionButton,floatingActionButton中的代码并不会进行rebuild操做。

因此在某些状况下,咱们可使用Selector来代替Consumer,性能会更高。

2.2.4. MultiProvider

在开发中,咱们须要共享的数据确定不止一个,而且数据之间咱们须要组织到一块儿,因此一个Provider必然是不够的。

咱们在增长一个新的ChangeNotifier

import 'package:flutter/material.dart';
 class UserInfo {  String nickname;  int level;   UserInfo(this.nickname, this.level); }  class UserProvider extends ChangeNotifier {  UserInfo _userInfo = UserInfo("why", 18);   set userInfo(UserInfo info) {  _userInfo = info;  notifyListeners();  }   get userInfo {  return _userInfo;  } } 复制代码

若是在开发中咱们有多个Provider须要提供应该怎么作呢?

方式一:多个Provider之间嵌套

  • 这样作有很大的弊端,若是嵌套层级过多不方便维护,扩展性也比较差
runApp(ChangeNotifierProvider(
 create: (context) => CounterProvider(),  child: ChangeNotifierProvider(  create: (context) => UserProvider(),  child: MyApp()  ),  )); 复制代码

方式二:使用MultiProvider

runApp(MultiProvider(
 providers: [  ChangeNotifierProvider(create: (ctx) => CounterProvider()),  ChangeNotifierProvider(create: (ctx) => UserProvider()),  ],  child: MyApp(), )); 复制代码

备注:全部内容首发于公众号,以后除了Flutter也会更新其余技术文章,TypeScript、React、Node、uniapp、mpvue、数据结构与算法等等,也会更新一些本身的学习心得等,欢迎你们关注

公众号
公众号
相关文章
相关标签/搜索