Flutter Bloc搭建通用項目架構

前言:

  • 最近工作較忙,利用了一些晚上下班的時間,終于寫完了一個bloc Demo,之前在學習Bloc的時候看了很多文章,雖然有很多的文章在說flutter bloc模式的應用,但是百分之八九十的文章都是在說,真正寫使用bloc作者開發的flutter_bloc卻少之又少。沒辦法,只能去bloc的github上去找使用方式,最后去bloc官網翻文檔。本篇文章著重講的是bloc在項目中的使用,以及常見的場景和使用時遇到的問題。
  • 針對網絡請求和一些常用工具也進行了封裝,寫了幾個有針對性的頁面,做項目的話可以直接拿來用。老規矩先上效果。
cubit-list
bloc-grid
bloc-stagger

正文:

flutter_bloc使用將從下圖的三個維度說明


image
  • bloc 基本思想

Flutter Bloc(Business Logic Component)是一種基于流的狀態管理解決方案,它將應用程序的狀態與事件(也稱為操作)分離開來。Bloc接收事件并根據它們來更新應用程序的狀態。Bloc通常由三個主要部分組成:事件(input)、狀態(output)和業務邏輯。使用Flutter Bloc,您可以將應用程序分解為不同的模塊,從而使其易于維護和擴展。

  • Flutter Bloc的核心概念:
  • State:
    表示應用程序的狀態。它可以是任何類型的對象,例如數字、字符串、布爾值或自定義類。是Bloc提供給外部的數據媒介,view層通過state獲取bloc里面的數據。

  • Event:
    表示操作或事件,例如按鈕按下、API調用或用戶輸入,常用場景進入頁面進行網絡數據請求,就定義一個網絡請求的Event,當用戶點擊按鈕就定義一個點擊的Event,然后去bloc內部去處理數據然后通過state回調給view來更新狀態。

  • Bloc:
    通過Event獲取外部操作,在內部處理邏輯接口請求或者數據處理,然后更新state,通過state把最新數據傳遞給view刷新狀態。
    BlocProvider:是一個Flutter Bloc提供的小部件,它可以幫助我們在整個應用程序中共享和提供Bloc的實例。

  • Cubit:
    相比bloc省去了Event層,view可以直接進行調用內部方法,同樣也是在內部處理邏輯接口請求或者數據處理,然后更新state,通過state把最新數據傳遞給view刷新狀態。

  • BlocProvider:
    是一個Flutter Bloc提供的小部件,它可以幫助我們在整個應用程序中共享和提供Bloc的實例。通俗來講就是完成contextbloc對象的綁定,在我們需要用到bloc的時候,通過context就可以拿到bloc對象。BlocProvider使用的時機很重要,稍有不慎就會報錯,下面會說。

  • MultiBlocProvider
    主要的使用場景就是在main方法中,綁定多個contextbloc對象,一般綁定的是在App一啟動就需要展示處理邏輯的頁面。

  • BlocBuilder:
    是一個Flutter Bloc提供的小部件,它會在狀態發生變化時自動重建,并用于構建頁面。通俗來講就是,當state對象內部的值發生變化時,BlocBuilder會自動重現構建來刷新widget。還用一個很重要的方法buildWhen:就是可以通過stateh或者view里面的其他屬性來判斷頁面是否需要重新進行構建。

  • BlocListener:
    監聽bloc里面的狀態,通過也是通過state進行回調,來執行某個事件,比如說通知刷新或界面跳轉...里面也有一個重要的方法listenWhen:可以有選擇性的進行監聽。

  • BlocConsumer:
    BlocBuilderBlocListener聚合體,既有構建功能又有監聽功能。里面有builder listener buildWhen listenWhen四個方法,也很常用。

  • 使用 Bloc 和 cubit 開發一個頁面完整流程。
  • bloc模式:
    1.創建類,生成bloc類和樣板代碼,這里bloc官方提供的有插件,在Android Studio安裝使用即可,不在多說。
    2.綁定bloccontext,使用BlocProvider

BlocProvider<NovelDetailNavBloc>(
          create: (BuildContext context) => NovelDetailNavBloc(),
          child: NovelDetailPage(
            imageUrl: imageUrl,
          ),
        )

3.定義Event

/// 獲取數據
class GetNovelDetailEvent extends NovelDetailEvent {
  GetNovelDetailEvent(this.mainPath, this.seriesPath, this.recommendPath);

  final String mainPath;
  final String seriesPath;
  final String recommendPath;
}

4.定義State

class NovelDetailState extends BaseState {
  CartoonModelData? mainModel;
  List<CartoonRecommendDataInfos>? recommendList;
  List<CartoonSeriesDataSeriesComics>? seriesList;

  NovelDetailState init() {
    return NovelDetailState()
      ..netState = NetState.loadingState
      ..mainModel = CartoonModelData()
      ..recommendList = []
      ..seriesList = [];
  }

  NovelDetailState clone() {
    return NovelDetailState()
      ..netState = netState
      ..mainModel = mainModel
      ..recommendList = recommendList
      ..seriesList = seriesList;
  }
}

5.在Bloc處理邏輯,并更新state發送更新通知

NovelDetailBloc() : super(NovelDetailState().init()) {
    on<GetNovelDetailEvent>(_getNovelDetailEvent);
  }

  Future<void> _getNovelDetailEvent(event, emit) async {
    XsEasyLoading.showLoading();

    /// 主數據
    ResponseModel? responseModel =
        await LttHttp().request<CartoonModelData>(event.mainPath, method: HttpConfig.mock);

    /// 同系列數據
    ResponseModel? responseModel2 =
        await LttHttp().request<CartoonSeriesData>(event.seriesPath, method: HttpConfig.mock);

    /// 推薦數據
    ResponseModel? responseModel3 =
        await LttHttp().request<CartoonRecommendData>(event.recommendPath, method: HttpConfig.mock);
    XsEasyLoading.dismiss();
    state.mainModel = responseModel.data;
    CartoonSeriesData cartoonSeriesData = responseModel2.data;
    state.seriesList = cartoonSeriesData.seriesComics;
    CartoonRecommendData cartoonRecommendData = responseModel3.data;
    state.recommendList = cartoonRecommendData.infos;
    state.netState = NetState.dataSuccessState;
    emit(state.clone());
  }

6.在view中搭建UI,通過state完成賦值操作。

Widget buildPage(BuildContext context) {
    return BlocConsumer<BlocStaggeredGridViewBloc, StaggeredGridViewState>(
      listener: _listener,
      builder: (context, state) {
        return resultWidget(state, (baseState, context) => mainWidget(state), refreshMethod: () {
          _pageNum = 1;
          _getData();
        });
      },
    );
  }

完成上面幾步,就基本玩成了一個網絡列表的開發。

bloc模式

再結合這張官方圖,有助于快速調整思路。Demo

  • cubit模式:
    cubit模式和bloc的不同就是省去了Event層,其他的用法都是一樣,Demo中有具體的例子。
    這就就不貼代碼了。還是結合官方圖,可以快速理解。

    cubit模式

  • buildWhen:

在實際的開發工作中,并不是每次state里面的屬性發生變化都需要build頁面,這個時候就需要buildWhen了.

  • 使用場景
    登錄注冊,登錄時有兩個輸入框,一個輸入手機號碼,一個輸入密碼,那么當輸入手機號碼的時候,只需要刷新手機號碼的widget,輸入密碼時,只需要刷新密碼的widget,那么這種場景就需要buildWhen來實現。首先來看一下buildWhen的內部實現
/// Signature for the `buildWhen` function which takes the previous `state` and
/// the current `state` and is responsible for returning a [bool] which
/// determines whether to rebuild [BlocBuilder] with the current `state`.
typedef BlocBuilderCondition<S> = bool Function(S previous, S current);

大致意思就是該方法返回兩個state,根據之前的state和 當前的state來判斷是否需要刷新當前的widget,看到這里這種場景就很好實現了。代碼如下:Demo

 buildWhen: (previous, current) {
        if (type == 1) {
          return previous.phoneNumber != current.phoneNumber;
        } else {
          return previous.codeNumber != current.codeNumber;
        }
      },
  • 好處
    減少每次build樹的范圍和次數,極大的提升了性能。也是顆粒化刷新的一種常用方式。

  • 實現原理
    底層使用providerSelect來實現的,下篇文章會著重講一個Select.

  • listenWhen:

當在bloc或者cubit中進行網絡請求或者數據處理時,往往widget需要根據處理結果去執行某些事件,這時候就需要使用listen了。

  • 使用場景
    bloc或者cubit中網絡請求成功后,在 widget中,需要相應的結束下拉刷新或者上拉加載或者展示沒有更多數據了,這時候在widget中使用BlocListenr或者BlocConsumer,然后實現listen監聽方法即可,但是最高效的使用listernWhen來實現,因為實際的開發當中,bloc或者cubit中會處理很多的邏輯,比如處理點贊或者收藏邏輯時,就不需要widget里面處理結束下拉刷新等事件了,只需要build頁面即可。所以這種場景最好使用listernWhen了。
listener: _listener,
      listenWhen: (state1, state2) {
        if (state1.netLoadCount != state2.netLoadCount) {
          return true;
        }
        return false;
      },

state中,定義一個屬性netLoadCount,只有當前state的netLoadCount上一個state的netLoadCount不一致時才會監聽,才會去執行事件。

  • 顆粒化刷新或局部刷新:
  • 例子1
    使用buildWhen來實現,就是上面實現登錄注冊頁面的邏輯,不在多說。

  • 例子2

    顆粒化刷新例2

    以本Demo中的這個頁面為例,首先來說,這個頁面所有的數據都是網絡請求而來,然后頁面往上滑動時,根據滑動距離來改變導航欄的透明度和頁面變化。那么就是當一開始進入頁面,進行網絡請求,然后build頁面,當滑動頁面時,只需要build導航欄widget就可以了,因為除了導航欄變化,別的都沒有變化,沒有必要從此頁面的根節點進行刷新。

  • 代碼實現方案1:(兩個Bloc實現):
    當滑動ListView時,頁面會在此BlocBuilder下全部都會刷新,然而,我們在滑動ListView時,只需要刷新導航欄widget,所以,可以再創建一個NavBloc NavState NavEvent了,導航欄widget用新的導航欄的BlocBuilder包裹,當滑動ListView時,更新新創建的NavState這樣導航欄widget就刷新了,而根節點的state并沒有改變,所以整體頁面不會重新build這樣就實現了局部刷新。

  • 代碼實現方案2:(一個Bloc實現):
    使用buildWhen來實現,BlocBuilder不放在page的根節點,滑動視圖ListViewNavWidget分別用同一個BlocBuilder來包裹,根據不同的條件來選擇重新build這兩個widget.在本Demo中`有案例實現可自行查看.

  • 單頁面多網絡請求實現思路
  • 思路1
    定義一個bloc或者cubit,使用一個BlocBuilderBlocBuilder放在頁面根節點.所有接口串行處理,等數據全部請求成功,更新state,調用emit()方法,刷新頁面。loading時間會長,體驗不是很好。

  • 思路2
    定義一個bloc或者cubit,所有的接口并行處理,最后使用Future.wait來組合數據。loading時間短,體驗好,注意異常邏輯處理。具體使用那種思路來實現,具體業務具體分析吧,本Demo中兩種思路都有實現。

  • 針對bloc特性 封裝網絡請求

使用bloc多了,就會發現在event中如果這樣請求網絡會報錯。代碼如下:

https().updateData(params,
              onSuccess: (data) {
                emit();
          });

之前遇到過這樣的問題,具體的報錯信息就不貼了,bloc拋出的大致意思就是event方法是從上往下同步順序執行的,所以當onSuccess異步回調時,這個event方法實際已經被消費掉了,所以就報錯了。這是bloc模式下event的問題,在cubit模式下,沒有此問題,可以放心大膽的寫。為了在項目中使用方便,避免出錯,網絡統一封裝成了這樣,在哪種模式下都沒有問題。

 ResponseModel? responseModel =
        await LttHttp().request<CartoonModelData>(event.mainPath, method: HttpConfig.get);
  • 網絡請求封裝思路
    返回值用通用的ResponseModel來接受,里面有code message <T>data 方便根據不同的code值進行不同處理邏輯,然后request方法需要傳入一個泛型T,傳入的這個泛型T,就是返回值ResponseModeldata,可能思路有點繞,看看代碼就明白了。這樣把 json解析啥的都放在網絡里面去處理了,很方便。
  await LttHttp().request<CartoonModelData>(event.mainPath, methodHttpConfig.get);
state.mainModel = responseModel.data;
  • json轉model
    使用FlutterJsonBeanFactory插件來完成,使用方便,教程可以自行百度。使用時要注意引入別的model時,用絕對路徑還是相對路徑的問題。

  • BasePage設計

常規設計吧,滿足日常開發使用,屬性如下。

/// 是否渲染buildPage內容
  bool _isRenderPage = false;

  /// 是否渲染導航欄
  bool isRenderHeader = true;

  /// 導航欄顏色
  Color? navColor;

  /// 左右按鈕橫向padding
  final EdgeInsets _btnPaddingH = EdgeInsets.symmetric(horizontal: 14.w, vertical: 14.h);

  /// 導航欄高度
  double navBarH = AppBar().preferredSize.height;

  /// 頂部狀態欄高度
  double statusBarH = 0.0;

  /// 底部安全區域高度
  double bottomSafeBarH = 0.0;

  /// 頁面背景色
  Color pageBgColor = const Color(0xFFF9FAFB);

  /// header顯示頁面title
  String pageTitle = '';

  /// 是否允許某個頁iOS滑動返回,Android物理返回鍵返回
  bool isAllowBack = true;

  bool resizeToAvoidBottomInset = true;

  /// 是否允許點擊返回上一頁
  bool isBack = true;
  • BaseState設計

項目里面所有的 state都繼承于 BaseState為啥要這樣做??
因為在開發一個頁面需要根據網絡返回的狀態來判斷顯示正常頁面 空數據頁面 網絡報錯頁面等等,也就是說頁面的顯示狀態是由state來控制的,那么這些代碼肯定不可能,新創建一個頁面就寫一堆判斷,這些判斷通過把BaseState交給BasePage來實現。

/// BaseState
/// 項目中所有需要根據網絡狀態顯示頁面的state必須繼承于BaseState
enum NetState {
  /// 初始狀態
  initializeState,

  /// 加載狀態
  loadingState,

  /// 錯誤狀態,顯示失敗界面
  error404State,

  /// 錯誤狀態,顯示刷新按鈕
  errorShowRefresh,

  /// 空數據狀態
  emptyDataState,

  /// 加載超時
  timeOutState,

  /// 數據獲取成功狀態
  dataSuccessState,
}

abstract class BaseState {
  /// 頁面狀態
  NetState netState = NetState.loadingState;

  /// 是否還有更多數據
  bool? isNoMoreDataState;

  /// 數據是否請求完成
  bool? isNetWorkFinish;

  /// 數據源
  List? dataList;

  /// 網絡加載次數 用這個屬性判斷 BlocConsumer 是否需要監聽刷新數據
  int netLoadCount = 0;
}

  • 思路
    bloc或者 cubit中通過網絡返回ResponseModel中的code來給state賦值,在widget中,將state傳給BasePage,最終BasePage會根據state返回一個界面正確的展示效果。
處理網絡層根據 ResponseModel 給state改變狀態代碼
class HandleState {
  static handle(ResponseModel responseModel, BaseState state) {
    if (responseModel.code == 100200) {
      if ((state.dataList ?? []).isEmpty) {
        state.netState = NetState.emptyDataState;
      } else {
        state.netState = NetState.dataSuccessState;
      }
    } else if (responseModel.code == 404) {
      state.netState = NetState.error404State;
    } else if (responseModel.code == -100) {
      state.netState = NetState.timeOutState;
    } else {
      state.netState = NetState.errorShowRefresh;
    }
  }
}

widget中build代碼
@override
  Widget buildPage(BuildContext context) {
    return BlocConsumer<MessageModuleCubit, MessageModuleState>(
      listener: _listener,
      listenWhen: (state1, state2) {
        if (state1.netLoadCount != state2.netLoadCount) {
          return true;
        }
        return false;
      },
      builder: (context, state) {
        return resultWidget(state, (baseState, context) => mainWidget(state), refreshMethod: () {
          _pageNum = 1;
          _getData();
        });
      },
    );
  }
BasePage 中處理代碼
Widget resultWidget(BaseState state, BodyBuilder builder, {Function? refreshMethod}) {
    if (state.netState == NetState.loadingState) {
      return const SizedBox();
    } else if (state.netState == NetState.emptyDataState) {
      return emptyWidget('暫無數據');
    } else if (state.netState == NetState.errorShowRefresh) {
      return errorWidget('網絡錯誤', refreshMethod ?? () {});
    } else if (state.netState == NetState.error404State) {
      return net404Widget('頁面404了');
    } else if (state.netState == NetState.initializeState) {
      return emptyWidget('NetState 未初始化,請將狀態置為dataSuccessState');
    } else if (state.netState == NetState.timeOutState) {
      return timeOutWidget('加載超時,請重試~', refreshMethod ?? () {});
    } else {
      return builder(state, context);
    }
  }

另外,所有的異常視圖都支持在widget中重寫,如果有特殊情況樣式的展示,直接重寫即可。

  • 路由設計

使用的是fluro,使用人數和點贊量很高,也比較好用,就不多說了。

  • 各種base類的設計

為了更高效的開發,Demo里面封裝了常用widget的封裝,比如BaseListView BaseGridView等等,代碼寫起來簡直不要太爽!

結束:

就寫到這里吧,針對于Bloc的項目架構設計已經可以了,一直認為,技術就是用來溝通的,沒有溝通就沒有長進,在此,歡迎各種大佬吐槽溝通。Coding不易,如果感覺對您有些許的幫助,歡迎點贊評論。

聲明:

僅開源供大家學習使用,禁止從事商業活動,如出現一切法律問題自行承擔!!!

僅學習使用,如有侵權,造成影響,請聯系本人刪除,謝謝

安裝地址

項目地址

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

推薦閱讀更多精彩內容