响应式的编程框架中都会有一个永恒的主题——“状态(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流程图.pngBLoC工作流程如下:
- 用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.png3.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
网友评论