bloc+rxdart 项目实战,flutter的mvc方案

6,783 阅读9分钟

文章结构:

  • bloc+rxdart的几大优点
  • 示例代码简单实现调用网络接口,然后刷新页面显示
  • bloc+rxdart实现原理
  • bloc+rxdart经典使用场景,解决了业务场景中的两个问题。

1.bloc+rxdart的几大优点

1).实现MVC模式

widget只做UI展示,bloc实现控制逻辑,model做数据封装。 我在运营线的新需求开发中实际使用了bloc做了一个新的页面,就是商机败北页面。 这个页面有三级的败北原因选择,败北原因是级联的,还有每次选择设备后要重新请求库存,提交败北等操作。

2).实现跨层级model访问和方法调用

在同一个页面中跨widget数据获取数据和操作比较多,例如提交败北的时候,需要获取多个子widget中的数值,如果没有使用bloc的话,就需要在子widget的构造方法中传递model或回调方法。

如果页面层级较多,这种model和回调方法就需要逐级传递。 例如在下图左边,需要把submiModel或回调方法从最上层一直传递到最底部,才能做到收集所有的子widget中的信息,最终集合所有的widget中的参数到submitModel中,提交败北请求。

下面右边的代码就是这样逐层传递参数的方法,如果层级很多的话会非常头疼,不仅要重复的声明变量,而且如果修改的话要每个地方都改。

而使用了bloc的话,构造函数就特别干净,不需要逐层传递model或者回调方法。

下面的两块代码都是页面widget结构中最下层的两个widget,不需要在构造方法中传递任何参数或回调方法,但是同样能调用拿到model和调用回调方法。怎么实现的继续往下看。:)

3).不直接使用setState方法(StreamController)

之前在项目中直接使用setState会产生一些问题,例如当前State是unmounted状态,这时直接调用setState会抛异常。

而使用bloc的话刷新页面是使用如下方式。关键的代码是_requestController.add

4). 更加简洁的局部刷新(StreamBuilder)

我们现有项目中有一些非常复杂的页面,但是只使用了一个文件,所有的业务逻辑和widget组件都在这个页面里面。

下面是一段这种混杂网络请求、widget显示的代码示例。

有多个网络请求和对应的数据显示widget,不论哪个请求返回或者用户点击事件,只要调用一个setState就能刷新页面更新显示。

这样相比于多个子widget的方式就不用逐层传递数据和回调方法。但是破坏了面向对象开发的基本原则:信息隐藏,比如修改某个widget的一个小功能,因为在一个很大的文件中修改,其中业务逻辑错综复杂,改一个小功能可能就会影响其他逻辑,产生意料不到的后果,这一块的代码就变得非常难以维护。

同时直接调用setState刷新整个页面的性能也有问题,也会产生抛异常的问题。


而使用bloc的方式会更加自由,可以像第二点中的图中一样使用多个子widget,在子widget中进行刷新。

也可以直接改造上图的代码,在一个widget中实现局部刷新。

第四个优点其实和第三个是一并实现的,都是通过streamBuidler和streamController,这两者是配套使用的。其实bloc就是streamBuilder+streamController+ancestorWidgetOfExactType,和web端的ajax比较类似,几项现有技术整合出了新的东西

StreamBuilder要传入一个stream对象才能实现刷新,而这个stream要从streamController中获取的。

当在上面的代码中调用_requestController.add(deviceModel)后,下面的StreamBuilder就会自动调用builder方法,实现局部刷新StreamBuilder,而不是刷新外层的InkWell。

StreamBuilder是一个Widget,可以在任意地方插入,实现局部刷新非常简单。

2.简单示例代码

要实现一个BlocProvider,现有项目中已经集成,整个项目只需要一个BlocProvider,可以放在Utils目录中。

实现Bloc其实就只用这一个类和flutter自带的方法就行了,代码非常简单,并不需要在yaml文件中引入第三方库。

所有的bloc都需要继承BlocBase,通常一个bloc对应一个会刷新页面的业务逻辑(如网络请求、切换tab)。

bloc初始化可以在任意父页面、爷爷页面中,获取只需要使用BlocProvider.of(context)。
使用bloc代码如下:
有三点需要注意的。 1.bloc要保存在state中,不然会因为widget重新构建而丢失。 2.bloc需要调用dispose,在bloc中的dispose中会调用streamController的close方法,最好把bloc的dispose和create在同一个state中调用。 3.BlocProvider要在更外面一层创建。

下面是bloc创建和blocProvider的代码。

3.bloc+rxdart实现原理

1).bloc实现原理

bloc触发刷新的方式就是使用StreamBuilder+StreamController,但是直接使用StreamController有一个很大的缺点就是只能进行一对一的listen,且模式非常单一。所以需要使用到rxdart,下面会讲到。

bloc另一重要特性就是跨层级获取bloc,在bloc可以获取到model或者调用方法。

其中使用了ancestorWidgetOfExactType,因为flutter widget是树状结构的,结构层次保存在BuildContext中,可以从子节点依次往父节点找符合类型的widget,所以所有使用了BlocProvider包裹的widget都可以通过of方法找到,再返回widget中的bloc。(bloc最终是保存在外一层的state中)。

final type = _typeOf<BlocProvider<T>>();
BlocProvider<T> provider = context.ancestorWidgetOfExactType(type);

在下图中,最下层的widget调用of方法后,可以直接获取到顶层页面的submitBloc,因为flutter widget是树状结构的,结构层次保存在BuildContext中,可以从子节点依次往父节点找符合类型的widget,所以所有使用了BlocProvider包裹的widget都可以通过of方法找到,再返回widget中的bloc。(bloc最终是保存在外一层的state中)。

2).rxdart实现原理

StreamController实现了观察者模式,监听者不是直接被调用,而是处于观察状态,当event加入streamController后,监听者获得异步回调。

rxdart是对StreamController的扩展,提供了更多的模式。分为两个部分Subject和Observable。
其中Observable对stream封装后提供多个处理方法,例如map、expand、merge、every、contact。

var obs = Observable(Stream.fromIterable([1,2,3,4,5]))
    .map((item)=>++item);
obs.listen(print);
输出:2 3 4 5 6

Subject是对StreamController的扩展,常用的有以下几种。

PublishSubject:StreamController广播版,streamController只能有一个listener,PublishSubject可以多次listen。下面的其他几种Subject也都是广播版。

BehaviorSubject: 缓存最近一次的事件。如果先发生event,后listen,也能收到缓存的event。

ReplaySubject: 缓存所有的事件,之前加入的所有event,listen后,都会顺序发送过来。

4.经典使用场景

1).多个网络图片组件共用一个http下载。

开发zn_web_image这个网络图片下载缓存组件的时候,为了测试数据加载缓存的请求,把图片链接放在一个数组中并多次重复。发现同样的图片链接在上面已经下载完成后,下面还会重复下载。

发现这是由于每次都创建新的bloc导致的,通过使用一个bloc list解决了这个问题。

static ZNImageBLocList _getInstance() {
  if (_instance == null) {
    _instance = new ZNImageBLocList._internal();
  }
  return _instance;
}

List<ZNImageBloc> blocList;
 
ZNImageBloc getBloc(String url,ZNImageConfig config){
  for(ZNImageBloc bloc in blocList){
    if(bloc.imageUrl==url){
      return bloc;
    }
  }
  ZNImageBloc bloc = new ZNImageBloc(url, config);
  blocList.add(bloc);
  return bloc;
}
class ZnWebImageState extends State<ZnWebImage> {
  ZNImageBloc bloc;

  @override
  void dispose() {
    // TODO: implement dispose
    bloc.dispose();
    super.dispose();
  }

  @override
  void initState() {
    // TODO: implement initState
    super.initState();

    bloc = ZNImageBLocList.instance.getBloc(widget.url, widget.config);
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return BlocProvider<ZNImageBloc>(
      bloc: bloc,
      child: ZNImageContainer(),
    );
  }
}

这样多个zn_web_image的widget就能共用同一个bloc,在下载过程中同步显示进度条,下载完成后同时收到通知显示下载好的图片。

2).tabBarView中多个页面共用一个是否通过认证属性。

bloc在征信项目中也解决了一个很头疼的问题,就是否通过认证的状态,这个状态全局共用,且有多个变更入口,认证状态变更后要求所有页面更新显示。

用到个人认证的地方有多个入口:

1.第一个tab中的首页顶部。

2.确认订单页面,可以从第一个tab首页进入,也可以从第二个tab的订单列表进入。

3.第四个tab的个人中心页面。

最开始没有使用bloc的时候,写了多个网络请求获取认证状态,isAuth属性也在每个页面分别保存。

1.首页通过requestAuth从服务器端获取isAuth属性。

2.确认订单页面也需要从requestAuth获取isAuth属性。虽然首页通过请求获取到了isAuth属性,从首页进入确认订单页面可以传递isAuth属性。但是从订单列表进入时没有这个属性,订单列表是和首页同时初始化的,还是需要网络请求。

3.个人中心页面也需要从requestAuth获取isAuth属性。虽然首页通过请求获取到了isAuth属性,但是个人中心页面是和首页同时在tabbarView中初始化的,不能通过构造方法传递isAuth属性。

第二个问题就是从其中一个页面进入认证页面完成认证后,其他所有页面都需要刷新认证状态,没有使用bloc时,在这些页面的生命周期方法中写了重新发起网络请求来刷新isAuth状态。

这样非常的麻烦,而且工作量大很多。

使用bloc之后,isAuth这样全局共用的属性,只需要在MaterialApp外面加一层BlocProvider就行了,任何一个子页面都能直接获取isAuth,并且能同步isAuth的状态更新,刷新UI显示。

//个人认证bloc
CreditAuthBloc authBloc;

@override
void dispose() {
  authBloc.dispose();
  super.dispose();
}

@override
void initState() {
  super.initState();
  authBloc = CreditAuthBloc();
}


@override
Widget build(BuildContext context) {
  return BlocProvider<CreditAuthBloc>(bloc: authBloc,child: MaterialApp(
    title: '',
    initialRoute:'/',
    home: Scaffold(
        body: Column(
          children: <Widget>[
            Expanded(
              child: TabBarView(physics: NeverScrollableScrollPhysics(),controller: tabController, children: [
                ZNMarketPage(),//首页
                ZNOrderList(),//订单列表
                ZNBill(),
                ZNMyCenter(),//个人中心
              ]),
            ),
          ],
        )),
  ),);
}

子页面获取bloc

  //使用bloc中的auth
//  int userAuthStatus = 0; //个人认证 0: 未认证, 1: 已认证
  CreditAuthBloc authBloc;
  
  @override
  void didChangeDependencies() {
    // TODO: implement didChangeDependencies
    super.didChangeDependencies();
    authBloc = BlocProvider.of<CreditAuthBloc>(context);
  }

通过streamBuilder使用bloc