flutter学习-状态State管理

Posted GY-93

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了flutter学习-状态State管理相关的知识,希望对你有一定的参考价值。

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

这里我们就来学习下声明式编程的状态管理

1. 为什么需要状态管理

1.1 认识状态管理

很多从命令式编程框架(androidios原生开发者)转成声明式编程(Flutter、Vue、React等)刚开始并不是适应,因为需要换一个角度来考虑APP的开发模式。

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

上图是Flutter应用的构建过程

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

1.2 不同的状态管理分类

1.2.1 短时状态(Ephemeral state)

某些状态只需要在自己的widget中使用即可

  • 比如我们之前做的简单的计数器案例(counter)
  • 比如一个PageView组件记录当前的页面
  • 比如一个动画记录当前的进度
  • 比如一个BottomNavigationBar中当前被选中的tab

这种状态只需要我们使用StatefulWidget对应的State类自己管理即可,Widget树中其它部分并不需要访问这个状态。

这种方式在之前的学习中,我们已经使用很多次,相信我们已经很熟悉了。

1.2.2 应用状态App State

开发中也有非常多的状态需要再多个地方进行共享

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

这种状态我们如果在Widget之间传递来、传递去、那是无穷尽的,并且代码的耦合度会变的非常高,牵一发而动全身,无论是编写代码质量、后期维护、可扩展性都非常差。

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

1.2.3如何选择不同的管理方式

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

  • 某些短时状态可能之后的开发维护中升级成应用状态

但是我们可以遵守下面这幅图的流程:

关于我们选择哪种管理状态方式更好,采取原则:选择能够减少麻烦的方式

2. 共享状态

2.1 InheritedWidget

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

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

//创建一个数据共享模型
class GYInheritedWidget extends InheritedWidget {
  //创建一个需要共享的数据,这里我们可以写一个变量通过外界传递进来, 也可以直接写死一个共享数据
  final int counter;

  //2.定义构造方法
  GYInheritedWidget({required this.counter,  required Widget child}) : super(child: child);

  //3.我们习惯定义一个of方法来获取这个对象: 获取组件最近的当前的InheritedWidget对象
  static GYInheritedWidget? of(BuildContext context) {
    //该方法的作用:沿着Element树,去找最近的GYInheritedWidget,从Element中取出Widget对象
    return context.dependOnInheritedWidgetOfExactType();
  }

  //4。重写父类的抽象方法,该方法的作用是决定要不要调用State中didChangeDependencies方法(前面学习声明周期方法时学习过)
  // true: 执行当前依赖InheritedWidget的State中的didChangeDependencies方法;false:表示不调用该方法
  @override
  bool updateShouldNotify(GYInheritedWidget oldWidget) {
    // TODO: implement updateShouldNotify 
    return oldWidget.counter != counter;
  }
}
  • 这里定义一个of方法方法,该方法是通过context开始查找祖先GYInheritedWidget(可以查看源码查找过程)
@override
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies!.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }

  @override
  T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
    if (ancestor != null) {
      return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }
  • updateShouldNotify方法是对比新旧GYInheritedWidget,是否需要对更新相关依赖的Widget

完整示例代码:



import 'dart:math';

import 'package:flutter/material.dart';//导入对应的库
import 'package:hello_flutter/http/http_dio_tools.dart';

//main函数作为程序的入口
void main() => runApp(MyApp());

class MyApp extends StatefulWidget {

  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  int _counter = 100;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      //如何使用共享数据模型: 需要再共享的widget的外层,使用共享数据模型包裹
        home: Scaffold(
          body: GYInheritedWidget(
            counter: _counter,
            child: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  GYShowData01(),
                  GYShowData02()
                ],
              ),
            ),
          ),
          floatingActionButton: FloatingActionButton(
            child: Icon(Icons.add),
            onPressed: (){
              setState(() {
                _counter ++;
              });
            },
          ),
        )
    );
  }
}

class GYShowData01 extends StatefulWidget {
  @override
  _GYShowData01State createState() => _GYShowData01State();
}

class _GYShowData01State extends State<GYShowData01> {


  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print("执行了_GYShowData01State中的didChangeDependencies");
  }

  @override
  Widget build(BuildContext context) {
    GYInheritedWidget? inheritedWidget = GYInheritedWidget.of(context);
    int counter = 0;
    if (inheritedWidget != null) {
        counter = inheritedWidget.counter;
    }

    return Card(
      color: Colors.red,
      child: Text("当前计数: $counter", style: TextStyle(fontSize: 30),),
    );
  }
}

class GYShowData02 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    GYInheritedWidget? inheritedWidget = GYInheritedWidget.of(context);
    int counter = 0;
    if (inheritedWidget != null) {
      counter = inheritedWidget.counter;
    }

    return Container(
      color: Colors.blue,
      child: Text("当前计数: $counter", style: TextStyle(fontSize: 30),),
    );
  }
}

2.2 Provider

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

在使用之前,我们需要先引入对它的依赖,截止目前,Provider的版本为6.0.0

2.2.1 Provider的基本使用

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

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

我们用一个简单的计数器案例,来使用Provider来实现

第一步:创建自己的ChangeNotifier

我们需要一个ChangeNotifier来保存我们的状态,所以创建它

  • 这里我们可以使用继承自ChangeNotifier,也使用使用with关键字进行混入,这取决去是否需要继承自其它的类
  • 我们使用一个私有的_counter并且提供了getter和setter
  • 在setter方法中我们监听到_counter改变,就调用notifiyListeners方法,通知所有的Consumer更新
//1.创建ChangeNotifier 来保存共享数据
//class GYDataChangerNotifier with ChangeNotifier
class GYDataChangerNotifier extends ChangeNotifier {
  int _counter = 100;

  int get counter => _counter;

  set counter(int value) {
    _counter = value;
    //当修改共享数据时,通知监听者
    notifyListeners();
  }
}

第二步:在Widget tree中插入ChangeNotifierProvider

我们需要再Widget tree中插入ChangeNotifierProvider,以便Consumer可以获取到数据

  • 将ChangeNotifierProvider放到了顶层,这样方便在整个应用的任何地方可以使用CounterProvider
void main() {
  runApp(
    //使用ChangeNotifierProvider作为顶层,不管在那里都可以访问这个共享数据
    ChangeNotifierProvider(create: (ctx) {
      return GYDataChangerNotifier();
    }, child: MyApp(),)
  );
}

第三步:在需要使用共享数据的地方访问数据(显示数据)

class GYShowData01 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //访问共享数据 static T of<T>(BuildContext context, {bool listen = true}) 
    int counter = Provider.of<GYDataChangerNotifier>(context).counter;
    return Container(
      color: Colors.blue,
      child: Text("当前计数: $counter", style: TextStyle(fontSize: 30),),
    );
  }
}

class GYShowData02 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //访问共享数据 static T of<T>(BuildContext context, {bool listen = true})
    int counter = Provider.of<GYDataChangerNotifier>(context).counter;
    return Container(
      color: Colors.blue,
      child: Text("当前计数: $counter", style: TextStyle(fontSize: 30),),
    );
  }
}

** 第四部:点击加好,需要修改数据(状态)**

  • 在floatingActionButton中使用Consumer,当点击按钮时,修改CounterNotifier中的counter数据;
class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
          body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  GYShowData01(),
                  GYShowData02()
                ],
              ),
            ),
          /*
          Consumer({
          Key? key,
          required this.builder,
          Widget? child,
          }) : super(key: key, child: child);
		//build类型
          final Widget Function(
          BuildContext context,
          T value,
          Widget? child,
           ) builder;
           */
          floatingActionButton: Consumer<GYDataChangerNotifier>(
            builder: (ctx, model,child){
              return FloatingActionButton(
                child: Icon(Icons.add),
                onPressed: (){
                     //修改共享数据
                    model.counter += 1;
                },
              );
            },
          ),
        )
    );
  }
}

Consumer的build方法解析:

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

2.2.2 Provider.of的弊端

事实上,因为Provider是基于inheritedWidget,所以我们在使用ChangeNotifier中的数据时,我们可以通过Provider.of的方式时,例如下面代码:

Text("当前计数: ${Provider.of<GYDataChangerNotifier>(context).counter}

我们可以发现这种方式很简洁,那么在开发中我们是否需要选择这种方式?

  • 答案是否定的,更多的时候我们还是选择Consumer的方式

为什么呢?因为Consumer在刷新整个Widget树时,会尽可能少的rebuild Widget。

来看下Provider.ofConsumer的代码结果比较:

class GYShowData01 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("GYShowData01   build方法执行");
    //访问共享数据 static T of<T>(BuildContext context, {bool listen = true})
    //int counter = Provider.of<GYDataChangerNotifier>(context).counter;
    return Container(
      color: Colors.blue,
      child: Text("当前计数: ${Provider.of<GYDataChangerNotifier>(context).counter}", style: TextStyle(fontSize: 30),),
    );
  }
}

class GYShowData02 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("GYShowData02   build方法执行");
    //访问共享数据 static T of<T>(BuildContext context, {bool listen = true})
    //int counter = Provider.of<GYDataChangerNotifier>(context).counter;
    return Container(
      color: Colors.blue,
      child: Consumer<GYDataChangerNotifier>(builder: (ctx, model, child){
        return Text("当前计数: ${model.counter}", style: TextStyle(fontSize: 30));
      })
    );
  }
}

根据结果比较可以得出如下结论:

  • Provider.of: 当Provider中的数据发生改变时, Provider.of所在的Widget整个build方法都会重新构建
  • Consumer:整个Widget的build方法不会执行,只会执行Consumer的build方法,这样就减少了不必要的build方法的执行,性能更高

2.2.3 Selector的选择

Consumer是否是最好的选择? 并不是,consumer也会存在弊端

  • 比如上述代码点击了floatingActionButton时,我们发现floatingActionButton位置重新build了,但是该位置有从新build的必要吗? 没有,因为它只是在操作数据,并没有展示
  • 如何做到让它不要重新build?使用Selector来代替Consumer
/*
            Selector({
              Key? key,
              required ValueWidgetBuilder<S> builder,
              required S Function(BuildContext, A) selector,
              ShouldRebuild<S>? shouldRebuild,
              Widget? child,
              })
           */
            floatingActionButton: Selector<GYDataChangerNotifier, GYDataChangerNotifier>(
              builder: (ctx, model, child) {
                print("floatingActionButton  build方法");
                return FloatingActionButton(
                  child: child,
                  onPressed: () {
                    //修改共享数据
                    model.counter += 1;
                  },
                );
              },
              selector: (ctx, modelA) {
                return modelA; //把A转换成S
              },
              shouldRebuild: (pre, next) {
                return false;  //表示需不需要从新构建(build),true:表示需要重新构建,会执行build方法, false则不会
              },
              child: Icon(Icons.add),
            )
  • Selector:
    • selector方法(作用,对原有的数据进行转换)
    • shouldRebuild(作用,要不要重新构建)
  • Selector和Consumer对比,不同之处主要是三个关键点:
    • 关键点一:
      • 泛型参数一:我们这次要使用的Provider
      • 泛型参数二:转换之后的数据类型,比如我这里转换之后依然是使用GYDataChangerNotifier,那么他们两个就是一样的类型
    • 关键点2:selector回调函数
      • 转换的回调函数,你希望如何进行转换
      • S Function(BuildContext, A) selector
      • 我这里没有进行转换,所以直接将A实例返回即可
    • 关键点3:是否希望重新rebuild
      • 这里也是一个回调函数,我们可以拿到转换前后的两个实例
      • bool Function(T previous, T next);
      • 因为这里我不希望它重新rebuild,无论数据如何变化,所以这里我直接return false

这个时候我们重新运行程序,点击按钮,发现floatingActionButton中的代码并不会重新的build,所以在某些时候我们可以使用Selector来代替Consumer

2.2.4 MultiProvider

在开发中,我们需要共享的数据肯定不止一个,并且数据之间我们需要组织到一起,所以一个Provider必然是不够的。

那在开发中我们有多个Provider需要提供应该怎么办?

这里我就不创建新的Provider了,直接使用上面的Provider来演示

方法一:多个Provider之间的嵌套:

void main() {
  runApp(
      //使用ChangeNotifierProvider作为顶层,不管在那里都可以访问这个共享数据
      ChangeNotifierProvider(
    create: (ctx) {
      return GYDataChangerNotifier();
    },
    child: ChangeNotifierProvider(
      create: (ctx) {
        return GYDataChangerNotifier();
      },
      child: MyApp(),
    ),
  ));
}

不过这种做法,有大的弊端,如果嵌套的层级过多,就不方便维护,扩展性也比较差

方法二: 使用MulitProiver

void main() {
  runApp(MultiProvider(providers: [
    ChangeNotifierProvider(create: (ctx) => GYDataChangerNotifier()),
    ChangeNotifierProvider(create: (ctx) => GYDataChangerNotifier()),
    ChangeNotifierProvider(create: (ctx) => GYDataChangerNotifier())
  ]));
}

如果同一个地方需要访问多个不同Provider的数据,那怎么办了?可以嵌套,但是这里Provider给我们提供了一个Consumer2这样一个类,可以同时传入两个数据

相对的还有Consumer3一直到Consumer6

以上是关于flutter学习-状态State管理的主要内容,如果未能解决你的问题,请参考以下文章

Flutter漫说:组件生命周期State状态管理及局部重绘的实现(Inherit)

Flutter 报错 DioError [DioErrorType.DEFAULT]: Bad state: Insecure HTTP is not allowed by platform(代码片段

Flutter Stateful Widget 重新创建 State

Flutter 组件的生命周期State 管理及局部重绘 | 开发者说·DTalk

Flutter 组件的生命周期State 管理及局部重绘 | 开发者说·DTalk

为了弄懂Flutter的状态管理, 我用10种方法改造了counter app