凯发娱发k8

flutter bloc状态管理 简单上手 -凯发娱发k8

2023-10-18

我们都知道,flutter中widget的状态控制了ui的更新,比如最常见的statefulwidget,通过调用setstate({})方法来刷新控件。那么其他类型的控件,比如statelesswidget就不能更新状态来吗?答案当然是肯定可以的。前文已经介绍过几种状态管理

stream

streamdart 提供的一种数据流订阅管理的"工具",感觉有点像 android 中的 eventbus 或者 rxbusstream 可以接收任何对象,包括是另外一个 stream,接收的对象通过 streamcontrollersink 进行添加,然后通过 streamcontroller 发送给 stream,通过 listen 进行监听,listen 会返回一个 streamsubscription 对象,streamsubscription 可以操作对数据流的监听,例如 pauseresumecancel 等。

stream 分两种类型:

    single-subscription stream:单订阅 stream,整个生命周期只允许有一个监听,如果该监听 cancel 了,也不能再添加另一个监听,而且只有当有监听了,才会发送数据,主要用于文件 io 流的读取等。
    broadcast stream:广播订阅 stream,允许有多个监听,当添加了监听后,如果流中有数据存在就可以监听到数据,这种类型,不管是否有监听,只要有数据就会发送,用于需要多个监听的情况。

还是看下例子会比较直观:

class _streamhomestate extends state {
streamcontroller _controller = streamcontroller(); // 创建单订阅类型 `streamcontroller`
sink _sink;
streamsubscription _subscription; @override
void initstate() {
super.initstate(); _sink = _controller.sink; // _sink 用于添加数据
// _controller.stream 会返回一个单订阅 stream,
// 通过 listen 返回 streamsubscription,用于操作流的监听操作
_subscription = _controller.stream.listen((data) => print('listener: $data')); // 添加数据,stream 会通过 `listen` 方法打印
_sink.add('a');
_sink.add();
_sink.add(11.16);
_sink.add([, , ]);
_sink.add({'a': , 'b': });
} @override
void dispose() {
super.dispose();
// 最后要释放资源...
_sink.close();
_controller.close();
_subscription.cancel();
} @override
widget build(buildcontext context) {
return scaffold(
body: container(),
);
}
}

运行后看下控制台的输出:

果然把所有的数据都打印出来了,前面有说过,单订阅的 stream 只有当 listen 后才会发送数据,不试试我还是不相信的,我们把 _sink.add 放到 listen 前面去执行,再看控制台的打印结果。居然真的是一样的,google 粑粑果然诚不欺我。
接着试下 pauseresume 方法,看下数据如何监听,修改代码:

_sink = _controller.sink;
_subscription = _controller.stream.listen((data) => print('listener: $data'));
_sink.add('a');
_subscription.pause(); // 暂停监听
_sink.add();
_sink.add(11.16);
_subscription.resume(); // 恢复监听
_sink.add([, , ]);
_sink.add({'a': , 'b': });

再看控制台的打印,你们可以先猜下是什么结果,我猜大部分人都会觉得应该是不会有 11 和 11.16 打印出来了。然而事实并非这样,打印的结果并未发生变化,也就是说,调用 pause 方法后,stream 被堵住了,数据不继续发送了。

接下来看下广播订阅 stream,对代码做下修改:

streamcontroller _controller = streamcontroller.broadcast();  //广播订阅 stream
sink _sink;
streamsubscription _subscription; @override
void initstate() {
super.initstate(); _sink = _controller.sink; // _sink 用于添加数据 _sink.add('a');
_subscription = _controller.stream.listen((data) => print('listener: $data')); // 添加数据,stream 会通过 `listen` 方法打印
_sink.add();
_subscription.pause();
_sink.add(11.16);
_subscription.resume(); _sink.add([, , ]);
_sink.add({'a': , 'b': });
} @override
void dispose() {
super.dispose();
// 最后要释放资源...
_sink.close();
_controller.close();
_subscription.cancel();
}

我们再看下控制台的打印:

总结:

单订阅 stream 只有当存在监听的时候,才发送数据,广播订阅 stream 则不考虑这点,有数据就发送;当监听调用 pause 以后,不管哪种类型的 stream 都会停止发送数据,当 resume 之后,把前面存着的数据都发送出去。

sink 可以接受任何类型的数据,也可以通过泛型对传入的数据进行限制,比如我们对 streamcontroller 进行类型指定 streamcontroller _controller = streamcontroller.broadcast(); 因为没有对 sink 的类型进行限制,还是可以添加除了 int 外的类型参数,但是运行的时候就会报错,_controller 对你传入的参数做了类型判定,拒绝进入。

stream 中还提供了很多 stremtransformer,用于对监听到的数据进行处理,比如我们发送 0~19 的 20 个数据,只接受大于 10 的前 5 个数据,那么可以对 stream 如下操作:

_subscription = _controller.stream
.where((value) => value > )
.take()
.listen((data) => print('listen: $data')); list.generate(, (index) => _sink.add(index));

那么打印出来的数据如下图:

除了 wheretake 还有很多 transformer, 例如 mapskip 等等,小伙伴们可以自行研究。了解了 stream 的基本属性后,就可以继续往下了~

  我们上面已经说了,stream的特性就是当数据源发生变化的时候,会通知订阅者,那么我们是不是可以延展一下,实现当数据源发生变化时,改变控件状态,通知控件刷新的效果呢?flutter为我们提供了streambuilder
  所以,streambuilder是stream在ui方面的一种使用场景,通过它我们可以在非statefulwidget中保存状态,同时在状态改变时及时地刷新ui。

streambuilder

streambuilder其实是一个statefulwidget,它通过监听stream,发现有数据输出时,自动重建,调用builder方法。前面提到了 stream 通过 listen 进行监听数据的变化,flutter 就为我们提供了这么个部件 streambuilder 专门用于监听 stream 的变化,然后自动刷新重建。接着来看下源码

streambuilder(
key: ...可选...
stream: ...需要监听的stream...
initialdata: ...初始数据,否则为空...
builder: (buildcontext context, asyncsnapshot snapshot){
if (snapshot.hasdata){
return ...基于snapshot.hasdata返回的控件
}
return ...没有数据的时候返回的控件
},
)

下面是一个模仿官方自带demo“计数器”的一个例子,使用了streambuilder,而不需要任何setstate:

import 'package:flutter/material.dart';
import 'dart:async'; class counterpage extends statefulwidget {
@override
_counterpagestate createstate() => _counterpagestate();
} class _counterpagestate extends state {
int _counter = ;
final streamcontroller _streamcontroller = streamcontroller(); @override
void dispose(){
_streamcontroller.close();
super.dispose();
} @override
widget build(buildcontext context) {
return scaffold(
appbar: appbar(title: text('stream version of the counter app')),
body: center(
child: streambuilder( // 监听stream,每次值改变的时候,更新text中的内容
stream: _streamcontroller.stream,
initialdata: _counter,
builder: (buildcontext context, asyncsnapshot snapshot){
return text('you hit me: ${snapshot.data} times');
}
),
),
floatingactionbutton: floatingactionbutton(
child: const icon(icons.add),
onpressed: (){
// 每次点击按钮,更加_counter的值,同时通过sink将它发送给stream;
// 每注入一个值,都会引起streambuilder的监听,streambuilder重建并刷新counter
_streamcontroller.sink.add( _counter);
},
),
);
}
}

这种实现方式比起setstate是一个很大的改进,因为我们不需要强行重建整个控件和它的子控件,只需要重建我们希望重建的streambuilder(当然它的子控件也会被重建)。我们之所以依然使用statefulwidget的唯一原因就是:streamcontroller需要在控件dispose()的时候被释放

能不能完全抛弃statefulwidget?bloc了解下

上一步,我们摒弃了 setstate 方法,那么下一步,我们试试把 statefulwidget 替换成 statelesswidget 吧,而且官方也推荐使用 statelesswidget 替换 statefulwidget,这里就需要提下 bloc 模式了。

bloc是business logic component(业务逻辑组建)的缩写,就是将ui与业务逻辑分离,有点mvc的味道。

说实话,现在 google 下 「flutter bloc」能搜到很多文章,基本上都是通过 inheritedwidget 来实现的,但是 inheritedwidget 没有提供 dispose 方法,那么就会存在 streamcontroller 不能及时销毁等问题,所以,参考了一篇国外的文章,reactive programming - streams - bloc 这里通过使用 statefulwidget 来实现,当该部件销毁的时候,可以在其 dispose 方法中及时销毁 streamcontroller,这里我还是先当个搬运工,搬下大佬为我们实现好的基类

import 'package:flutter/material.dart';
abstract class basebloc {
void dispose(); // 该方法用于及时销毁资源
} class blocprovider extends statefulwidget {
final widget child; // 这个 `widget` 在 stream 接收到通知的时候刷新
final t bloc; blocprovider({key key, @required this.child, @required this.bloc}) : super(key: key); @override
_blocproviderstate createstate() => _blocproviderstate(); // 该方法用于返回 bloc 实例
static t of(buildcontext context) {
final type = _typeof>(); // 获取当前 bloc 的类型
// 通过类型获取相应的 provider,再通过 provider 获取 bloc 实例
blocprovider provider = context.ancestorwidgetofexacttype(type);
return provider.bloc;
} static type _typeof() => t;
} class _blocproviderstate extends state> { @override
void dispose() {
widget.bloc.dispose(); // 及时销毁资源
super.dispose();
} @override
widget build(buildcontext context) {
return widget.child;
}
}

接着我们对前面的例子使用 bloc 进行修改。

首先,我们需要创建一个 bloc 类,用于修改 count 的值:

import '../widget/basebloc.dart';
import 'dart:async';
class counterbloc extends basebloc {
int _count = ;
int get count => _count; // stream
streamcontroller _countcontroller = streamcontroller.broadcast(); stream get countstream => _countcontroller.stream; // 用于 streambuilder 的 stream void dispatch(int value) {
_count = value;
_countcontroller.sink.add(_count); // 用于通知修改值
} @override
void dispose() {
_countcontroller.close(); // 注销资源
}
}

在使用 bloc 前,需要在最上层的容器中进行注册,也就是 materialapp 中.

import 'package:flutter/material.dart';
import './widget/basebloc.dart';
import './bloc/counter.dart'; void main() => runapp(myapp()); class myapp extends statelesswidget {
@override
widget build(buildcontext context) {
// 这里对创建的 bloc 类进行注册,如果说有多个 bloc 类的话,可以通过 child 进行嵌套注册即可
// 放在最顶层,可以全局调用,当 app 关闭后,销毁所有的 bloc 资源,
// 也可以在路由跳转的时候进行注册,至于在哪里注册,完全看需求
// 例如实现主题色的切换,则需要在全局定义,当切换主题色的时候全局切换
// 又比如只有某个或者某几个特殊界面调用,那么完全可以通过在路由跳转的时候注册
return blocprovider(
child: materialapp(
debugshowcheckedmodebanner: false,
home: streamhome(),
),
bloc: counterbloc());
}
} class streamhome extends statelesswidget {
@override
widget build(buildcontext context) {
// 获取注册的 bloc,必须先注册,再去查找
final counterbloc _bloc = blocprovider.of(context);
return scaffold(
body: safearea(
child: container(
alignment: alignment.center,
child: streambuilder(
initialdata: _bloc.count,
stream: _bloc.countstream,
builder: (_, snapshot) => text('${snapshot.data}', style: textstyle(fontsize: 20.0)),
),
)),
floatingactionbutton:
// 通过 bloc 中的 dispatch 方法进行值的修改,通知 stream 刷新界面
floatingactionbutton(onpressed: () =>
_bloc.dispatch(_bloc.count ), child: icon(icons.add)),
);
}
}

重新运行后,查看效果还是一样的。所以我们成功的对 statefulwidget 进行了替换。

先总结下 bloc:

​ 1. 成功的把页面和逻辑分离开了,页面只展示数据,逻辑通过 bloc 进行处理

​ 2. 减少了 setstate 方法的使用,提高了性能

​ 3. 实现了状态管理

多个bloc的使用

每一个有业务逻辑的页面的顶层都应该有自己的bloc;
每一个“足够复杂的组建(complex enough component)”都应该有相应的bloc;
可以使用一个applicationbloc来处理整个app的状态。

下面的例子展示了在整个app的顶层使用applicationbloc,在counterpage的顶层使用incrementbloc:

void main() => runapp(
blocprovider(
bloc: applicationbloc(),
child: myapp(),
)
); class myapp extends statelesswidget {
@override
widget build(buildcontext context){
return materialapp(
title: 'streams demo',
home: blocprovider(
bloc: incrementbloc(),
child: counterpage(),
),
);
}
} class counterpage extends statelesswidget {
@override
widget build(buildcontext context){
final incrementbloc counterbloc = blocprovider.of(context);
final applicationbloc appbloc = blocprovider.of(context); ...
}
}

一个实践demo

大佬构建了一个伪应用程序来展示如何使用所有这些概念。 完整的源代码可以在github上找到。

flutter bloc状态管理 简单上手的相关教程结束。

网站地图