一、Bloc 介紹
Bloc 的名字比較新穎,這個狀態管理框架的目的是將 UI 層和業務邏輯進行分離。Bloc 的復雜度處于 ScopedModel 和 Redux 之間,相較于 ScopedModel,Bloc 擁有分明的架構處于業務邏輯,相較于 Redux,Bloc 著重于業務邏輯的分解,使得整個框架對于開發來講簡單實用。
二、Bloc 的層次結構
Bloc 分為三層:
- Data Layer(數據層),用于提供數據。
- Bloc(Business Logic) Layer(業務層),通過繼續 Bloc 類實現,用于處理業務邏輯。
- Presentation Layer(表現層),用于 UI 構建。
Presentation Layer 只與 Bloc Layer 交互,Data Laye 也只與 Bloc Layer 交互。Bloc Layer 作為重要一層,處于表現層和數據層之間,使得 UI 和數據通過 Bloc Layer 進行交互。
由此可見,Bloc 的架構和客戶端主流的 MVC 和 MVP 架構比較相似,但也存在 Event 和 State 的概念一同構成響應式框架。
三、Bloc 需要知道的概念
BlocProvider,通常做為 App 的根布局。BlocProvider 可以保存 Bloc,在其它頁面通過BlocProvider.of<Bloc>(context)
獲取 Bloc。
Event,用戶操作 UI 后發出的事件,用于通知 Bloc 層事件發生。
State,頁面狀態,可用于構建 UI。通常是 Bloc 將接收到的 Event 轉化為 State。
Bloc 架構的核心是 Bloc 類,Bloc 類是一個抽象類,有一個 mapEventToState(event)
方法需要實現。mapEventToState(event)
顧名思義,就是將用戶點擊 View 時發出的 event 轉化為構建 UI 所用的 State。另外,在 StatefulWidget 中使用 bloc 的話,在 widget dispose 時,要調用 bloc.dispose()
方法進行釋放。
四、Bloc 的實踐
這里以常見的獲取列表選擇列表為例子。一個頁面用于展示選中項和跳轉到列表,一個頁面用于顯示列表。
- 引入 Redux 的第三方庫
在 pubspec.yaml
文件中引入 flutter_bloc
第三方庫支持 bloc 功能。
# 引入 bloc 第三方庫
flutter_bloc: ^0.9.0
- 使用 Bloc 插件
這一步可有可無,但使用插件會方便開發,不使用的話也沒什么問題。
Bloc 官方提供了 VSCode 和 Android studio 的插件,方便生成 Bloc 框架用到的相關類。
下文以 Android studio 的插件為例。
比如 list 頁面,該插件會生成相應的類
從生成的五個文件中也可以看到,list_bloc
負責承載業務邏輯,list_page
負責編寫 UI 界面,list_event
和 list_state
分別是事件和狀態,其中 list.dart
文件是用于導出前面四個文件的。
具體使用可見
- 使用 BlocProvider 作為根布局
在 main.dart
中,使用 BlocProvider 作為父布局包裹,用于傳遞需要的 bloc。Demo 中包含兩個頁面,一個是展示頁面 ShowPage,一個是列表頁面 ListPage。
上面講到,Bloc 的核心功能在于 Bloc 類,對于展示頁面 ShowPage,會有一個 ShowBloc 繼續自 Bloc 類。由于展示頁面 ShowPage 會和列表頁面 ListPage 有數據的互動,所以這里將 ShowBloc 保存在 BlocProvider 中進行傳遞。
@override
Widget build(BuildContext context) {
return BlocProvider(
bloc: _showBloc,
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ShowPage()));
}
- 展示頁面 ShowPage
① ShowEvent
列表的 item 點擊后,需要發送一個 event 通知其它頁面列表被選中,這里定義一個 SelectShowEvent 作為這種 event 通知。
class SelectShowEvent extends ShowEvent {
String selected;
SelectShowEvent(this.selected);
}
② ShowState
State 用于表示一種界面狀態,即一個 State 就對應一個界面。插件在一開始會生成一個默認狀態,InitialShowState。我們可以使用 InitialShowState 來代表初始的界面。另外,我們自己定義一種狀態,SelectedShowState,代表選中列表后的 State。
@immutable
abstract class ShowState {}
class InitialShowState extends ShowState {}
class SelectedShowState extends ShowState {
String _selectedString = "";
String get selected => _selectedString;
SelectedShowState(this._selectedString);
}
③ ShowBloc
Bloc 的主要職責是接收 Event,然后把 Event 轉化為對應的 State。這里的 ShowBloc 繼續自 Bloc,需要重寫實現抽象方法 mapEventToState(event)
。在這個方法中,我們判斷傳過來的 event 是不是 SelectShowEvent,是則拿到 SelectShowEvent 中的 selected 變量去構建 SelectedShowState。mapEventToState(event)
返回的是一個 Stream,我們通過 yield 關鍵字去返回一個 SelectedShowState。
class ShowBloc extends Bloc<ShowEvent, ShowState> {
@override
ShowState get initialState => InitialShowState();
@override
Stream<ShowState> mapEventToState(
ShowEvent event,
) async* {
if (event is SelectShowEvent) {
yield SelectedShowState(event.selected);
}
}
}
④ ShowPage
在 ShowPage 的界面上,我們需要根據 showBloc 中是否有被選中的列表項目去展于頁面,所以這里我們先使用使用BlocProvider.of<ShowBloc>(context)
去拿到 showBloc,接著再用 BlocBuilder 根據 showBloc 構建界面。使用 BlocBuilder 的好處就是可以讓頁面自動響應 showBloc 的變化而變化。
var showBloc = BlocProvider.of<ShowBloc>(context);
...
BlocBuilder(
bloc: showBloc,
builder: (context, state) {
if (state is SelectedShowState) {
return Text(state.selected);
}
return Text("");
}),
- 列表頁面 ListPage
① ListEvent
列表頁面,我們一開始需要從網絡中拉取列表數據,所以定義一個 FetchListEvent 事件在進入頁面時通知 ListBloc 去獲取列表。
@immutable
abstract class ListEvent extends Equatable {
ListEvent([List props = const []]) : super(props);
}
class FetchListEvent extends ListEvent {}
② ListState
InitialListState 是插件默認生成的初始狀態,另外定義一個 FetchListState 代表獲取列表完成的狀態。
@immutable
abstract class ListState extends Equatable {
ListState([List props = const []]) : super(props);
}
class InitialListState extends ListState {}
class FetchListState extends ListState {
List<String> _list = [];
UnmodifiableListView<String> get list => UnmodifiableListView(_list);
FetchListState(this._list);
}
③ ListBloc
在 ListBloc 中,進行從網絡獲取列表數據的業務。這里通過一個延時操作摸擬網絡請求,最后用 yield 返回列表數據。
class ListBloc extends Bloc<ListEvent, ListState> {
@override
ListState get initialState => InitialListState();
@override
Stream<ListState> mapEventToState(
ListEvent event,
) async* {
if (event is FetchListEvent) {
// 模擬網絡請求
await Future.delayed(Duration(milliseconds: 2000));
var list = [
"1. Bloc artitechture",
"2. Bloc artitechture",
"3. Bloc artitechture",
"4. Bloc artitechture",
"5. Bloc artitechture",
"6. Bloc artitechture",
"7. Bloc artitechture",
"8. Bloc artitechture",
"9. Bloc artitechture",
"10. Bloc artitechture"
];
yield FetchListState(list);
}
}
}
④ ListPage
在列表頁面初始化時有兩個操作,一個是初始化 listBloc,一個是發出列表請求的 Event。
@override
void initState() {
bloc = ListBloc(); // 初始化listBloc
bloc.dispatch(FetchListEvent()); // 發出列表請求事件
super.initState();
}
接下用,便是用 BlocBuilder 去響應狀態。當 state 是 InitialListState,說明未獲取列表,則顯示 loading 界面,當 state 是 FetchListState 時,說明已經成功獲取列表,顯示列表界面。
body: BlocBuilder(
bloc: bloc,
builder: (context, state) {
// 根據狀態顯示界面
if (state is InitialListState) {
// 顯示 loading 界面
return buildLoad();
} else if (state is FetchListState) {
// 顯示列表界面
var list = state.list;
return buildList(list);
}
}));
最后,記得對 bloc 進行 dispose()
。
@override
void dispose() {
bloc.dispose();
super.dispose();
}
具體代碼可以到 github 查看。
總結
在 Bloc 的架構中,將一個頁面和一個 Bloc 相結合,由頁面產生 Event,Bloc 根據業務需要將 Event 轉化為 State,再把 State 交給頁面中的 BlocBuilder 構建 UI。Demo 中只是給出了簡單的狀態管理,實際項目中,比如網絡請求,有請求中、請求成功、請求失敗的多種狀態,可以做適當封裝使 Bloc 更加易用。相比于 Redux,Bloc 不需要將所有狀態集中管理,這樣對于不同模塊的頁面易于拆分,對于代碼量比較大的客戶端而言,Bloc 的架構會相對比較友好。