Flutter狀態管理學習手冊(三)——Bloc

Windin發表於2019-05-02

一、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 進行互動。

Flutter狀態管理學習手冊(三)——Bloc

由此可見,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 的實踐

這裡以常見的獲取列表選擇列表為例子。一個頁面用於展示選中項和跳轉到列表,一個頁面用於顯示列表。

Flutter狀態管理學習手冊(三)——Bloc

  1. 引入 Redux 的第三方庫

pubspec.yaml 檔案中引入 flutter_bloc 第三方庫支援 bloc 功能。

  # 引入 bloc 第三方庫
  flutter_bloc: ^0.9.0
複製程式碼
  1. 使用 Bloc 外掛

這一步可有可無,但使用外掛會方便開發,不使用的話也沒什麼問題。

Bloc 官方提供了 VSCode 和 Android studio 的外掛,方便生成 Bloc 框架用到的相關類。 下文以 Android studio 的外掛為例。

比如 list 頁面,該外掛會生成相應的類

Flutter狀態管理學習手冊(三)——Bloc

從生成的五個檔案中也可以看到,list_bloc 負責承載業務邏輯,list_page 負責編寫 UI 介面,list_eventlist_state 分別是事件和狀態,其中 list.dart 檔案是用於匯出前面四個檔案的。

具體使用可見

Android studio 的 Bloc 外掛

VSCode 的 Bloc 外掛

  1. 使用 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()));
  }
複製程式碼
  1. 展示頁面 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("");
    }),
複製程式碼
  1. 列表頁面 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 的架構會相對比較友好。

相關文章