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

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

image

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

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

image
  1. 引入 Redux 的第三方庫

pubspec.yaml 文件中引入 flutter_bloc 第三方庫支持 bloc 功能。

  # 引入 bloc 第三方庫
  flutter_bloc: ^0.9.0
  1. 使用 Bloc 插件

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

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

比如 list 頁面,該插件會生成相應的類

image

從生成的五個文件中也可以看到,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 的架構會相對比較友好。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,156評論 6 529
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,866評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 174,880評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,398評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,202評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,743評論 1 320
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,822評論 3 438
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 41,962評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,476評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,444評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,579評論 1 365
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,129評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,840評論 3 344
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,231評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,487評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,177評論 3 388
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,568評論 2 370

推薦閱讀更多精彩內容