Flutter轻量级状态管理

响应式的编程框架中都会有一个永恒的主题——“状态(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 _accountSub = PublishSubject();
  PublishSubject get accountSub => _accountSub;

  final PublishSubject _passwordSub = PublishSubject();
  PublishSubject get passwordSub => _passwordSub;

  final PublishSubject _validSub = PublishSubject();
  PublishSubject get validSub => _validSub;

  final PublishSubject _loginSub = PublishSubject();
  PublishSubject get loginSub => _loginSub;

  // 构造方法
  LoginBlocProvider() {
    _handleSubscript();
  }

  // 登录操作
  void doLogin() async {
    await Future.delayed(Duration(seconds: 3));

    print('登录成功 => 用户名:$_account, 密码:$_password');

    _loginSub.add('登录成功~');
  }

  // 处理订阅
  void _handleSubscript() {
    CombineLatestStream([_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 {
  LoginBlocProvider _bloc;

  @override
  void initState() {
    super.initState();

    _bloc = LoginBlocProvider();
  }

  @override
  Widget build(BuildContext context) {
    return Provider(
      create: (ctx) => _bloc,
      child: Column(
        children: [
          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(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(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 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(context);

    return Container(
        width: 128,
        height: 48,
        child: StreamBuilder(
          stream: _bloc.validSub,
          initialData: false,
          builder: (BuildContext context, AsyncSnapshot 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(
          builder: (ctx, counterPro, child) {
            return Text("当前计数:${counterPro.counter}", style: TextStyle(fontSize: 20, color: Colors.red),);
          }
        ),
      ),
      floatingActionButton: Consumer(
        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(
        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(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: [
            Text("当前计数:${Provider.of(context).counter}",
              style: TextStyle(fontSize: 30, color: Colors.purple),
            )
          ],
        ),
      ),
      floatingActionButton: Consumer(
        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(
  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 _counterSub = BehaviorSubject.seeded(10);
  BehaviorSubject 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 _nameSub = BehaviorSubject.seeded('name');
  BehaviorSubject 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 {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('counter provider page'),
          actions: [
            IconButton(
                icon: Icon(Icons.people),
                onPressed: () {
                  Navigator.pushNamed(context, ProviderPage2.routeName);
                })
          ],
        ),
        body: Center(
          child: Consumer2(
              builder: (context, cntProvider, nameProvider, child) {
            return StreamBuilder(
                initialData: '初始化',
                stream: CombineLatestStream(
                    [cntProvider.counterSub, nameProvider.nameSub], (values) {
                  print('合并的值是啥:${values.join(' + ')}');
                  return values.join(' + ');
                }),
                builder: (context, snapshot) {
                  return Chip(label: Text(snapshot.data));
                });
          }),
        ),
        floatingActionButton: Consumer2(
          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 {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('name provider page'),
          actions: [
            IconButton(
                icon: Icon(Icons.people),
                onPressed: () {
                  Navigator.pushNamed(context, ProviderPage.routeName);
                })
          ],
        ),
        body: Center(
          child: Consumer2(
              builder: (context, cntProvider, nameProvider, child) {
            return StreamBuilder(
                initialData: '初始化',
                stream: CombineLatestStream(
                    [cntProvider.counterSub, nameProvider.nameSub], (values) {
                  print('合并的值是啥:${values.join(' + ')}');
                  return values.join(' + ');
                }),
                builder: (context, snapshot) {
                  return Chip(label: Text(snapshot.data));
                });
          }),
        ),
        floatingActionButton: Consumer2(
          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轻量级状态管理)