今天就來實作UI的部分,以及來小小的比較一下BLoC與MobX的差異
我們把這個頁面分成三個檔案posts_page
、posts_list
、 posts_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,而這裡會有兩個參數 bloc
及builder
,但如果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 的風險存在。
至於 PostListItem
及 BottomLoader
就只是單純的UI實作沒什麼特別好說的點,有興趣的讀者可以參閱文末的完整程式碼。
寫到這裡就來稍微比較一下 MobX及 BLoC,先打個預防針就是這兩個狀態管理框架都是我近期才接觸到的,所以無法給出一些很全面的意見。
首先從門檻來說,我覺得MobX親民許多我只要用 decorator 就能宣告完 observable
及可以去更改它的action
。
但在 BLoC 我需要分成三個地方來寫:表示狀態格式的 state
及可以變更狀態的事件格式 event
最後才是在 bloc
實作當我遇到「哪個event
後state
會有怎樣的變化」。
但如果是大規模專案的話,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
今天寫完後就只剩下最後一天了,明天就用部署來結束好了。