美文网首页前端开发那些事儿
Flutter轻量级状态管理

Flutter轻量级状态管理

作者: 半心_忬 | 来源:发表于2020-12-15 16:53 被阅读0次

    响应式的编程框架中都会有一个永恒的主题——“状态(State)管理”,无论是在React/Vue(两者都是支持响应式编程的Web开发框架)还是Flutter中,他们讨论的问题和解决的思想都是一致的。言归正传,我们想一个问题,StatefulWidget的状态应该被谁管理?Widget本身?父Widget?都会?还是另一个对象?答案是取决于实际情况!以下是管理状态的最常见的方法:

    • Widget管理自己的状态。
    • Widget管理子Widget状态。
    • 混合管理(父Widget和子Widget都管理状态)。

    如何决定使用哪种管理方法?下面是官方给出的一些原则可以帮助你做决定:

    • 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父Widget管理。
    • 如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由Widget本身来管理。
    • 如果某一个状态是不同Widget共享的则最好由它们共同的父Widget管理。

    在Widget内部管理状态封装性会好一些,而在父Widget中管理会比较灵活。有些时候,如果不确定到底该怎么管理状态,那么推荐的首选是在父widget中管理(灵活会显得更重要一些)。

    一、状态管理的现状

    由于flutter发展的时间不长,状态管理方案各家也都在探索,目前主流的状态管理,scope_model、redux、fish_redux、BloC、rxDart、provider等等,还有一些探索中的模式,融合多个模式的优点,比如reBloc,它们都各具优势,也都不完美。

    目前工程中使用的是fish_redux,使用redux的理念对业务层再做了一层封装,对于我们现在的项目来说,太重,学习成本也很高,不利于项目开发的介入,再者,现在flutter版本更新频繁,三方的更新速度过慢,跟不上业务的发展。

    flutter的状态管理分类

    按使用的范围来分,flutter的状态管理分为两种:局部状态和全局状态

    • 局部状态:flutter原生提供了InheritWidget控件来实现局部状态的控制。当InheritedWidget发生变化时,它的子树中所有依赖它数据的Widget都会进行rebuild。但当业务复杂时,逻辑与UI耦合严重,变的难以维护,复用性也会非常差。

    • 全局状态:Flutter没有提供原生的全局状态管理,基本上是需要依赖第三方库来实现。虽然在根控件上使用InheritedWidget也可以实现,不过会带来很多的问题,比如状态传递过深,难以维护等。

    个人推荐状态管理

    要应对如上的状态管理,由于主流方案都各具优势,也都不完美,必然是组合使用,个人觉得目前最好的方案是RxBloc和provider的组合使用:

    • RxBloc在处理大量异步事件以及分离业务逻辑上表现很优秀,APP业务异步事件非常多,复杂业务也很多,UI和业务逻辑分离是家常便饭,需要优秀的设计来支持,但是在共享状态上还有一些缺陷
    • Provider是官方团队推荐的状态管理包,内部封装了InheritedWidget,RxBloc在共享状态上还有一些缺陷由Provider来弥补
    • Rx社区活跃,对Stream做了扩展,变的更好用功能也更强大,Provider是由官方开发,该组合持续稳定
    • 局部状态使用InheritedWidget实现是没有问题的,不使用是因为会产生很多的胶水的代码

    Tips:

    具体每个方案的优劣就不在本文中详述,自行google即可,这里着重介绍RxBloc和Provider的流程和使用。

    在这之前,你需要了解如下概念:

    • Dart中的Stream是什么,StreamBuilder是什么,怎么使用
    • Rx的基本概念及对Stream封装后的基本使用

    二、局部状态管理 —— RxBLoC

    局部状态管理,其实flutter自身已经为我们提供了状态管理,而且你经常都在用到,它就是 Stateful widget。当我们接触到flutter的时候,首先需要了解的就是有些小部件是有状态的,有些则是无状态的。StatelessWidget 与StatefulWidget。

    在stateful widget中,我们widget的描述信息被放进了State,而stateful widget只是持有一些immutable的数据以及创建它的状态而已。它的所有成员变量都应该是final的,当状态发生变化的时候,我们需要通知视图重新绘制,这个过程就是setState。

    这看上去很不错,我们改变状态的时候setState一下就可以了。

    在我们一开始构建应用的时候,也许很简单,我们这时候可能并不需要状态管理。如下图,setState就足够了。

    simple.png

    但是随着功能的增加,应用程序将会有几十个甚至上百个状态。这个时候应用应该会是这样。

    nan.png

    一旦当app的交互变得复杂,setState出现的次数便会显著增加,每次setState都会重新调用build方法,这势必对于性能、代码的可读性和维护性带来一定的影响。

    那我们就会希望有一种更加强大的方式,来管理我们的状态:

    • 能不能不使用setState就能刷新页面呢?
    • 页面足够复杂的话,能否将业务和UI分离,提升可读性和可维护性?
    • 如果页面足够复杂的话,能不能尽量少重新调用子widget的build方法,提升性能?
    • 即使页面简单,该方式也能胜任,并且不会造成麻烦(比如不像fish_redux有那么多的模板代码)

    于是BLoC呼之欲出,来帮我们处理这些问题。

    BLoC是什么

    BLoC代表业务逻辑组件(Business Logic Component),由来自Google的两位工程师 Paolo Soares和Cong Hui设计,并在2018年DartConf期间(2018年1月23日至24日)首次展示。有兴趣的话可以点击观看Youtube视频

    BLoC是一种利用reactive programming方式构建应用的方法,这是一个由流构成的完全异步的世界。

    bloc流程图.png

    BLoC工作流程如下:

    • 用StreamBuilder包裹有状态的部件,streambuilder将会监听一个流
    • 这个流来自于BLoC
    • 有状态小部件中的数据来自于监听的流。
    • 用户交互手势被检测到,产生了事件。例如按了一下按钮。
    • 调用bloc的功能来处理这个事件
    • 在bloc中处理完毕后将会吧最新的数据add进流的sink中
    • StreamBuilder监听到新的数据,产生一个新的snapshot,并重新调用build方法
    • Widget被重新构建

    BLoC能够允许我们完美的分离业务逻辑!再也不用考虑什么时候需要刷新了(setState不需要我们显示调用),一切交给StreamBuilder和BLoC!

    Tips:

    通过上面的分析,也许我们会说那我们就可以跟StatefulWidget说88了,但通过测试后,准确地描述,应该是可以和大部分StatefulWidget说88,至少保持一个StatefulWidget,使用其state来保存BLoC实例,Stream在不需要使用的时候,需要显示的调用close方法,不然会造成内存泄露或循环引用

    使用RxDart

    ReactiveX是一个强大的库,用于通过使用可观察序列来编写异步和基于事件的程序。它突破了语言和平台的限制,为我们编写异步程序提供了极大的便利。

    如果之前接触过Rx系列,相信已收获Rx带来的便利。

    仅使用flutter提供的Stream足够我们实现BLoC,但RxDart丰富和扩展了Stream,使BLoC更简单更强大。

    RxDart对Stream做了哪些封装,不是本文的重点,需要了解的话自行Google,RxDart具体的API到github自行查看RxDart

    举个栗子

    我们使用BLoC来实现如下这个功能,简单的一个登陆(忽略丑巨的UI,测试而已哈...),需求如下:

    登录demo.png
    • 输入的账号显示在头部已输入账号text中
    • 账号在620位,密码在612位,符合条件,登录按钮才可用
    • 点击登录,3s后,修改底部登录状态为已登录
    定义BLoC 抽象类

    BLoC中无论是直接使用Stream还是RxDart,本质都是Stream,在Stream不需要使用的时候,我们需要显示地调用close方法,所以写一个简单的抽象类,所有的BLoC对象都继承该抽象类,Stream的close都在dispose方法中实现。

    // 所有Bloc的基类
    abstract class BlocProviderBase {
      // 销毁stream
      void dispose();
    }
    
    创建 LoginBLoC 登录BLoC
    /// 登录 bloc
    class LoginBlocProvider extends BlocProviderBase {
      String _account = '';
      String _password = '';
    
      final PublishSubject<String> _accountSub = PublishSubject<String>();
      PublishSubject<String> get accountSub => _accountSub;
    
      final PublishSubject<String> _passwordSub = PublishSubject<String>();
      PublishSubject<String> get passwordSub => _passwordSub;
    
      final PublishSubject<bool> _validSub = PublishSubject<bool>();
      PublishSubject<bool> get validSub => _validSub;
    
      final PublishSubject<String> _loginSub = PublishSubject<String>();
      PublishSubject<String> get loginSub => _loginSub;
    
      // 构造方法
      LoginBlocProvider() {
        _handleSubscript();
      }
    
      // 登录操作
      void doLogin() async {
        await Future.delayed(Duration(seconds: 3));
    
        print('登录成功 => 用户名:$_account, 密码:$_password');
    
        _loginSub.add('登录成功~');
      }
    
      // 处理订阅
      void _handleSubscript() {
        CombineLatestStream<String, bool>([_accountSub, _passwordSub], (values) {
          return values.first.length >= 6 &&
              values.first.length <= 20 &&
              values.last.length >= 6 &&
              values.last.length <= 12;
        }).listen((value) {
          _validSub.sink.add(value);
        });
    
        _accountSub.listen((value) {
          _account = value;
        });
    
        _passwordSub.listen((value) {
          _password = value;
        });
      }
    
      // 销毁
      void dispose() {
        _accountSub.close();
        _passwordSub.close();
        _validSub.close();
        _loginSub.close();
      }
    }
    

    为什么要使用私有变量“_”,提供get方法

    一个应用需要大量开发人员参与,你写的代码也许在几个月之后被另外一个开发看到了,这时候假如你的变量没有被保护的话,那么是可以随意改变其中的属性的,比如_account,如果直接进行赋值,那么就破坏了整个BLoC的流程。

    虽然两种方式的效果完全一样,但是第二种方式将会让我们的business logic零散的混入其他代码中,提高了代码耦合程度,非常不利于代码的维护以及阅读,所以为了让BLoC完全分离我们的业务逻辑,请务必使用私有变量。

    创建 LoginBLoC 实例

    flutter常被人诟病的一点是嵌套过深,我们可以通过抽取子widget来一定程度上规避嵌套地狱,本例中抽取了多个子Widget,一会详细看代码,但同时也就会带来一个BLoC实例从父widget传递到子widget的问题,这里我们使用Provider来实现局部共享,不使用InheritWidget的原因,上文中已说明,就不赘述了,Provider的具体使用,后面会详解,这里先主要说明RxBLoC。

    上文中也提到,我们通过Stream和StreamBuilder实现局部刷新,完全不需要使用setState了,那也就不需要使用StatefulWidget,但是我们需要在页面销毁的时候,调用BLoC实例的dispose方法,我们就至少需要一个顶层的StatefulWidget来保存BLoC实例。

    于是我们在state中创建并保存BLoC实例,并在build的顶层,使用Provider来共享该实例,且在state的dispose中调用BLoC实例中的dispose方法,关闭Stream:

    class _ProviderSharePageHomeState extends State<ProviderSharePageHome> {
      LoginBlocProvider _bloc;
    
      @override
      void initState() {
        super.initState();
    
        _bloc = LoginBlocProvider();
      }
    
      @override
      Widget build(BuildContext context) {
        return Provider(
          create: (ctx) => _bloc,
          child: Column(
            children: <Widget>[
              SizedBox(
                height: 50,
              ),
              LoginAccountWidget(),
              SizedBox(
                height: 10,
              ),
              AccountWidget(),
              SizedBox(
                height: 10,
              ),
              PasswordWidget(),
              SizedBox(
                height: 10,
              ),
              LoginButtonWidget(),
              SizedBox(
                height: 10,
              ),
              LoginStateWidget(),
            ],
          ),
        );
      }
    
      @override
      void dispose() {
        super.dispose();
    
        _bloc.dispose();
      }
    }
    
    LoginBLoC 的使用
    • 输入流

    以账号输入为例,与BLoC的连接如下:

    class AccountWidget extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final _bloc = Provider.of<LoginBlocProvider>(context);
    
        return TextField(
          onChanged: (value) {
            _bloc.accountSub.add('$value');
          },
          decoration: InputDecoration(
            labelText: '用户名',
            filled: true,
          ),
        );
      }
    }
    

    通过对TextField的onChanged方法监听,将新的输入数据通过bloc中的对应的stream,发送给bloc,由bloc做对应的逻辑处理。

    • 输出流

    以输入的用户名text为例,使用StreamBuilder构建如下:

    class LoginAccountWidget extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final _bloc = Provider.of<LoginBlocProvider>(context);
    
        return Container(
            width: double.infinity,
            height: 40,
            color: Colors.black12,
            child: Center(
              child: StreamBuilder(
                stream: _bloc.accountSub.where((origin) {
                  // 丢弃
                  return origin.length >= 6 && origin.length <= 20;
                }).debounceTime(Duration(milliseconds: 500)),
                initialData: '',
                builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
                  return Text(
                    "输入的用户名:${snapshot.data.isEmpty ? '' : snapshot.data}",
                    style: TextStyle(color: Colors.red),
                  );
                },
              ),
            ));
      }
    }
    

    当输入的账号和密码符合规则,登录按钮按钮才会变得可用,同样是是使用StreamBuilder:

    class LoginButtonWidget extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final _bloc = Provider.of<LoginBlocProvider>(context);
    
        return Container(
            width: 128,
            height: 48,
            child: StreamBuilder(
              stream: _bloc.validSub,
              initialData: false,
              builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
                return FlatButton(
                  color: Colors.blueAccent,
                  disabledColor: Colors.blueAccent.withAlpha(50),
                  child: Text(
                    '登录',
                    style: TextStyle(color: Colors.white),
                  ),
                  onPressed: snapshot.data
                      ? () {
                          print('点击了登录');
    
                          _bloc.doLogin();
                        }
                      : null,
                );
              },
            ));
      }
    }
    

    如此,整个登录功能就实现了,BLoC的流程就是这样,其他功能的代码请详见DEMO。

    大型应用中应该如何组织 BLoC

    大型应用程序需要多个BLoC。一个好的模式是为每个屏幕使用一个顶级组件,并为每个复杂足够的小部件使用一个。但是,太多的BLoC会变得很麻烦。此外,如果您的应用中有数百个可观察量(流),则会对性能产生负面影响。换句话说:不要过度设计你的应用程序。

    ——Filip Hracek

    三、全局状态管理 —— Provider

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

    3.1 Provider的基本使用

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

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

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

    • 这里我们可以使用继承自ChangeNotifier,也可以使用混入,这取决于概率是否需要继承自其它的类
    • 我们使用一个私有的_counter,并且提供了getter和setter
    • 在setter中我们监听到_counter的改变,就调用notifyListeners方法,通知所有的Consumer进行更新
    class CounterProvider extends ChangeNotifier {
      int _counter = 100;
      intget counter {
        return _counter;
      }
      set counter(int value) {
        _counter = value;
        notifyListeners();
      }
    }
    
    3.1.2 在Widget Tree中插入ChangeNotifierProvider

    我们需要在Widget Tree中插入ChangeNotifierProvider,以便Consumer可以获取到数据:

    • 将ChangeNotifierProvider放到了顶层,这样方便在整个应用的任何地方可以使用CounterProvider
    void main() {
      runApp(ChangeNotifierProvider(
        create: (context) => CounterProvider(),
        child: MyApp(),
      ));
    }
    
    3.1.3 使用Consumer引入和修改状态
    • 引入位置一:在body中使用Consumer,Consumer需要传入一个builder回调函数,当数据发生变化时,就会通知依赖数据的Consumer重新调用builder方法来构建;
    • 引入位置二:在floatingActionButton中使用Consumer,当点击按钮时,修改CounterNotifier中的counter数据;
    class HomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text("provider测试"),
          ),
          body: Center(
            child: Consumer<CounterProvider>(
              builder: (ctx, counterPro, child) {
                return Text("当前计数:${counterPro.counter}", style: TextStyle(fontSize: 20, color: Colors.red),);
              }
            ),
          ),
          floatingActionButton: Consumer<CounterProvider>(
            builder: (ctx, counterPro, child) {
              return FloatingActionButton(
                child: child,
                onPressed: () {
                  counterPro.counter += 1;
                },
              );
            },
            child: Icon(Icons.add),
          ),
        );
      }
    }
    
    3.1.4 创建一个新的页面,在新的页面中修改数据
    class BasicProviderSecondPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text("第二个页面"),
          ),
          floatingActionButton: Consumer<CounterProvider>(
            builder: (ctx, counterPro, child) {
              return FloatingActionButton(
                child: child,
                onPressed: () {
                  counterPro.counter += 1;
                },
              );
            },
            child: Icon(Icons.add),
          ),
        );
      }
    }
    
    3.2 Provider详解
    3.2.1 Consumer的builder方法解析
    • 参数一:context,每个build方法都会有上下文,目的是知道当前树的位置
    • 参数二:ChangeNotifier对应的实例,也是我们在builder函数中主要使用的对象
    • 参数三:child,目的是进行优化,如果builder下面有一颗庞大的子树,当模型发生改变的时候,我们并不希望重新build这颗子树,那么就可以将这颗子树放到Consumer的child中,在这里直接引入即可(注意我案例中的Icon所放的位置)
    3.2.2 Provider.of解析

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

    Text("当前计数:${Provider.of<CounterProvider>(context).counter}",
      style: TextStyle(fontSize: 30, color: Colors.purple),
    ),
    

    我们会发现很明显上面的代码会更加简洁,那么开发中是否要选择上面这种方式呢?

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

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

    方式一:Provider.of的方式完整的代码:

    • 当我们点击了floatingActionButton时,HomePage的build方法会被重新调用。
    • 这意味着整个HomePage的Widget都需要重新build
    class HomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        print("调用了HomePage的build方法");
        return Scaffold(
          appBar: AppBar(
            title: Text("Provider"),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text("当前计数:${Provider.of<CounterProvider>(context).counter}",
                  style: TextStyle(fontSize: 30, color: Colors.purple),
                )
              ],
            ),
          ),
          floatingActionButton: Consumer<CounterProvider>(
            builder: (ctx, counterPro, child) {
              return FloatingActionButton(
                child: child,
                onPressed: () {
                  counterPro.counter += 1;
                },
              );
            },
            child: Icon(Icons.add),
          ),
        );
      }
    }
    

    方式二:将Text中的内容采用Consumer的方式修改如下:

    • 你会发现HomePage的build方法不会被重新调用;
    • 设置如果我们有对应的child widget,可以采用上面案例中的方式来组织,性能更高;
    3.2.3 Selector的选择

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

    • 比如当点击了floatingActionButton时,我们在代码的两处分别打印它们的builder是否会重新调用;
    • 我们会发现只要点击了floatingActionButton,两个位置都会被重新builder;
    • 但是floatingActionButton的位置有重新build的必要吗?没有,因为它是否在操作数据,并没有展示;
    • 如何可以做到让它不要重新build了?使用Selector来代替Consumer

    直接上代码:

    floatingActionButton: Selector<CounterProvider, CounterProvider>(
      selector: (ctx, provider) => provider,
      shouldRebuild: (pre, next) => false,
      builder: (ctx, counterPro, child) {
        print("floatingActionButton展示的位置builder被调用");
        return FloatingActionButton(
          child: child,
          onPressed: () {
            counterPro.counter += 1;
          },
        );
      },
      child: Icon(Icons.add),
    ),
    

    Selector和Consumer对比,不同之处主要是三个关键点:

    • 关键点1:泛型参数是两个
      • 泛型参数一:我们这次要使用的Provider
      • 泛型参数二:转换之后的数据类型,比如我这里转换之后依然是使用CounterProvider,那么他们两个就是一样的类型
    • 关键点2:selector回调函数
      • 转换的回调函数,你希望如何进行转换
      • S Function(BuildContext, A) selector
      • 我这里没有进行转换,所以直接将A实例返回即可
    • 关键点3:是否希望重新rebuild
      • 这里也是一个回调函数,我们可以拿到转换前后的两个实例;
      • bool Function(T previous, T next);
      • 因为这里我不希望它重新rebuild,无论数据如何变化,所以这里我直接return false;

    这个时候,我们重新测试点击floatingActionButton,floatingActionButton中的代码并不会进行rebuild操作。

    所以在某些情况下,我们可以使用Selector来代替Consumer,性能会更高。

    3.2.4 MultiProvider

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

    我们再增加一个新的ChangeNotifier

    import'package:flutter/material.dart';
    
    class UserInfo {
      String nickname;
      int level;
    
      UserInfo(this.nickname, this.level);
    }
    
    class UserProvider extends ChangeNotifier {
      UserInfo _userInfo = UserInfo("test", 18);
    
      set userInfo(UserInfo info) {
        _userInfo = info;
        notifyListeners();
      }
    
      get userInfo {
        return _userInfo;
      }
    }
    

    如果在开发中我们有多个Provider需要提供应该怎么做呢?

    方式一:多个Provider之间嵌套

    • 这样做有很大的弊端,如果嵌套层级过多不方便维护,扩展性也比较差
    runApp(ChangeNotifierProvider(
        create: (context) => CounterProvider(),
        child: ChangeNotifierProvider(
          create: (context) => UserProvider(),
          child: MyApp()
        ),
      ));
    

    方式二:使用MultiProvider

    runApp(MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (ctx) => CounterProvider()),
        ChangeNotifierProvider(create: (ctx) => UserProvider()),
      ],
      child: MyApp(),
    ));
    
    3.3 RxBLoC+Provider 栗子

    由于RxBLoC是使用StreamBuilder来连接BLoC中的Stream,当有新数据时,会自动刷新子Widget,在全局共享时,我们并不需要使用Provider的notify功能,所以共享的数据直接使用我们定义好的BLoC就可以了。

    由于我们不需要notify功能,所以在APP顶层共享数据是,也不需要使用ChangeNotifierProvider,直接使用Provider即可,当共享多个BLoC时,使用MultiProvider,这个例子即是演示共享多个状态。

    3.3.1 小需求

    需要在全局共享一个count和一个name,count的初始值是10,name的初始值是name,在count页面,点击右下角的+,count累加,在name页面点击右下角的+,name在后面拼接一个1字符串,count页面和name页面,都显示count+name的格式化字符串。

    counter.png name.png
    3.3.2 创建共享的count和name的BLoC
    /// 数值 bloc
    class CounterBlocProvider extends BlocProviderBase {
      int _counter = 10;
    
      BehaviorSubject<int> _counterSub = BehaviorSubject.seeded(10);
      BehaviorSubject<int> get counterSub => _counterSub;
    
      // 构造方法
      CounterBlocProvider() {
        _handleSubscript();
      }
    
      // 增加操作
      void doAdd() {
        print('执行了 counter 增加操作');
    
        _counterSub.add(++_counter);
      }
    
      // 处理订阅
      void _handleSubscript() {
        _counterSub.listen((value) {
          _counter = value;
        });
      }
    
      // 销毁
      void dispose() {
        _counterSub.close();
      }
    }
    
    /// name bloc
    class NameBlocProvider extends BlocProviderBase {
      String _name = 'name';
    
      BehaviorSubject<String> _nameSub = BehaviorSubject.seeded('name');
      BehaviorSubject<String> get nameSub => _nameSub;
    
      // 构造方法
      NameBlocProvider() {
        _handleSubscript();
      }
    
      // 增加操作
      void doAdd() {
        print('执行了 name 增加操作');
    
        _nameSub.add(_name + '1');
      }
    
      // 处理订阅
      void _handleSubscript() {
        // _nameSub.add(_name);
        _nameSub.listen((value) {
          _name = value;
        });
      }
    
      // 销毁
      void dispose() {
        _nameSub.close();
      }
    }
    
    3.3.3 在APP顶层共享全局状态
    void main() {
      runApp(MultiProvider(
        providers: [
          Provider(create: (ctx) => CounterBlocProvider()),
          Provider(create: (ctx) => NameBlocProvider()),
        ],
        child: MyApp(),
      ));
    }
    
    3.3.4 创建count page 和 name page,使用全局共享状态

    由于我们需要显示的内容是共享的两个BLoC状态,所以对两个Stream进行了合并操作,使用了RxDart中的CombineLatestStream,无论是count还是name发生了变化,在显示的地方都会实时刷新。

    如果是单纯使用Stream,这个功能实现会比较麻烦,这也是Rx带来的便利的体现。

    class ProviderPage extends StatefulWidget {
      static const String routeName = "/providerPage";
    
      const ProviderPage({Key key}) : super(key: key);
    
      @override
      _ProviderPageState createState() => _ProviderPageState();
    }
    
    class _ProviderPageState extends State<ProviderPage> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
              title: Text('counter provider page'),
              actions: <Widget>[
                IconButton(
                    icon: Icon(Icons.people),
                    onPressed: () {
                      Navigator.pushNamed(context, ProviderPage2.routeName);
                    })
              ],
            ),
            body: Center(
              child: Consumer2<CounterBlocProvider, NameBlocProvider>(
                  builder: (context, cntProvider, nameProvider, child) {
                return StreamBuilder(
                    initialData: '初始化',
                    stream: CombineLatestStream<dynamic, dynamic>(
                        [cntProvider.counterSub, nameProvider.nameSub], (values) {
                      print('合并的值是啥:${values.join(' + ')}');
                      return values.join(' + ');
                    }),
                    builder: (context, snapshot) {
                      return Chip(label: Text(snapshot.data));
                    });
              }),
            ),
            floatingActionButton: Consumer2<CounterBlocProvider, NameBlocProvider>(
              builder: (context, cntProvider, nameProvider, child) {
                return FloatingActionButton(
                    child: Icon(Icons.add),
                    onPressed: () {
                      cntProvider.doAdd();
                    });
              },
            ));
      }
    }
    
    class ProviderPage2 extends StatefulWidget {
      static const String routeName = "/providerPage2";
    
      const ProviderPage2({Key key}) : super(key: key);
    
      @override
      _ProviderPage2State createState() => _ProviderPage2State();
    }
    
    class _ProviderPage2State extends State<ProviderPage2> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
              title: Text('name provider page'),
              actions: <Widget>[
                IconButton(
                    icon: Icon(Icons.people),
                    onPressed: () {
                      Navigator.pushNamed(context, ProviderPage.routeName);
                    })
              ],
            ),
            body: Center(
              child: Consumer2<CounterBlocProvider, NameBlocProvider>(
                  builder: (context, cntProvider, nameProvider, child) {
                return StreamBuilder(
                    initialData: '初始化',
                    stream: CombineLatestStream<dynamic, dynamic>(
                        [cntProvider.counterSub, nameProvider.nameSub], (values) {
                      print('合并的值是啥:${values.join(' + ')}');
                      return values.join(' + ');
                    }),
                    builder: (context, snapshot) {
                      return Chip(label: Text(snapshot.data));
                    });
              }),
            ),
            floatingActionButton: Consumer2<CounterBlocProvider, NameBlocProvider>(
              builder: (context, cntProvider, nameProvider, child) {
                return FloatingActionButton(
                    child: Icon(Icons.add),
                    onPressed: () {
                      nameProvider.doAdd();
                    });
              },
            ));
      }
    }
    

    注意点:

    • 这里我们使用Consumer2,其实就是Consumer的升级,支持2个泛型,Consumer2、3...6,以此类推,同理
    • Selector与Consumer一样,也有Selector2...6,就是为了支持多个数据共享

    问题点:

    • 由于Selector相对Consumer,能减少子Widget的build方法调用次数,所以能使用Selector当然使用Selector,但是这个需求我尝试了很多次,只能使用Consumer,如果有大佬用Selector能够实现,望不吝赐教!!!

    四、后记

    没有最完美的代码,也没有最完美的框架,只有适合自己的框架,以上内容仅供参考~

    上述代码的DEMO,传送门

    参考文档:

    https://mp.weixin.qq.com/s/ywGQnaYpioPxlYvYTSpR4w
    https://www.jianshu.com/p/7573dee97dbb
    https://www.jianshu.com/p/a5d7758938ef
    https://www.jianshu.com/p/e0b0169a742e

    相关文章

      网友评论

        本文标题:Flutter轻量级状态管理

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