Flutter 状态管理

Posted 一叶飘舟

tags:

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

StatefulWidget

Flutter 中的组件,按状态划分:

  StatelessWidget(无状态组件)

  StatefulWidget(状态组件)

状态组件是包含可变状态的组件,状态的特点:

(1)当组件构建完成后,可同步读取

(2)可以在组件的生命周期中改变

按状态作用域划分

  组件内私有状态(StatefulWidget)

  跨组件状态共享(InheritedWidget, Provider)

  全局状态(Redux, fish-redux, Mobx......)

状态组件的组成

  StatefulWidget(组件本身不可变 @immutable)

  State(将变化的状态放到 State 中维护)

import 'package:flutter/material.dart';

class Home extends StatelessWidget 
  const Home(Key? key) : super(key: key);

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(
        title: const Text('StatefulWidget'),
      ),
      body: const MyState(),
    );
  


class MyState extends StatefulWidget 
  const MyState(Key? key) : super(key: key);

  @override
  State<MyState> createState() => _MyStateState();


class _MyStateState extends State<MyState> 
  int _num = 0;

  // 执行加的方法
  void _increment() 
    setState(() 
      _num++;
    );
  

  // 减的方法
  void _decrement() 
    setState(() 
      if (_num == 0) return;
      _num--;
    );
  

  @override
  Widget build(BuildContext context) 
    return Center(
      child: Column(
        children: [
          // 减
          ElevatedButton(
            onPressed: _decrement,
            child: const Icon(Icons.minimize),
          ),
          // 数字
          Padding(
            padding: const EdgeInsets.all(20),
            child: Text('$_num'),
          ),
          // 加
          ElevatedButton(
            onPressed: _increment,
            child: const Icon(Icons.add),
          )
        ],
      ),
    );
  

生命周期

  • initState() 组件对象插入到元素树中时,通常根据后台接口的返回数据对状态进行初始化
  • didChangeDependencies() 当前状态对象的依赖改变时
  • build() 组件渲染时
  • setState() 组件对象的内部状态变更时
  • didUpdateWidget() 组件配置更新时
  • deactivate() 组件对象在元素树中暂时移除时
  • dispose() 组件对象在元素树中永远移除时

// ignore_for_file: avoid_print
import 'package:flutter/material.dart';

class Home extends StatelessWidget 
  const Home(Key? key) : super(key: key);

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(
        title: const Text('LifeCycle'),
      ),
      body: const MyState(),
    );
  


class MyState extends StatefulWidget 
  const MyState(Key? key) : super(key: key);

  @override
  State<MyState> createState() => _MyStateState();


class _MyStateState extends State<MyState> 
  int _num = 0;

  @override
  void initState() 
    // TODO: implement initState
    super.initState();
    print('initState, 执行initState方法,_num: $_num');
    // 通常根据后台接口返回的数据对状态进行初始化
    _num = 1;
  

  @override
  void didChangeDependencies() 
    // TODO: implement didChangeDependencies
    super.didChangeDependencies();
    print('didChangeDependencies, 当前状态对象的依赖改变了, _num: $_num');
  

  @override
  void didUpdateWidget(covariant MyState oldWidget) 
    // TODO: implement didUpdateWidget
    super.didUpdateWidget(oldWidget);
    print('didUpdateWidget, 组件配置更新了,_num: $_num');
  

  @override
  void deactivate() 
    // TODO: implement deactivate
    super.deactivate();
    print('deactivate, 组件对象在元素树中暂时移除了,_num: $_num');
  

  @override
  void dispose() 
    // TODO: implement dispose
    super.dispose();
    print('dispose, 组件对象在元素树中永远移除了,_num: $_num');
  

  // 执行加的方法
  void _increment() 
    setState(() 
      print('setState, 组件对象的内部状态变更了,_num: $_num');
      _num++;
    );
  

  // 减的方法
  void _decrement() 
    setState(() 
      print('setState, 组件对象的内部状态变更了,_num: $_num');
      if (_num == 0) return;
      _num--;
    );
  

  @override
  Widget build(BuildContext context) 
    print('build, 组件渲染时,_num: $_num');
    return Center(
      child: Column(
        children: [
          // 减
          ElevatedButton(
            onPressed: _decrement,
            child: const Icon(Icons.minimize),
          ),
          // 数字
          Padding(
            padding: const EdgeInsets.all(20),
            child: Text('$_num'),
          ),
          // 加
          ElevatedButton(
            onPressed: _increment,
            child: const Icon(Icons.add),
          )
        ],
      ),
    );
  

InheritedWidget

What:

  提供沿树向下,共享数据的功能

  即子组件可以获取父组件(InheritedWidget 的子类)的数据

Why:

  依赖构造函数传递数据的方式不能满足业务需求。如图,组件D和组件G共享的数据是通过组件A取数据以后逐级向下透传的,也就是说数据需要逐级传递,比较麻烦,而且不是所有组件都需要相关数据

所以,需要一个新的,更好的跨组件数据传输方案。图中,所有的组件都可以获取顶层父组件 InheritedWidget 的数据,可以直接获取,不需要依赖其它组件传递

How:

  BuildContext.dependOnInheritedWidgetOfExactType<MyInheritedWidget>()

构造函数

InheritedWidget(Key? key, required Widget child)

InhertiedWidget抽象类包含一个常量构造器,继承该抽象类的子类可以提供一个常量构造器,在使用这些子类时可以用 const 表达式来调用

child:Widget,表示在组件树中挂载在该子类下的组件

key:Key?,表示要传递的数据

import 'package:flutter/material.dart';

class Home extends StatelessWidget 
  const Home(Key? key) : super(key: key);

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(
        title: const Text('InheritedWidget'),
      ),
      body: const CalculatorState(),
    );
  


// 组件一:挂载在 InheritedWidget 下,它提供了一个可共享的数据 num
class CalculatorState extends StatefulWidget 
  const CalculatorState(Key? key) : super(key: key);

  @override
  State<CalculatorState> createState() => _CalculatorStateState();


class _CalculatorStateState extends State<CalculatorState> 
  int _num = 0;

  // 执行加的方法
  void _increment() 
    setState(() 
      _num++;
    );
  

  // 减的方法
  void _decrement() 
    setState(() 
      if (_num == 0) return;
      _num--;
    );
  

  @override
  Widget build(BuildContext context) 
    // 要调用 of 方法,of 方法中的参数 context 必须是 InheritedWidget 的后代
    // 也就是说,组件必须挂在 InheritedWidget 下面
    return ShareDataWidget(
      // 提供 ShareDataWidget 必须的两个属性 num 和 child
      num: _num,
      child: Center(
        child: Column(
          children: [
            // 减
            ElevatedButton(
              onPressed: _decrement,
              child: const Icon(Icons.minimize),
            ),
            // 数字
            const Padding(
              padding: EdgeInsets.all(20),
              // 跨组件访问数据
              child: MyCounter(),
            ),
            // 加
            ElevatedButton(
              onPressed: _increment,
              child: const Icon(Icons.add),
            )
          ],
        ),
      ),
    );
  


// 组件二:调用 of() 方法,获取 InheritedWidget 共享的数据 num
class MyCounter extends StatefulWidget 
  const MyCounter(Key? key) : super(key: key);

  @override
  State<MyCounter> createState() => _MyCounterState();


class _MyCounterState extends State<MyCounter> 
  @override
  Widget build(BuildContext context) 
    // 使用 InheritedWidget 中的共享数据
    if (ShareDataWidget.of(context)?.num == null) 
      return const Text('0');
    
    return Text(ShareDataWidget.of(context)!.num.toString());
  


// 数据共享组件
class ShareDataWidget extends InheritedWidget 
  const ShareDataWidget(
    Key? key,
    required this.num,
    required this.child,
  ) : super(key: key, child: child);

  final int num;
  final Widget child;

  // 静态方法 of 用于调用 BuildContext.dependOnInheritedWidgetOfExactType
  // 这样做的目的是为了让类可以定义自己的回调逻辑,即便该类没有出现在 context 上下文中
  static ShareDataWidget? of(BuildContext context) 
    return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
  

  @override
  bool updateShouldNotify(ShareDataWidget oldWidget) 
    return true;
  

Provider

Provider 是对 InheritedWidget 的封装,使其使用更容易使用且易于复用

文档:https://pub.flutter-io.cn/packages/provider

安装:flutter pub add provider

引用:import 'package:provider/provider.dart';

优点:

(1)简化资源的分配与处置

(2)懒加载

(3)创建新类时减少大量的模板代码

(4)支持 DevTools

实现原理

ChangeNotifier:class,可以封装应用程序的状态,用于向监听器发送通知,如果被定义为 ChangeNotifier,就可以订阅它的状态变化。对于简单的程序,一个 ChangeNotifier 就可以满足需求;对于复杂的应用,由于会有多个模型,所以可能会有多个 ChangeNotifier

ChangeNotifierProvider:widget,用于监听 ChangeNotifier 的实例并将其暴露给子孙节点,并且当 ChangeNotifier.notifyListeners 被调用时,会重新构建。虽然数据是单独存放在 ChangeNotifier 中,但是面对组件时,直接提供数据的是 Provider

如果你想提供更多状态,可以使用 MultiProvider:

void main() 
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => CartModel()),
        Provider(create: (context) => SomeOtherClass()),
      ],
      child: const MyApp(),
    ),
  );

区别于 InheritedWidget 把状态存放在根节点上,Provider 会把状态和根节点做分割处理,状态会被提取出来保存在 ChangeNotifier 组件中,使用数据时,需要把数据注册到根节点 ChangeNotifierProdiver 上,后续的逻辑基本相同。

图中的 Producer (生产者)组件修改了数据以后,ChangeNotifierProvier (观察者)就会做两件事情,一是通知 ChangeNotifier(被观察者)更新 data,二是通知所有的 Listener(听众,即消费者)更新UI界面。也就是说,Provider 会把整个应用分成三个部分:

(1)数据部分

(2)监控数据的部分

(3)UI界面

整个过程也可以类比 MVVM 的状态管理模式

使用步骤:

(1)创建数据模型

(2)创建 Provider

(3)在子组件中使用数据模型

示例:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// Step 1
// 1、创建数据模型
class LikesModel extends ChangeNotifier 
  // 模拟一个私有变量(实际需要把这个类放在一个单独的文件中)
  int _counter = 0;
  // 模拟一个私有的getter
  int get counter => _counter;

  void incrementCounter() 
    // 累加
    _counter++;

    // 通知 UI 更新(监听了这个模型的组件重新构建)
    notifyListeners();
  


// Step 2
class Home extends StatelessWidget 
  const Home(Key? key) : super(key: key);

  @override
  Widget build(BuildContext context) 
    // 2、创建 Provider (注册数据模型)
    return ChangeNotifierProvider(
      create: (BuildContext context) => LikesModel(),
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Provider'),
        ),
        body: const MyHomePage(),
      ),
    );
  


// Step 3
class MyHomePage extends StatelessWidget 
  const MyHomePage(Key? key) : super(key: key);

  @override
  Widget build(BuildContext context) 
    return Container(
      padding: const EdgeInsets.all(10),
      child: Column(
        children: [
          // 3、在子组件中使用数据模型
          Text(
            '点赞了:$context.watch<LikesModel>().counter 次',
            style: const TextStyle(
              color: Colors.red,
              fontSize: 26,
            ),
          ),
          TextButton(
            onPressed: () => context.read<LikesModel>().incrementCounter(),
            child: const Icon(Icons.thumb_up),
          ),
        ],
      ),
    );
  

读取值

可以使用 BuildContext 上的扩展属性(由 provider 注入)

(1)context.watch<T>(),widget 能够监听到 T 类型的 provider 发生的改变

(2)context.read<T>(),直接返回 T,不会监听改变

(3)context.select<T, R>(R cb(T value)),允许 widget 只监听 T 上的一部分内容的改变

需要注意的是,context.read<T>() 方法不会在值变化时让 widget 重新构建,并且不能在 StatelessWidget.build 和 State.build 内调用。换句话说,它可以在除了这两个方法以外的任意位置调用

Text(
  // 此处如果使用 context.read 来读取 counter,将监听不到状态更改,视图不会更新
  '点赞了:$context.watch<LikesModel>().counter 次',
  style: const TextStyle(
    color: Colors.red,
    fontSize: 26,
  ),
),

这些方法会从传入的 BuildContext 关联的 widget 开始,向上查找 widget 树,并返回查找到的层级最近的 T 类型的 provider(未找到时将抛出错误)。例如,把 Step3 中的 MyHomePage 直接作为 Step2 中的 body 组件,就会报如下错误

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// Step 1
// 1、创建数据模型
class LikesModel extends ChangeNotifier 
  // 模拟一个私有变量(实际需要把这个类放在一个单独的文件中)
  int _counter = 0;
  // 模拟一个私有的getter
  int get counter => _counter;

  void incrementCounter() 
    // 累加
    _counter++;

    // 通知 UI 更新(监听了这个模型的组件重新构建)
    notifyListeners();
  


// Step 2
class Home extends StatelessWidget 
  const Home(Key? key) : super(key: key);

  @override
  Widget build(BuildContext context) 
    // 2、创建 Provider (注册数据模型)
    return ChangeNotifierProvider(
      create: (BuildContext context) => LikesModel(),
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Provider'),
        ),
        // todo: 注意看这里,会产生异常
        body: Container(
          padding: const EdgeInsets.all(10),
          child: Column(
            children: [
              // 3、在子组件中使用数据模型
              Text(
                '点赞了:$context.watch<LikesModel>().counter 次',
                style: const TextStyle(
                  color: Colors.red,
                  fontSize: 26,
                ),
              ),
              TextButton(
                onPressed: () => context.read<LikesModel>().incrementCounter(),
                child: const Icon(Icons.thumb_up),
              ),
            ],
          ),
        ),
      ),
    );
  

ProviderNotFoundException (Error: Could not find the correct Provider<LikesModel> above this Home Widget

大概意思是说,Home组件的 BuildContext 不包含 Provider<LikesModel>,产生这个错误的原因是使用的 BuildContext 是当前正在读取数据的 provider 的祖先元素,当你创建好一个 provider 以后立即读取其数据就会报这个错。

解决方法:

(1)将上下文传递给与构建方法分离。参考示例中的 Step2、Step3 的写法,拆成两个组件

(2)将上下文包装在 Builder 中

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// Step 1
// 1、创建数据模型
class LikesModel extends ChangeNotifier 
  // 模拟一个私有变量(实际需要把这个类放在一个单独的文件中)
  int _counter = 0;
  // 模拟一个私有的getter
  int get counter => _counter;

  void incrementCounter() 
    // 累加
    _counter++;

    // 通知 UI 更新(监听了这个模型的组件重新构建)
    notifyListeners();
  


// Step 2
class Home extends StatelessWidget 
  const Home(Key? key) : super(key: key);

  @override
  Widget build(BuildContext context) 
    // 2、创建 Provider (注册数据模型)
    return ChangeNotifierProvider(
      create: (BuildContext context) => LikesModel(),
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Provider'),
        ),
        // todo: 修改点:将上下文包装在 Builder 中
        body: Builder(builder: (BuildContext context) 
          return Container(
            padding: const EdgeInsets.all(10),
            child: Column(
              children: [
                // 3、在子组件中使用数据模型
                Text(
                  '点赞了:$context.watch<LikesModel>().counter 次',
                  style: const TextStyle(
                    color: Colors.red,
                    fontSize: 26,
                  ),
                ),
                TextButton(
                  onPressed: () =>
                      context.read<LikesModel>().incrementCounter(),
                  child: const Icon(Icons.thumb_up),
                ),
              ],
            ),
          );
        ),
      ),
    );
  

DataTable

DataTable 是 Flutter 中的表格

columns:声明表头列表

  DataColumn:表头单元格

rows:声明数据列表

  DataRow:一行数据

  DataCell:数据单元格

import 'package:flutter/material.dart';

class Home extends StatelessWidget 
  const Home(Key? key) : super(key: key);

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(
        title: const Text('DataTable'),
      ),
      body: ListView(
        children: const [UserTable()],
      ),
    );
  


class UserTable extends StatefulWidget 
  const UserTable(Key? key) : super(key: key);

  @override
  State<UserTable> createState() => _UserTableState();


class _UserTableState extends State<UserTable> 
  @override
  Widget build(BuildContext context) 
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 1),
      child: SingleChildScrollView(
        child: DataTable(
          // 表头
          columns: const [
            // 表头单元格
            DataColumn(label: Text('姓名')),
            DataColumn(label: Text('年龄')),
            DataColumn(label: Text('性别')),
            DataColumn(label: Text('简介')),
          ],
          // 表格主体
          rows: const [
            // 行
            DataRow(cells: [
              // 列
              DataCell(Text('张三')),
              DataCell(Text('18')),
              DataCell(Text('男')),
              DataCell(Text('一首张三的歌')),
            ]),
            DataRow(cells: [
              DataCell(Text('二丫')),
              DataCell(Text('22')),
              DataCell(Text('女')),
              DataCell(Text('没有简介')),
            ]),
          ],
        ),
      ),
    );
  

import 'package:flutter/material.dart';

class Home extends StatelessWidget 
  const Home(Key? key) : super(key: key);

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(
        title: const Text('DataTable'),
      ),
      body: const UserTable(),
    );
  


class User 
  String name;
  int age;
  String gender;
  String intro;
  bool selected;

  User(this.name, this.age, this.gender, this.intro, this.selected = false);


class UserTable extends StatefulWidget 
  const UserTable(Key? key) : super(key: key);

  @override
  State<UserTable> createState() => _UserTableState();


class _UserTableState extends State<UserTable> 
  List<User> data = [
    User('美国队长', 200, '男', '最会甩锅的男人'),
    User('钢铁侠', 72, '男', 'i love you more than 4000'),
    User('蜘蛛侠', 18, '男', '彼得帕克和玛莉简拍照时被五彩毒蜘蛛咬了', selected: true),
    User('黑寡妇', 36, '女', '把你的洗澡水给杜兰特喝一点吧'),
  ];

  bool _sortAscending = true;
  List<DataRow> _getUserRows() 
    List<DataRow> dataRows = [];
    for (int i = 0; i < data.length; i++) 
      dataRows.add(
        DataRow(
          selected: data[i].selected,
          onSelectChanged: (selected) 
            // ignore: avoid_print
            setState(() 
              data[i].selected = selected!;
            );
          ,
          cells: [
            DataCell(Text(data[i].name)),
            DataCell(Text('$data[i].age')),
            DataCell(Text(data[i].gender)),
            DataCell(Text(data[i].intro)),
          ],
        ),
      );
    
    return dataRows;
  

  @override
  Widget build(BuildContext context) 
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 1),
      child: SingleChildScrollView(
        scrollDirection: Axis.horizontal,
        // 表格
        child: DataTable(
          // 需要排序的列
          sortColumnIndex: 1,
          // 排序方式,false:降序,true:升序
          sortAscending: _sortAscending,
          // 行高
          dataRowHeight: 100,
          // 每一行的首位两列的表格边缘和父容器间的外边距
          horizontalMargin: 10,
          // 单元格的水平外边距
          columnSpacing: 100,
          // 表头
          columns: [
            const DataColumn(label: Text('姓名')),
            DataColumn(
              label: const Text('年龄'),
              // 该列是否表示数值,如是,则该列内容右对齐
              numeric: true,
              // 排序:ascending false:降序,true:升序
              onSort: (int columnIndex, bool ascending) 
                setState(() 
                  _sortAscending = ascending;
                  if (ascending) 
                    // compareTo:负数表示a小于b,正数表示a大于b
                    data.sort((a, b) => a.age.compareTo(b.age));
                   else 
                    data.sort((a, b) => b.age.compareTo(a.age));
                  
                );
              ,
            ),
            const DataColumn(label: Text('性别')),
            const DataColumn(label: Text('介绍')),
          ],
          // 表格主体
          rows: _getUserRows(),
        ),
      ),
    );
  

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

Flutter如何状态管理

flutter Provide (状态管理篇)

用于 Web 状态管理的 Flutter

Flutter 状态管理- 使用 MobX

Flutter 响应式状态管理框架GetX

Flutter 响应式状态管理框架GetX