美文网首页
Flutter系列十:Flutter状态管理之Provider的

Flutter系列十:Flutter状态管理之Provider的

作者: chonglingliu | 来源:发表于2021-04-11 19:54 被阅读0次

    状态管理在Flutter中非常重要,但是它包含的内容又非常的广泛。

    本文我们首先了解下什么是状态状态管理呢?然后我们来了解官方的状态管理库Provider的使用,最后分析下Provider背后的秘密。

    Provider

    状态管理

    状态

    Flutter是声明式编程,Widget定义的UI都是在build()函数中实现的,这个函数的功能就是将状态转换成UI

    UI = f(state)

    官方对状态的定义如下:

    whatever data you need in order to rebuild your UI at any moment in time

    翻译过来就是:状态就是任何时间任何场景下重构UI所需要的数据。

    这里面至少可以看到两层含义:

    1. 状态就是数据;
    2. 状态的改变驱动了UI的改变。

    状态的分类

    我们可以把状态分为局部状态全局状态

    局部状态就是Widget内部持有的状态,典型代表就是StatefuleWidget和它对应的State局部状态只会影响单个Widget的UI呈现。

    当某个状态需要在多个Widget使用,或者在整个APP中使用,那它就是全局状态了。全局状态的典型代表就是InheritedWidget

    我们在InheritedWidget的使用和源码分析这篇文章中已经详细介绍过了InheritedWidget的相关内容,当然我们也提到过它的一些不是太完善的地方。

    状态管理库

    我们这里所说的状态管理库主要是指对全局状态的一些处理库,除了InheritedWidget外,还有一些最近非常流行的库:

    它目前是评分最高的库,适合大型的项目。但是它有一个缺点就是理解起来比较困难,编写代码方式也很独特,需要编写一些重复的代码模板。

    它是Flutter官方团队共同维护的一个项目,由于有官方背景,所以不用担心后期的维护升级问题。

    getx是目前上升趋势最快的一个库,使用非常简单,代码也很简介,功能很多。

    当然还有其他一些库,譬如mobx,flutter_redux等,当然你很大可能也不会用到。

    我们将会对Providergetx这两个库的使用和源码进行介绍。

    Provider的使用

    和介绍InheritedWidget时使用的案例类似,本文讲解Provider的时候使用是一个简单的计数器案例:有一个number全局状态,有三个Widget会使用到它,点击FloatingActionButton可以将number的值加1。效果如下:

    Demo

    当然复杂的多界面逻辑的实现方法使用的方法是一样的。譬如实现下面的功能:


    官方的示例

    基本使用

    使用前得先引入库:

    dependencies:
      provider: ^5.0.0
    

    接下来我们分三步来了解它的使用:

    1. number封装到ChangeNotifier中,创建需要共享的状态
    class NumberModel extends ChangeNotifier {
      int _number = 0;
    
      int get number => _number;
    
      set number(int value) {
        _number = value;
        notifyListeners();
      }
    }
    

    ChangeNotifierFlutter Framework的基础类,不是Provider库中的类。ChangeNotifier继承自Listenable,也就是ChangeNotifier可以通知观察者值的改变(实现了观察者模式)。

    NumberModel有一个_number状态,然后提供了获取的方法get和设置set的方法。

    1. 在应用程序的顶层添加ChangeNotifierProvider
    void main() {
      runApp(
        ChangeNotifierProvider(
          create: (ctx) => NumberModel(),
          child: MyApp(),
        ),
      );
    }
    

    将应用的顶层设置为ChangeNotifierProvider, 然后将MyApp()变为它的子Widget

    ChangeNotifierProvidercreate函数需要返回ChangeNotifier

    1. 其它Widget使用共享的状态

    有四个地方需要使用到共享的状态,三个显示文字的Text WidgetFloatingActionButton

    • Provider.of
    class NumberWidget1 extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        // 获取NumberModel的number
        int number = Provider.of<NumberModel>(context).number;
        return Container(
          child: Text(
            "点击次数: $number",
            style: TextStyle(fontSize: 30),
          ),
        );
      }
    }
    

    我们将Text Widget封装成了NumberWidget1, 通过int number = Provider.of<NumberModel>(context).number;获取到NumberModelnumber值,然后就可以显示了。

    class HomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        // 1 获取NumberModel
        NumberModel model = Provider.of<NumberModel>(context);
    
        return Scaffold(
            appBar: AppBar(
              title: Text("Provider"),
            ),
            body: Center(
              child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
            ),
            floatingActionButton: FloatingActionButton(
              onPressed: () {
                // 2 修改number值
                model.number++;
              },
              child: Icon(Icons.add),
            ));
      }
    }
    

    FloatingActionButton也需要通过Provider.of<NumberModel>(context)方法先拿到NumberModel,然后调用set方法改变number的值。

    全部代码:

    void main() {
      runApp(
        ChangeNotifierProvider(
          create: (ctx) => NumberModel(),
          child: MyApp(),
        ),
      );
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
              primarySwatch: Colors.blue, splashColor: Colors.transparent),
          home: HomePage(),
        );
      }
    }
    
    class HomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        
        NumberModel model = Provider.of<NumberModel>(context);
    
        return Scaffold(
            appBar: AppBar(
              title: Text("Provider"),
            ),
            body: Center(
              child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
            ),
            floatingActionButton: FloatingActionButton(
              onPressed: () {
                model.number++;
              },
              child: Icon(Icons.add),
            ));
      }
    }
    
    class NumberWidget1 extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        int number = Provider.of<NumberModel>(context).number;
        return Container(
          child: Text(
            "点击次数: $number",
            style: TextStyle(fontSize: 30),
          ),
        );
      }
    }
    
    class NumberModel extends ChangeNotifier {
      int _number = 0;
    
      int get number => _number;
    
      set number(int value) {
        _number = value;
        notifyListeners();
      }
    }
    
    • Consumer

    问题:Provider.of有一个问题,就是当状态值发生变化后,Provider.of所在的Widget整个build方法都会重新构建。

    上面的例子中,FloatingActionButton会引起Scaffold的重构,所以对性能的影响是最大的。

    Consumer<NumberModel>(
        builder: (context, value, child) {
            return FloatingActionButton(
                onPressed: () {
                    value.number++;
                },
            child: Icon(Icons.add),
            );
        },
    )
    

    我们将FloatingActionButtonConsumer包裹,builder中的value参数就是我们需要的NumberModel了。

    这里我们可以进一步优化一下,对child进行复用。

    Consumer<NumberModel>(
        builder: (context, value, child) {
            return FloatingActionButton(
                onPressed: () {
                    value.number++;
                },
                child: child,
            );
            },
        child: Icon(Icons.add),
    ));
    

    我们将child传入Consumer的构造函数就能实现复用了。

    child复用的逻辑我们在前一篇关于动画源码的文章中有解释,如果需要可以回头参阅。

    差异部分的代码如下:

    class HomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
              title: Text("Provider"),
            ),
            body: Center(
              child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
            ),
            floatingActionButton: Consumer<NumberModel>(
              builder: (context, value, child) {
                return FloatingActionButton(
                  onPressed: () {
                    value.number++;
                  },
                  child: child,
                );
              },
              child: Icon(Icons.add),
            ));
      }
    }
    
    • Consumer

    问题:Consumer总归还是需要重构的,其实我们使用FloatingActionButton的时候只是用到了NumberModel的设置方法,根本没有用到它的_number属性,所以即使_number改变了,我们也是可以不需要重构的。

    如果不需要重构,我们可以使用Selector

    Selector<NumberModel, NumberModel>(
        selector: (ctx, numberModel) => numberModel,
        shouldRebuild: (previous, next) => false,
        builder: (context, value, child) {
            return FloatingActionButton(
                onPressed: () {
                    value.number++;
                },
            child: child,
            );
        },
        child: Icon(Icons.add),
    )
    

    代码解释:

    1. Selector的泛型中有两个参数类型,第一个是原始类型,第二个是转换后的类型,也就是说Selector多了一个对数据进行转换的功能;
    2. selector是进行数据类型转换的函数;
    3. shouldRebuild是确实是否需要重构,我们明显是不需要的,所以传false;
    4. builderConsumer的功能就是类似的了。

    差异部分的代码如下:

    class HomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
              title: Text("Provider"),
            ),
            body: Center(
              child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
            ),
            floatingActionButton: Selector<NumberModel, NumberModel>(
              selector: (ctx, numberModel) => numberModel,
              shouldRebuild: (previous, next) => false,
              builder: (context, value, child) {
                return FloatingActionButton(
                  onPressed: () {
                    value.number++;
                  },
                  child: child,
                );
              },
              child: Icon(Icons.add),
            ));
      }
    }
    

    多个状态的使用

    有时候某个Widget可能需要使用多个状态,我们接下来就介绍这种情况的使用方法。

    1. 创建多个需要共享的状态
    class RandomNumberModel extends ChangeNotifier {
      int _randomNumber = Random().nextInt(100);
    
      int get randomNumber => _randomNumber;
    
      void resetRandomNumber() {
        _randomNumber = Random().nextInt(100);
        notifyListeners();
      }
    }
    
    

    我们再创建一个RandomNumberModel,里面有一个随机的数值_randomNumber, 并且设置获取方法get和设置方法resetRandomNumber

    1. 将应用程序的顶层改为MultiProvider
    void main() {
      runApp(
        MultiProvider(
          providers: [
            ChangeNotifierProvider<NumberModel>(
              create: (ctx) => NumberModel(),
            ),
            ChangeNotifierProvider<RandomNumberModel>(
              create: (ctx) => RandomNumberModel(),
            ),
          ],
          child: MyApp(),
        ),
      );
    }
    

    MultiProviderproviders放置的是共享的多个Provider

    1. 其它Widget使用共享的状态
    • Provider.of
    class NumberWidget1 extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        // 读取
        int number = Provider.of<NumberModel>(context).number;
        // 读取
        int randomNumber = Provider.of<RandomNumberModel>(context).randomNumber;
        return Container(
          // 使用
          child: Text(
            "点击次数: $number 随机数: $randomNumber",
            style: TextStyle(fontSize: 30),
          ),
        );
      }
    }
    

    我们可以通过Provider.of分别取到NumberModelRandomNumberModel,然后读取到相应的值。

    • Consumer2
    class NumberWidget2 extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Container(
          child: Consumer2<NumberModel, RandomNumberModel>(
            builder: (context, value, value2, child) {
              return Text("点击次数: ${value.number}  随机数: ${value2.randomNumber}",
                  style: TextStyle(fontSize: 30));
            },
          ),
        );
      }
    }
    
    

    Consumer2中两个泛型代表使用的哪两个数据,build方法中的value就是NumberModel,value2就是RandomNumberModel,然后读取到相应的值。

    • Selector2
    class NumberWidget3 extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Container(
          child: Selector2<NumberModel, RandomNumberModel, Tuple2<int, int>>(
            selector: (ctx, value1, value2) => Tuple2(value1.number, value2.randomNumber),
            builder: (context, value, child) {
              return Text("点击次数: ${value.item1}  随机数: ${value.item2}",
                  style: TextStyle(fontSize: 30));
            },
            shouldRebuild: (previous, next) => previous != next,
          )
        );
      }
    }
    
    1. Selector2有三个泛型参数:NumberModelRandomNumberModel代表使用的两个数据类型,第三个参数表示由前两个数据转换成的新的数据类型,我们需要使用两个int值。

    使用Tuple2需要引入三方库 tuple: ^2.0.0。使用它的优点是它内置了==比较操作符,不需要我们去自己比较元素是否相等了。

    1. selector的三个参数为:BuildContextNumberModelRandomNumberModel, 返回值就是转换后的数据。

    builder方法中就可以直接使用value.item1value.item2了。

    1. shouldRebuild方法的previousnext的类型是Tuple2<int, int>,可以直接比较。如果相同就不重构了。

    多个状态使用的补充

    Consumer2还有几个好兄弟:,Consumer3Consumer4Consumer5Consumer6

    Selector2也有几个好兄弟:,Selector3Selector4Selector5Selector6

    通过名字可以知道,他们分别可以组合对应的多个数据。

    全部代码:

    void main() {
      runApp(
        MultiProvider(
          providers: [
            ChangeNotifierProvider<NumberModel>(
              create: (ctx) => NumberModel(),
            ),
            ChangeNotifierProvider<RandomNumberModel>(
              create: (ctx) => RandomNumberModel(),
            ),
          ],
          child: MyApp(),
        ),
      );
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
              primarySwatch: Colors.blue, splashColor: Colors.transparent),
          home: HomePage(),
        );
      }
    }
    
    class HomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text("Provider"),
          ),
          body: Center(
            child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [NumberWidget1(), NumberWidget2(), NumberWidget3()]),
          ),
          floatingActionButton: Consumer2<NumberModel, RandomNumberModel>(
            child: Icon(Icons.add),
            builder: (context, value, value2, child) {
              return FloatingActionButton(
                onPressed: () {
                  value.number++;
                  value2.resetRandomNumber();
                },
                child: child,
              );
            },
          ),
        );
      }
    }
    
    class NumberWidget1 extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        int number = Provider.of<NumberModel>(context).number;
        int randomNumber = Provider.of<RandomNumberModel>(context).randomNumber;
        return Container(
          child: Text(
            "点击次数: $number 随机数: $randomNumber",
            style: TextStyle(fontSize: 30),
          ),
        );
      }
    }
    
    class NumberWidget2 extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Container(
          child: Consumer2<NumberModel, RandomNumberModel>(
            builder: (context, value, value2, child) {
              return Text("点击次数: ${value.number}  随机数: ${value2.randomNumber}",
                  style: TextStyle(fontSize: 30));
            },
          ),
        );
      }
    }
    
    class NumberWidget3 extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Container(
          child: Selector2<NumberModel, RandomNumberModel, Tuple2<int, int>>(
            selector: (ctx, value1, value2) => Tuple2(value1.number, value2.randomNumber),
            builder: (context, value, child) {
              return Text("点击次数: ${value.item1}  随机数: ${value.item2}",
                  style: TextStyle(fontSize: 30));
            },
            shouldRebuild: (previous, next) => previous != next,
          )
        );
      }
    }
    
    class NumberModel extends ChangeNotifier {
      int _number = 0;
    
      int get number => _number;
    
      set number(int value) {
        _number = value;
        notifyListeners();
      }
    }
    
    class RandomNumberModel extends ChangeNotifier {
      int _randomNumber = Random().nextInt(100);
    
      int get randomNumber => _randomNumber;
    
      void resetRandomNumber() {
        _randomNumber = Random().nextInt(100);
        notifyListeners();
      }
    }
    

    Provider源码解析

    • Provider的基本架构如下:
    Provider架构
    1. 所有的Provider都继承自InheritedProvider
    2. InheritedProvider持有一个_CreateInheritedProvider对象_delegate, _delegate持有_ValueInheritedProviderState对象,_ValueInheritedProviderState对象通过createState()方法调用了InheritedProvidercreate()方法生成了_value_value也就是开发者提供的可监测对象ChangeNotifier;

    create()只有在需要使用_value时候才会调用,并不是InheritedProvider插入Widget Tree时候就调用,属于懒加载的实现。

    1. InheritedProvider有一个InheritedWidget子Widget _InheritedProviderScope。_InheritedProviderScope持有上面提到的_value的值;

    也就是说Provider依赖于InheritedWidget,找到对应的InheritedWidget就能获取对应的_value的值。

    1. Widget重构的时候如果调用Provider.of方法,会找到_value的值并且监听它的变化。
    • Provider的局部刷新逻辑如下:
    Provider的局部刷新
    1. _value值发生变化,会通知监听者刷新。其中会调用_InheritedProviderScope的markNeedsNotifyDependents方法,调用依赖WidgetdidChangeDependencies, 这两个方法都会调用markNeedsBuild(),进行重构;
    2. Widget重构的时候会调用Provider.of方法,更新对_value的监听,为下次重构做准备。
    • ConsumerSelector的优化逻辑:
    Consumer

    ConsumerSelector只是封装了一层SingleChildStatefulWidget,重构的范围限定在ConsumerSelector内部,内部调用的还是Provider.of方法。

    • MultiProvider的逻辑:
    多个Provider

    MultiProvider就是嵌套了多个Provider,其他和单个Provider没有什么差别。

    总结

    其实Provider库还提供了其他的几个ProviderListenableProvider,ValueListenableProvider,StreamProviderFutureProvider,它们都是我们开发中的可选项。

    至此,我们将Provider库的使用方式和底层的逻辑解释完了。

    相关文章

      网友评论

          本文标题:Flutter系列十:Flutter状态管理之Provider的

          本文链接:https://www.haomeiwen.com/subject/vmhzkltx.html