Day 29 | 状态管理-从官方范例来看如何使用BLoC (2)

今天就来实作UI的部分,以及来小小的比较一下BLoC与MobX的差异

我们把这个页面分成三个档案posts_pageposts_listposts_item

首先是这个最外层的 posts_page

class PostsPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Posts')),
      body: BlocProvider(
        create: (_) => PostBloc()..add(PostFetched()),
        child: PostsList(),
      ),
    );
  }
}

这里比较重要的就是 BlocProvider ,它的作用是让这个widget tree底下的所有的widget都能从context得到我们指定的BLoC,什麽意思?

就如同我们之前在说MobX store一样,如果我不想层层传入参数,那我势必得用一些方法让我的底下所有的widget可以简单且优雅得取出这个instance。

flutter_bloc 这个pub就提供了这个API让我们可以不用额外做其他处理就能将BLoC共享给底下的所有widget。

当然这个原理跟之前讲到get_it是不一样的这里就不详述他们的差异了,从这个例子来看只是刚好他们解决了同一个问题。

所以我们在 create 这个参数放PostBloc() ,但这里会看到我们用了 ..add(PostFetched()).. 就是执行完操作会return instance本身,所以整行来看就是我在传入PostBloc 的同时顺便对这个BLoC给了一个事件 PostFetched

接下来就来看 PostsList 的widget实作

这里就跟官方范例会有点差距,主要是因为其实原本的范例中没有实作loading 这个 status而是滚到最下面就跳出读取中的UI,但我自己觉得有点怪,所以後来就自己实作loading status。

主要是将state跟bloc改写了一点点

// in post_state.dart
enum PostStatus { initial, success, loading, failure }

// in post_bloc.dart
Future<void> _onPostFetched(
      PostFetched event, Emitter<PostState> emit) async {
    if (state.hasReachedMax) return;
    try {
      if (state.status == PostStatus.initial) {
        final posts = await _fetchPosts();
        return emit(state.copyWith(
          status: PostStatus.success,
          posts: posts,
          hasReachedMax: false,
        ));
      }

      emit(state.copyWith(
        status: PostStatus.loading,
      ));
      final posts = await _fetchPosts(state.posts.length);
      emit(posts.isEmpty
          ? state.copyWith(status: PostStatus.success, hasReachedMax: true)
          : state.copyWith(
              status: PostStatus.success,
              posts: List.of(state.posts)..addAll(posts),
              hasReachedMax: false,
            ));
    } catch (_) {
      emit(state.copyWith(status: PostStatus.failure));
    }
  }

主要就多一个enum及多emit一次loading中的state

说回来UI

final _scrollController = ScrollController();

@override
Widget build(BuildContext context) {
  return SingleChildScrollView(
    controller: _scrollController,
    child: BlocBuilder<PostBloc, PostState>(
      builder: (context, state) {
        switch (state.status) {
          case PostStatus.failure:
            return const Center(child: Text('failed to fetch posts'));
          case PostStatus.success:
          case PostStatus.loading:
            if (state.posts.isEmpty) {
              return const Center(child: Text('no posts'));
            }
            return Column(children: [
              ListView.builder(
                shrinkWrap: true,
                itemBuilder: (BuildContext context, int index) {
                  return index >= state.posts.length
                      ? const SizedBox()
                      : PostListItem(post: state.posts[index]);
                },
                itemCount: state.hasReachedMax
                    ? state.posts.length
                    : state.posts.length + 1,
                physics: const NeverScrollableScrollPhysics(),
              ),
              BottomLoader(
                  postStatus: state.status,
                  hasReachedMax: state.hasReachedMax),
            ]);
          default:
            return const Center(child: CircularProgressIndicator());
        }
      },
    ),
  );
}

最主要的就是 BlocBuilder 这个widget,他就是会响应BLoC的状态变化来进行rerender,而这里会有两个参数 blocbuilder ,但如果bloc 不传入的话他会自动从buildContext寻找bloc ,所以在使用 BlocProvider 是可以不用传入,除非这里只是本地状态。
这边我们需要使用泛型让我们能够取得BLoC及状态,然後 builder: (context, state) 才能去存取他们。

那剩下就是类似 StreamBuilder 一样根据状态来选择渲染的UI。

接着是滚动事件的处理

@override
void initState() {
  super.initState();
  _scrollController.addListener(_onScroll);
}

@override
  void dispose() {
    _scrollController
      ..removeListener(_onScroll)
      ..dispose();
    super.dispose();
  }

  void _onScroll() {
    if (_isBottom) context.read<PostBloc>().add(PostFetched());
  }

  bool get _isBottom {
    if (!_scrollController.hasClients) return false;
    final maxScroll = _scrollController.position.maxScrollExtent;
    final currentScroll = _scrollController.offset;
    return currentScroll >= (maxScroll * 0.9);
  }

基本上就是利用 _scrollController 来达成这个需求,首先在 initState 时让 _scrollController 去监听事件 _onScroll

_onScroll 的实作就是当滑到底部时就向 PostBloc 新增一个事件 PostFetched

_isBottom 就是当目前滚动到距离底部的只剩整个页面高度的十分之一的高度时就会回传 true

至於 dispose 就是当离开这个页面时也要将这个滚动监听给移除,否则每次进来这个页面都会重新建立这个事件监听但不会自动销毁,会有memory leaks 的风险存在。

至於 PostListItemBottomLoader 就只是单纯的UI实作没什麽特别好说的点,有兴趣的读者可以参阅文末的完整程序码。


写到这里就来稍微比较一下 MobX及 BLoC,先打个预防针就是这两个状态管理框架都是我近期才接触到的,所以无法给出一些很全面的意见。

首先从门槛来说,我觉得MobX亲民许多我只要用 decorator 就能宣告完 observable 及可以去更改它的action

但在 BLoC 我需要分成三个地方来写:表示状态格式的 state 及可以变更状态的事件格式 event 最後才是在 bloc 实作当我遇到「哪个eventstate会有怎样的变化」。

但如果是大规模专案的话,MobX 的 Store 就会显得非常臃肿,而这时BLoC因为我们已经拆成三个档案,不论我是要查阅我能用哪些事件或者状态的格式都会变得相当容易。

以学习管道来说,BLoC乐胜毕竟BLoC已经在flutter社群流行蛮久的,所以不论是教学影片及文章我都觉得比MobX多了不只一点,但不论是MobX或者BLoC他们的官方文件都算蛮完整的,所以有办法硬啃官方文件的话其实不会差太多的。

在实作一些更新逻辑上我其实比较喜欢BLoC的写法,虽然MobX帮我们做了满多事情的但有时候会觉得太多,像是 ObservableFuture 为了它我必须额外写一些code才能正确的响应状态。导致有些async action 会写的跟一般平时 function 会不太一样,又或者是利用 computed 来实作。

但如果是BLoC 我要更新成怎样就直接 emit ,我的状态更新逻辑是我在管理,只有画面更新是响应式的。

当然这是风格抉择的问题,这两个框架我都很喜欢只能说还是要按照自己的需求来选择就是了。


今天的程序码:

https://github.com/zxc469469/flutter_rest_api_playground/tree/Day29

今天写完後就只剩下最後一天了,明天就用部署来结束好了。


<<:  Day 28 如何撰写表徵测试

>>:  那些被忽略但很好用的 Web API / Share

Day 22 - 天眼CNN 的耳朵和嘴巴 - RNN(3) -GRU

GRU vs. LSTM LSTM 在一个单一单元中要完成很多的操作。当使用更大的网络时,与循环神经...

Vue.js 从零开始:Vue CLI 环境说明

Webpack如何产生档案 经过上一篇对於Webpack的介绍後,相信大家都有一定的认识,这边我们用...

Day14-Kubernetes 那些事 - Deployment 与 ReplicaSet(二)

前言 昨天的文章介绍了 Deployment 以及 ReplicaSet 的基本介绍後,接下来要介绍...

IT铁人DAY 10-Abstract Factory 抽象工厂

  今天要认识的Abstract Factory与Factory Method很像,算是Factor...

[重构倒数第04天] - 轮播套件难道只可以做图片轮播吗

前言 该系列是为了让看过Vue官方文件或学过Vue但是却不知道怎麽下手去重构现在有的网站而去规画的系...