iT邦幫忙

2021 iThome 鐵人賽

DAY 29
1
Modern Web

Flutter web 的奇妙冒險系列 第 29

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 | 狀態管理-從官方範例來看如何使用BLoC
下一篇
Day 30 | 將flutter web 部署至 netlify
系列文
Flutter web 的奇妙冒險30

1 則留言

0
juck30808
iT邦新手 3 級 ‧ 2021-10-12 18:27:58

第29天! 恭喜即將完賽 (拍手!!!

Todd iT邦新手 3 級 ‧ 2021-10-12 20:02:13 檢舉

感謝~
剛剛看了一下發現居然也是雲科人XD

我要留言

立即登入留言