Flutter 状态管理解惑(1)基础

334 阅读4分钟

概述

本篇,将从UI元素操作演进的过程,慢慢过渡到Flutter框架的UI刷新方式,最后通过手写Flutter Provider的方式,解决关于Flutter状态管理的疑惑。

UI元素操作方式的演进

android:

findViewById(R.id.text).setText("xxxx")

javascript:

getElementById("elementName").text = ""

以上是安卓和 js下的UI元素操作方式,这属于是常规UI框架上传统的UI操作方式。相当于是 通过拿到元素对象之后,再去对其中的某些属性进行修改。类似“遥控器”,要操作某个元素,必须先拿到它的遥控器,也可以称之为“句柄”。

在安卓的MVC开发模式中经常见到很多的findViewById, 后来慢慢出现了ButterKnife, ViewBinding这些概念,不过都没有脱离遥控器模式的范畴,后来的, DataBinding 视图和元素实现了双向绑定,形成了 MVVM的开发模式,不再是遥控器模式,而是直接通过操作数据本身,就能影响到视图,而通过操作视图也能影响到数据,数据和视图实现了一体化。

而同样是谷歌的作品,Flutter在开发模式上进行了框架式的预定义,即 MVVM (改变数据之后setState)的方式 成为主流 ,在某些特殊的场景下,依然可以使用 遥控器(定义key,绑定到widget,然后 key.currentState?.xxxxxx();)的方式 。 只不过此时,MVVM的思维已经彻底成为主流。

SetState的原理

这是一段AndroidStudio自动生成的简单的计数器dart代码:

import 'package:flutter/material.dart';
​
void main() {
  runApp(const MyApp());
}
​
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
​
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
​
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
​
  final String title;
​
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}
​
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
​
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
​
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text(widget.title)),
        body: Center(
            child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
              const Text('You have pushed the button this many times:'),
              Text('$_counter')
            ])),
        floatingActionButton: FloatingActionButton(
          onPressed: _incrementCounter,
          tooltip: 'Increment',
          child: const Icon(Icons.add),
        ));
  }
}

运行起来如下:

image-20230806104935922.png

当 我们需要修改 计数器的数字 时,我们的正确操作方式应该是对_count变量进行更改,然后调用 setState :

setState(() {
      _counter++;
});

之后,_MyHomePageState 的 build 则会重新执行。

容易忽略的是,在dart语言中,new 关键字被省略(但并不是不存在),其实每一个widget名称前头都有一个 new, 如果将上方的_MyHomePageState类补全new之后:

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
​
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
​
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(title: Text(widget.title)),
        body: new Center(
            child: new Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
              const Text('You have pushed the button this many times:'),
              new Text('$_counter')
            ])),
        floatingActionButton: new FloatingActionButton(
          onPressed: _incrementCounter,
          tooltip: 'Increment',
          child: const Icon(Icons.add),
        ));
  }
}

setState的原理,其实就是 重新执行build函数,将视图树结构重新创建。

(可能有人会有疑问,setState之后,并不是所有的widget都实际上去重建了,某些实际上不需要重建的 widget实际上它对应的element还是在重用,并没有造成太多的性能浪费,这里给出解释:确实Element重用了,但是这个只是 flutter框架在底层对代码的性能做出的兜底策略,让开发者即使写出烂代码,也能让app拥有不错的性能,而我们作为开发者,必须有写出优秀代码的追求!

可是这样就导致一个问题,我一个 小小widget的数据变化,居然要引起 整棵 widget树的全部重建,这不合理,很大的资源浪费!

我改变的只是 Text('$_counter')这个widget,但是,我们可以从androidStudio右侧的FlutterPerformance中看到,其他的所有widget都在参与重建。

image-20230806103836556.png

以上有多个widget都参与重绘了52次,而 我们理想中的操作,仅仅是 其中某一个Text被重绘52次而已。

如何解决setState造成的性能浪费?

const 关键字

上图中,icon并没有参与重绘, 观察代码:const Icon(Icons.add)它的创建方式是 const,而其他widget都是 new。

这两者的区别就是,const修饰的 widget会作为常量进行处理,也就是说,在整个widget树进行刷新的时候,它不参与销毁重建,而是进入缓存池,下次构建widget树时还是用同一个对象。

但是const有一定的局限性,被const修饰的 widget,不能接收外部传参。这种情况在我们必须传参构建一个widget的情况下就不推荐。

将 widget的创建过程不放在build内

这样确实可以绕过整棵树的刷新过程,比如:

final w = new Text('$_counter'),然后在 build内部直接用 w.

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
​
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
    
  late final w  = new Text('$_counter')
​
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(title: Text(widget.title)),
        body: new Center(
            child: new Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
              const Text('You have pushed the button this many times:'),
              w  // 外部定义的text
            ])),
        floatingActionButton: new FloatingActionButton(
          onPressed: _incrementCounter,
          tooltip: 'Increment',
          child: const Icon(Icons.add),
        ));
  }
}

这样能达成目的,但是这样这个组件就永久不能更新了,因为它永远不会参与 widget树的重绘。

它相当于绕过了 Flutte框架对于 setState 做出的性能兜底,历史倒退了属于是。

而且如果我们在创建 它时需要使用context,也无法使用 build函数自带的context上下文。

Widget拆分

比如上面的 new Text('$_counter')这个组件,如果我们只希望它在自身范围内进行setState重绘,那么,我们可以将它独立成为一个 StatefulWidget,让它自己刷新自己,但是这样又会引起一个问题,右下角的操作+按钮, 与 它处于两个不同的widget中,那么这就涉及到 多个widget之间交流状态的过程。

下一章节细说。

如何使得不同的组件之间能够交流状态

子widget使用外部变量

这个是最简单的,比如我们定义一个Foo的Widget:

class Foo extends StatelessWidget {
  final String content;

  const Foo.name({Key? key, required this.content}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(content),
        const FlutterLogo(size: 50, textColor: Colors.amber)
      ],
    );
  }
}

而父组件中只需要:const Foo(content: "外部传入的内容") 就能将内容传递进来。

子widget改变外部变量

比如我们想要在Foo中增加一个按钮,让它可以改变传入的值。此时,我们可以传递一个函数到Foo中,而这个函数的内容就是setState,如下:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void add(int v) {
    setState(() {
      _counter += v;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
            child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
          Text('$_counter', style: Theme.of(context).textTheme.headline4),
          Foo(content: "外部传入的内容", changeValueFunc: add)
        ])));
  }
}

class Foo extends StatelessWidget {
  final String content;

  final void Function(int) changeValueFunc;

  const Foo({Key? key, required this.content, required this.changeValueFunc})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(content),
        ElevatedButton(
            onPressed: () => changeValueFunc(1), child: const Text("+1")),
        ElevatedButton(
            onPressed: () => changeValueFunc(2), child: const Text("+2")),
        const FlutterLogo(size: 50, textColor: Colors.amber)
      ],
    );
  }
}

当按下内部的+按钮时,整棵widget树都将重建,每次重建都使用了最新的count值,UI也随之刷新。

外部组件改变子widget的状态

由于这次的子组件是一个有状态的,所以这次要使用 StatefulWidget 来定义Foo。要实现这个效果,我们要让子组件提供一个控制器给外部。

非常明显的案例就是,TextField这个组件,它给外界提供了一个 TextEditingController的控制器,可以由外部控制文本内容。

我们要实现的也是这种效果。

比如下方是一个简单的组件封装,可以自主改变logo:

class Foo extends StatefulWidget {
  const Foo({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return FooState();
  }
}

class FooState extends State<Foo> {
  double _logoSizeOffset = 20;

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.amber,
      child: Column(
        children: [
          FlutterLogo(
            size: _logoSizeOffset,
          ),
          Slider(
              value: _logoSizeOffset,
              max: 100,
              min: 20,
              onChanged: (v) {
                _logoSizeOffset = v;
                setState(() {});
              })
        ],
      ),
    );
  }
}

效果如下:

2023-08-08-09-49-12-image.png

现在不只是需要它可以自己改变logo大小,还需要由外界控制logo大小,实现方式如下:

  • 新建一个 ChangeNotify 实现类,实际上他就是一个控制器

    这里有一些要素要注意:

    1. 必须继承 ChangeNotifier,它是 Listenable 的其中一个实现,作用是实现值的监听,当值有变化时,通知所有的监听者

    2. 在内部定义 Foo需要的所有变量,数量不限,可以很多个。

    3. 定义操作函数,比如changeSize, 其中必须改变 变量的值,并且调用 notifyListeners() 去通知监听者。

    class LogoSizeController extends ChangeNotifier {
      double logoSizeOffset = 20;
    
      LogoSizeController({required this.logoSizeOffset});
    
      void changeSize(double size) {
        logoSizeOffset = size;
        notifyListeners();
      }
    }
    
  • 将控制器放置到 Foo中,注意,现在logoSize的控制权不再是Foo内部的某个double变量,而是 LogoSizeController控制器对象。

    1. Foo接收一个控制器对象

    2. FooState内部使用控制器中的变量值

    3. 需要根据数据变化而重绘刷新的位置,则需要用 AnimatedBuilder(或者新版本里面的 ListenableBuilder)包裹,并设置 控制器对象,还要 定义builder函数,注意:这个builder所返回的 Widget树结构都会随着 控制器数据的改变而重绘。所以,没必要重绘的位置,不要包裹进来。

    class Foo extends StatefulWidget {
      final LogoSizeController logoSizeController;
    
      const Foo({Key? key, required this.logoSizeController}) : super(key: key);
    
      @override
      State<StatefulWidget> createState() {
        return FooState();
      }
    }
    
    class FooState extends State<Foo> {
      @override
      Widget build(BuildContext context) {
        return AnimatedBuilder(
          animation: widget.logoSizeController,
          builder: (BuildContext context, Widget? child) {
            return Container(
              color: Colors.amber,
              child: Column(
                children: [
                  FlutterLogo(
                    size: widget.logoSizeController.logoSizeOffset,
                  ),
                  Slider(
                      value: widget.logoSizeController.logoSizeOffset,
                      max: 100,
                      min: 20,
                      onChanged: (v) {
                        widget.logoSizeController.changeSize(v);
                      })
                ],
              ),
            );
          },
        );
      }
    }
    
  • 使用Foo对象的地方,则需要创建控制器,并且设置给Foo,而只要拥有这个控制器对象,我们可以在任何位置(当前Widget,当前Widget的其他子组件)改变Foo的状态。

    class MyHomePage extends StatefulWidget {
      const MyHomePage({Key? key, required this.title}) : super(key: key);
    
      final String title;
    
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      LogoSizeController logoSizeController =
          LogoSizeController(logoSizeOffset: 30);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                ElevatedButton(
                    onPressed: () {
                      logoSizeController.changeSize(100);
                    },
                    child: const Text("logoSize Max")),
                ElevatedButton(
                    onPressed: () {
                      logoSizeController.changeSize(20);
                    },
                    child: const Text("logoSize Min")),
                Foo(
                  logoSizeController: logoSizeController,
                ),
              ],
            ),
          ),
        );
      }
    }
    

ChangeNotify的冷知识

本节详解上一篇提到的ChangeNotify类的基本用法。本节介绍两个小细节:

ValueNotifier

  1. 对于 单个值改变的监听,就像上面的logoSize,其实有更简单的写法 : ValueNotifier, 带泛型,需要传入初始值用法如下:

     var fontSize = ValueNotifier<double>(20); // 定义控制器
    

    使用控制器时,则必须多调一个.value才能拿到值:widget.cmmController.logoSize.value

    原理也十分简单,就是对我们常用场景的官方封装, 提供get方法以获取值,提供set方法改变值(排除值相同的情况),并通知到监听者。

    class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
    
      ValueNotifier(this._value);
    
      @override
      T get value => _value;
      T _value;
      set value(T newValue) {
        if (_value == newValue)
          return;
        _value = newValue;
        notifyListeners();
      }
    
      @override
      String toString() => '${describeIdentity(this)}($value)';
    }
    

Listenable.merge

Listenable提供了merge方法,如果一个组件需要两个或者更多ValueNotifier中的值,则可以用这个merge将他们共同监听,只要有一个发生变化,这个组件都会刷新。

用法如下:

class FooState extends State<Foo> {
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: Listenable.merge([
        widget.cmmController.logoSize,
        widget.cmmController.fontSize,
      ]),
      builder: (BuildContext context, Widget? child) {
        return Container(
          //...
        );
      },
    );
  }
}

完整代码如下

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final CommController _commController = CommController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ElevatedButton(
                onPressed: () {
                  _commController.logoSize.value = 100;
                },
                child: const Text("logoSize Max")),
            ElevatedButton(
                onPressed: () {
                  _commController.logoSize.value = 20;
                },
                child: const Text("logoSize Min")),
            ElevatedButton(
                onPressed: () {
                  _commController.fontSize.value += 10;
                },
                child: const Text("fontSize+10")),
            ElevatedButton(
                onPressed: () {
                  if (_commController.fontSize.value > 10) {
                    _commController.fontSize.value -= 10;
                  }
                },
                child: const Text("fontSize-10")),
            Foo(cmmController: _commController),
          ],
        ),
      ),
    );
  }
}

class CommController {
  var fontSize = ValueNotifier<double>(20);
  var logoSize = ValueNotifier<double>(20);
}

class Foo extends StatefulWidget {
  final CommController cmmController;

  const Foo({Key? key, required this.cmmController}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return FooState();
  }
}

class FooState extends State<Foo> {
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: Listenable.merge([
        widget.cmmController.logoSize,
        widget.cmmController.fontSize,
      ]),
      builder: (BuildContext context, Widget? child) {
        return Container(
          color: Colors.amber,
          child: Column(
            children: [
              Text(
                "文字尺寸",
                style: TextStyle(fontSize: widget.cmmController.fontSize.value),
              ),
              FlutterLogo(
                size: widget.cmmController.logoSize.value,
              ),
              Slider(
                  value: widget.cmmController.logoSize.value,
                  max: 100,
                  min: 20,
                  onChanged: (v) {
                    widget.cmmController.logoSize.value = v;
                  })
            ],
          ),
        );
      },
    );
  }
}

InheritedWidget 继承式组件

下篇继续。