2.5 状态管理

第1章中介绍了什么是状态,Provider是官方推出的状态管理模式,本节将具体介绍Flutter的状态管理。

2.5.1 有状态及无状态组件

无状态组件(StatelessWidget)是不可变的,这意味着它们的属性不能改变,所有的值都是最终的。

有状态组件(StatefulWidget)持有的状态可能在Widget生命周期中发生变化。实现一个StatefulWidget至少需要两个类:

❑StatefulWidget类,本身是不变的。

❑State类,在Widget生命周期中始终存在。

Flutter官方给出一个有状态组件的示例,点击右下角的+按钮,应用界面中间的数字会加1,如图2-7所示。

图2-7 Flutter官方默认示例

这个示例有几个关键的部分,解析如下。示例代码中MyHomePage必须继承自StatefulWidget类,如下所示:

        class MyHomePage extends StatefulWidget

重写createState方法,如下所示:

        @override
        MyHomePageState createState() => _MyHomePageState();

状态类必须继承自State类,如下所示:

        class _MyHomePageState extends State<MyHomePage>

定义一个普通变量_counter作为计数器变量,调用setState方法来控制这个变量的值的变化,如下所示:

        int _counter = 0;


        void _incrementCounter() {
          setState(() {
            // 计数器变量
            _counter++;
          });
        }

完整的示例代码如下所示:

        import 'package:flutter/material.dart';


        void main() => runApp(MyApp());


        // MyApp不需要做状态处理,所以此组件继承StatelessWidget即可
        class MyApp extends StatelessWidget {
    // 这个组件是整个应用的主组件
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        title: 'Flutter示例',
        theme: ThemeData(
          // 自定义主题
          primarySwatch: Colors.blue,
        ),
        home: MyHomePage(title: '无状态和有状态组件示例'),
      );
    }
  }


  // 主页需要继承自StatefulWidget
  class MyHomePage extends StatefulWidget {
    MyHomePage({Key key, this.title}) : super(key: key);


    // 标题
    final String title;


    // 必须重写createState方法
    @override
    _MyHomePageState createState() => _MyHomePageState();
  }
  // 状态类必须继承State类,注意后面需要指定为<MyHomePage>
  class _MyHomePageState extends State<MyHomePage> {
    int _counter = 0; //计数器


    void _incrementCounter() {
      // 调用State类里的setState方法来更改状态值,使得计数器加1
      setState(() {
        // 计数器变量,每次点击让其加1
        _counter++;
      });
    }


    @override
    Widget build(BuildContext context) {


      return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        // 居中布局
        body: Center(


          // 垂直布局
          child: Column(
          // 主轴居中对齐
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                  '你点击右下角按钮的次数:',
              ),
              Text(
                  '$_counter',               //绑定计数器的值
                            style: Theme.of(context).textTheme.display1,
                          ),
                        ],
                      ),
                    ),
                    floatingActionButton: FloatingActionButton(
                      onPressed: _incrementCounter, //点击+按钮调用自增函数
                      tooltip: '增加',
                      child: Icon(Icons.add),
                    ),
                  );
              }
            }

2.5.2 Provider的使用

当我们的应用足够简单时,你可能并不需要状态管理。但是随着功能的增加,应用程序将会有几十个甚至上百个状态,你的应用状态变得难以维护。Flutter实际上在一开始就为我们提供了一种状态管理方式,那就是StatefulWidget。但是我们很快发现,它正是造成上述问题的“罪魁祸首”。这时候,我们便迫切需要一个架构来帮助我们厘清这些关系,状态管理框架应运而生。

Provider顾名思义即为提供者,用于提供数据,无论是在单个页面还是在整个应用中都有它自己的解决方案,可以很方便地管理状态。

这里我们以计数器为例来详细讲解Provider的使用方法。步骤如下所示。

步骤1在工具目录下的pubspec.yaml文件中添加Provider的依赖。

步骤2创建数据Model,这里的Model实际上就是我们的状态,它不仅存储了我们的数据模型,还包含了更改数据的方法,并暴露出它想要暴露出的数据,如下所示:

        class Counter with ChangeNotifier {


          // 存储数据
          int _count = 0;
          // 提供外部能够访问的数据
          int get count => _count;


          // 提供更改数据的方法
          void increment(){
            _count++;
            // 通知所有听众进行刷新
            notifyListeners();
          }
        }

我们要存储的数据即为_count,下划线代表私有。通过get把_count值暴露出来。并提供increment方法用于更改数据。

这里使用了mixin的方式混入了ChangeNotifier,这个类能够帮助我们自动管理所有听众。当调用notifyListeners时,它会通知所有听众进行刷新。

步骤3创建顶层共享数据。这里使用MultiProvider可以创建多个共享数据,因为实际的应用不可能只有一个数据模型,代码如下:

        class MyApp extends StatelessWidget {
          @override
          Widget build(BuildContext context) {
            // 使用MultiProvider可以创建多个顶层共享数据
            return MultiProvider(
              providers: [
                ChangeNotifierProvider(builder: (_) => Counter()),
              ],
              child: MaterialApp(
                // 首页
              ),
            );
          }
        }

步骤4在子页面中获取状态,在这里我们分别编写了FirstPage及SecondPage。获取顶层数据的方法就是Provider.of<T>(context),这里的范型<T>指定为Counter。获取Counter里的值的代码如下所示:

        Provider.of<Counter>(context).count

在两个页面中均添加以上代码用于获取Counter的值。同时两个页面也可以改变Counter的值,使用方法如下所示:

        Provider.of<Counter>(context).increment();

上述两个页面的增加值的操作是同步的。当在FirstPage页面中点击增加值后,跳转至SecondPage页面时显示的值相同,反之,当在SecondRage中点击增加值后,跳转至FirstPage页面时显示的值相同,如图2-8所示。

图2-8 Provider运行示例

从执行的结果来看,第一个页面和第二个页面的数据得到了共享,改变其中一个页面的值会同时影响另一个页面。完整的代码如下所示:

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


        main() {
          runApp(MyApp(),);
        }


        class MyApp extends StatelessWidget {
          @override
          Widget build(BuildContext context) {
            // 使用MultiProvider可以创建多个顶层共享数据
            return MultiProvider(
              providers: [
                ChangeNotifierProvider(builder: (_) => Counter()),
              ],
              child: MaterialApp(
                title: "Provider示例",
                home: FirstPage(),
              ),
            );
          }
        }


        // 第一个页面
        class FirstPage extends StatelessWidget {
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              appBar: AppBar(
                title: Text("第一个页面"),
                actions: <Widget>[
                  FlatButton(
                      child: Text("下一页"),
                      // 路由跳转至第二页
                      onPressed: () =>
                          Navigator.push(context, MaterialPageRoute(builder: (context) {
                            return SecondPage();
                          })),
                  ),
                ],
              ),
              body: Center(
                // 获取计数器中的count值
                child: Text("${Provider.of<Counter>(context).count}"),
              ),
              floatingActionButton: FloatingActionButton(
                onPressed: () {
                  // 调用数据模型中的increment方法更改数据
                  Provider.of<Counter>(context).increment();
                },
                child: Icon(Icons.add),
                ),
              );
            }
          }


          // 第二个页面
          class SecondPage extends StatelessWidget {
            @override
            Widget build(BuildContext context){
              return Scaffold(
                appBar: AppBar(
                  title: Text("第二个页面"),
                ),
                body: Center(
                  // 获取计数器中的count值
                  child: Text("${Provider.of<Counter>(context).count}"),
                ),
                floatingActionButton: FloatingActionButton(
                  onPressed: () {
                    // 调用数据模型中的increment方法更改数据
                    Provider.of<Counter>(context).increment();
                  },
                  child: Icon(Icons.add),
                ),
              );
            }
          }


          /**
          * 计数器类Counter即为数据Model,实际上就是状态。
          * Counter不仅存储了数据,还包含了更改数据的方法,并暴露相关数据。
          * 使用mixin混入ChangeNotifier类,这个类能够自动管理所有听众。
          * 当调用notifyListeners时,它会通知所有听众进行刷新
          */
          class Counter with ChangeNotifier {


            // 存储数据
            int _count = 0;
            // 提供外部能够访问的数据
            int get count => _count;
            // 提供更改数据的方法
            void increment(){
              _count++;
              // 通知所有听众进行刷新
              notifyListeners();
            }
          }