前言:
-
最近工作較忙,利用了一些晚上下班的時間,終于寫完了一個bloc Demo,之前在學習Bloc的時候看了很多文章,雖然有很多的文章在說flutter bloc模式的應用,但是百分之八九十的文章都是在說,真正寫使用bloc作者開發的flutter_bloc卻少之又少。沒辦法,只能去bloc的github上去找使用方式,最后去bloc官網翻文檔。本篇文章著重講的是bloc在項目中的使用,以及常見的場景和使用時遇到的問題。
-
針對網絡請求和一些常用工具也進行了封裝,寫了幾個有針對性的頁面,做項目的話可以直接拿來用。老規矩先上效果。
cubit-list
|
bloc-grid
|
bloc-stagger
|
---|
正文:
flutter_bloc使用將從下圖的三個維度說明
-
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的實例。通俗來講就是完成context
和bloc
對象的綁定,在我們需要用到bloc
的時候,通過context
就可以拿到bloc
對象。BlocProvider
使用的時機很重要,稍有不慎就會報錯,下面會說。MultiBlocProvider
主要的使用場景就是在main
方法中,綁定多個context
和bloc
對象,一般綁定的是在App
一啟動就需要展示處理邏輯的頁面。BlocBuilder:
是一個Flutter Bloc提供的小部件,它會在狀態發生變化時自動重建,并用于構建頁面。通俗來講就是,當state
對象內部的值發生變化時,BlocBuilder
會自動重現構建來刷新widget
。還用一個很重要的方法buildWhen:
就是可以通過stateh或者view里面的其他屬性
來判斷頁面是否需要重新進行構建。BlocListener:
監聽bloc
里面的狀態,通過也是通過state
進行回調,來執行某個事件,比如說通知刷新或界面跳轉...
里面也有一個重要的方法listenWhen:
可以有選擇性的進行監聽。BlocConsumer:
BlocBuilder
和BlocListener
聚合體,既有構建功能又有監聽功能。里面有builder
listener
buildWhen
listenWhen
四個方法,也很常用。-
使用 Bloc 和 cubit 開發一個頁面完整流程。
bloc模式:
1.創建類,生成bloc
類和樣板代碼,這里bloc
官方提供的有插件,在Android Studio
安裝使用即可,不在多說。
2.綁定bloc
和context
,使用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();
});
},
);
}
完成上面幾步,就基本玩成了一個網絡列表的開發。
再結合這張官方圖,有助于快速調整思路。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
樹的范圍和次數,極大的提升了性能。也是顆粒化刷新的一種常用方式。實現原理
底層使用provider
的Select
來實現的,下篇文章會著重講一個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
的根節點,滑動視圖ListView
和NavWidget
分別用同一個BlocBuilder
來包裹,根據不同的條件來選擇重新build
這兩個widget
.在本Demo中`有案例實現可自行查看.-
單頁面多網絡請求實現思路
思路1
定義一個bloc
或者cubit
,使用一個BlocBuilder
,BlocBuilder
放在頁面根節點.所有接口串行處理,等數據全部請求成功,更新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
,就是返回值ResponseModel
的data
,可能思路有點繞,看看代碼就明白了。這樣把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不易,如果感覺對您有些許的幫助,歡迎點贊評論。
聲明:
僅開源供大家學習使用,禁止從事商業活動,如出現一切法律問題自行承擔!!!
僅學習使用,如有侵權,造成影響,請聯系本人刪除,謝謝