前言:
最近工作较忙,利用了一些晚上下班的时间,终于写完了一个bloc Demo,之前在学习Bloc的时候看了很多文章,虽然有很多的文章在说flutter bloc模式的应用,但是百分之八九十的文章都是在说,真正写使用bloc作者开发的flutter_bloc却少之又少。没办法,只能去bloc的github上去找使用方式,最后去bloc官网翻文档。本篇文章着重讲的是bloc在项目中的使用,以及常见的场景和使用时遇到的问题。
针对网络请求和一些常用工具也进行了封装,写了几个有针对性的页面,做项目的话可以直接拿来用。老规矩先上效果。
![]() |
![]() |
![]() |
|---|
正文:
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聚合体,既有构建功能又有监听功能。里面有builderlistenerbuildWhenlistenWhen四个方法,也很常用。
使用 Bloc 和 cubit 开发一个页面完整流程。
- bloc模式: 1.创建类,生成
bloc类和样板代码,这里bloc官方提供的有插件,在Android Studio安装使用即可,不在多说。 2.绑定bloc和context,使用BlocProvider
less
BlocProvider<NovelDetailNavBloc>(
create: (BuildContext context) => NovelDetailNavBloc(),
child: NovelDetailPage(
imageUrl: imageUrl,
),
)
3.定义Event
scala
/// 获取数据
class GetNovelDetailEvent extends NovelDetailEvent {
GetNovelDetailEvent(this.mainPath, this.seriesPath, this.recommendPath);
final String mainPath;
final String seriesPath;
final String recommendPath;
}
4.定义State
ini
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发送更新通知
ini
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完成赋值操作。
scss
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中有具体的例子。 这就就不贴代码了。还是结合官方图,可以快速理解。
buildWhen:
在实际的开发工作中,并不是每次state里面的属性发生变化都需要build页面,这个时候就需要buildWhen了.
- 使用场景 登录注册,登录时有两个输入框,一个输入手机号码,一个输入密码,那么当输入手机号码的时候,只需要刷新手机号码的
widget,输入密码时,只需要刷新密码的widget,那么这种场景就需要buildWhen来实现。首先来看一下buildWhen的内部实现
arduino
/// 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
kotlin
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了。
kotlin
listener: _listener,
listenWhen: (state1, state2) {
if (state1.netLoadCount != state2.netLoadCount) {
return true;
}
return false;
},
在state中,定义一个属性netLoadCount ,只有当前state的netLoadCount和上一个state的netLoadCount不一致时才会监听,才会去执行事件。
颗粒化刷新或局部刷新:
-
例子1 使用
buildWhen来实现,就是上面实现登录注册页面的逻辑,不在多说。 -
例子2
以本Demo中的这个页面为例,首先来说,这个页面所有的数据都是网络请求而来,然后页面往上滑动时,根据滑动距离来改变导航栏的透明度和页面变化。那么就是当一开始进入页面,进行网络请求,然后build页面,当滑动页面时,只需要build导航栏widget就可以了,因为除了导航栏变化,别的都没有变化,没有必要从此页面的根节点进行刷新。 -
代码实现方案1:(两个Bloc实现): 当滑动
ListView时,页面会在此BlocBuilder下全部都会刷新,然而,我们在滑动ListView时,只需要刷新导航栏widget,所以,可以再创建一个NavBlocNavStateNavEvent了,导航栏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中如果这样请求网络会报错。代码如下:
scss
https().updateData(params,
onSuccess: (data) {
emit();
});
之前遇到过这样的问题,具体的报错信息就不贴了,bloc抛出的大致意思就是event方法是从上往下同步顺序执行的,所以当onSuccess 异步回调时,这个event 方法实际已经被消费掉了,所以就报错了。这是bloc模式下event 的问题,在cubit模式下,没有此问题,可以放心大胆的写。为了在项目中使用方便,避免出错,网络统一封装成了这样,在哪种模式下都没有问题。
csharp
ResponseModel? responseModel =
await LttHttp().request<CartoonModelData>(event.mainPath, method: HttpConfig.get);
- 网络请求封装思路 返回值用通用的ResponseModel来接受,里面有
codemessage<T>data方便根据不同的code值进行不同处理逻辑,然后request方法需要传入一个泛型T,传入的这个泛型T,就是返回值ResponseModel的data,可能思路有点绕,看看代码就明白了。这样把json解析啥的都放在网络里面去处理了,很方便。
ini
await LttHttp().request<CartoonModelData>(event.mainPath, methodHttpConfig.get);
state.mainModel = responseModel.data;
-
json转model 使用
FlutterJsonBeanFactory插件来完成,使用方便,教程可以自行百度。使用时要注意引入别的model时,用绝对路径还是相对路径的问题。
BasePage设计
常规设计吧,满足日常开发使用,属性如下。
ini
/// 是否渲染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 来实现。
perl
/// 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改变状态代码
ini
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代码
kotlin
@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 中处理代码
kotlin
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不易,如果感觉对您有些许的帮助,欢迎点赞评论。
声明:
仅开源供大家学习使用,禁止从事商业活动,如出现一切法律问题自行承担!!!
仅学习使用,如有侵权,造成影响,请联系本人删除,谢谢 项目地址


