1.页面状态的bloc封装
1.1 定义一个基类用于bloc用于处理页面状态
状态主要有:loading,error,empty,以及展示内容的showContent
enum PageEnum {
showLoading,
showError,
showEmpty,
showContent,
}
1.2 定义一个枚举表示页面状态,另外还需定义事件的类,传递一些必要的数据
bloc流供baseWidget做状态的变化
class PageStatusEvent {
String errorDesc; //错误数据,主要是展示错误页面
bool isRefresh;//主要用于list列表数据
PageEnum pageStatus; 页面状态
PageStatusEvent({this.errorDesc,this.isRefresh, this.pageStatus});
}
1.3 BaseBloc封装
class BaseBloc {
void dispose() {
_pageEvent.close();
}
///请求专用的类
Repository repository = new Repository();
///主要是事件通知
BehaviorSubject _pageEvent =
BehaviorSubject();
get pageEventSink => _pageEvent.sink;
get pageEventStream => _pageEvent.stream.asBroadcastStream();
postPageEmpty2PageContent(bool isRefresh, Object list) {
pageEventSink.add(new PageStatusEvent(errorDesc : "", isRefresh: true,
pageStatus: ObjectUtil.isEmpty(list)
? PageEnum.showEmpty
: PageEnum.showContent));
}
postPageError(bool isRefresh, String errorMsg) {
pageEventSink.add(
new PageStatusEvent(errorDesc : errorMsg, isRefresh: isRefresh, pageStatus: PageEnum.showError));
}
}
主要提供了页面状态的Stream,提供子类使用,postPageEmpty2PageContent,postPageError 主要是的页面状态调用方法
2.BaseWidget封装
@override
Widget build(BuildContext context) {
return _buildWidgetDefault();
}
///构建默认视图
Widget _buildWidgetDefault() {
return WillPopScope(
child: Scaffold(
appBar: buildAppBar(),
body: _buildBody(),
),
);
}
///子类实现,构建各自页面UI控件
Widget buildWidget(BuildContext context);
///构建内容区
Widget _buildBody() {
bloc = BlocProvider.of(context);
return new StreamBuilder(
stream: bloc.pageEventStream,
builder:
(BuildContext context, AsyncSnapshot snapshot) {
PageStatusEvent status;
bool isShowContent = false;
if (snapshot == null || snapshot.data == null) {
isShowContent = false;
status =
PageStatusEvent(errorDesc : "", isRefresh: true, pageStatus: PageEnum.showLoading);
} else {
status = snapshot.data;
if ((!status.isRefresh) ||
(status.pageStatus == PageEnum.showContent &&
status.isRefresh)) {
isShowContent = true;
} else {
isShowContent = false;
}
}
return Container(
///内容区背景颜色
color: Colours.colorPrimaryWindowBg,
child: Stack(
children: [
buildWidget(context),
Offstage(
offstage: isShowContent,
child: getErrorWidget(status),
),
],
),
);
});
}
通过pageEventStream 事件来处理页面的状态,默认情况下展示loading状态,通过使用Stack 类似Android中的Framelayout帧布局来初始化loading页面和真正的业务布局。通过isShowContent来控制ErrorWidget视图的展示与否
Widget getErrorWidget(PageStatusEvent status) {
current = status.pageStatus;
if (status != null && status.isRefresh) {
if (status.pageStatus == PageEnum.showEmpty) {
return _buildEmptyWidget();
} else if (status.pageStatus == PageEnum.showError) {
return _buildErrorWidget(status.errorDesc);
} else {
return _buildLoadingWidget();
}
}
return _buildLoadingWidget();
}
错误页面的构建,可以自己自定义,详细的代码就不贴不来了,会根据status状态来返回对应的视图
void showLoadSuccess() {
if (current != PageEnum.showContent) {
current = PageEnum.showContent;
//展示内容
bloc.pageEventSink
.add(PageStatusEvent(errorDesc : "", isRefresh: true, pageStatus: PageEnum.showContent));
}
}
void showEmpty() {
if (current != PageEnum.showEmpty) {
current = PageEnum.showEmpty;
//展示空页面
bloc.pageEventSink
.add(PageStatusEvent(errorDesc : "", isRefresh: true, pageStatus: PageEnum.showEmpty));
}
}
void showError() {
if (current != PageEnum.showError) {
current = PageEnum.showError;
//展示错误页面
bloc.pageEventSink
.add(PageStatusEvent(errorDesc : "", isRefresh: true, pageStatus: PageEnum.showError));
}
}
void showLoading() {
if (current != PageEnum.showLoading) {
current = PageEnum.showLoading;
//展示loading页面
bloc.pageEventSink
.add(PageStatusEvent(errorDesc : "", isRefresh: true, pageStatus: PageEnum.showLoading));
}
}
另外还需要提供子类调用的四个状态更改的方法
3.bloc页面调用
class BarCodeBloc extends BaseBloc {
final BehaviorSubject _qrCodeController = BehaviorSubject();
get onQrCodeSink => _qrCodeController.sink;
get onQrCodeStream => _qrCodeController.stream;
Future getQrCode(String custId) {
repository.getQrCodeData(custId, onSuccess: (data) {
onQrCodeSink.add(data);
postPageEmpty2PageContent(true, data);
}, onFailure: (error) {
postPageError(true, error.errorDesc);
});
}
@override
void dispose() {
super.dispose();
_qrCodeController.close();
}
}
这是一个普通的二维码页面展示,根据api接口返回数据,直接调用postPageEmpty2PageContent用于展示业务布局,以及postPageError展示网络失败的布局
4.基本页面的使用逻辑
@override
Widget buildWidget(BuildContext context) {
return new StreamBuilder(
stream: bloc.onQrCodeStream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return Scaffold(
body: Container(
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
new QrImage(
data: snapshot.data,
size: Dimens.dp(200),
),
],
),
),
);
});
}
展示其实直接调用就可以了,不需要管理页面相关的状态逻辑,都是在父类帮忙完成了
5.List列表相关的封装
移动端开发很大一部分都是和ListView列表有点关,最好统一封装一下
5.1 上拉加载下拉刷新的控件封装
基于框架 pullToRefresh
//下拉刷新和上拉加载的回调
typedef void OnLoadMore();
typedef void OnRefresh();
class RefreshScaffold extends StatefulWidget {
const RefreshScaffold(
{Key key,
@required this.controller,
this.enablePullUp: true,
this.enablePullDown: true,
this.onRefresh,
this.onLoadMore,
this.child,
this.bottomBar,
this.headerWidget,
this.itemCount,
this.itemBuilder})
: super(key: key);
final RefreshController controller;
final bool enablePullUp;
final bool enablePullDown;
final OnRefresh onRefresh;
final OnLoadMore onLoadMore;
final Widget child;
//底部按钮
final Widget bottomBar;
//固定header的Widget
final PreferredSize headerWidget;
final int itemCount;
final IndexedWidgetBuilder itemBuilder;
@override
State createState() {
return new RefreshScaffoldState();
}
}
/// with AutomaticKeepAliveClientMixin 用于保持列表的状态
class RefreshScaffoldState extends State
with AutomaticKeepAliveClientMixin {
@override
void initState() {
super.initState();
SchedulerBinding.instance.addPostFrameCallback((_) {
widget.controller.requestRefresh();
});
}
@override
Widget build(BuildContext context) {
super.build(context);
return new Scaffold(
appBar: widget.headerWidget,
body: new SmartRefresher(
controller: widget.controller,
enablePullDown: widget.enablePullDown,
enablePullUp: widget.enablePullUp,
onRefresh: widget.onRefresh,
onLoading: widget.onLoadMore,
footer: ListFooterView(),
header: MaterialClassicHeader(),
child: widget.child ??
new ListView.builder(
itemCount: widget.itemCount,
itemBuilder: widget.itemBuilder,
)),
bottomNavigationBar: widget.bottomBar,
);
}
@override
bool get wantKeepAlive => true;
}
主要是增加保持页面状态的wantKeepAlive回调,以及对应的一些页面header或者底部Bottom的封装
5.2 BaseListWidget封装
/// B:对应 BLoc 数据加载的Bloc
/// E: 列表数据Entity
abstract class BaseListState extends BaseState {
RefreshController controller = new RefreshController();
@override
Widget buildWidget(BuildContext context) {
bloc.pageEventStream.listen((PageStatusEvent event) {
if (event.isRefresh) {
controller.refreshCompleted();
//这句有必要的,实测不加上会导致加载更多无法回调
controller.loadComplete();
} else {
if (event.pageStatus == PageEnum.showEmpty) {
controller.loadNoData();
} else if (event.pageStatus == PageEnum.showError) {
controller.loadFailed();
} else {
controller.loadComplete();
}
}
});
return new StreamBuilder(
stream: blocStream,
builder: (BuildContext context, AsyncSnapshot> snapshot) {
return RefreshScaffold(
controller: controller,
enablePullDown: isLoadMore(),
onRefresh: onRefresh,
onLoadMore: onLoadMore,
child: new ListView.builder(
itemCount: snapshot.data == null ? 0 : snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
E model = snapshot.data[index];
return buildItem(model);
},
),
bottomBar: buildBottomBar(),
headerWidget: buildHeaderWidget(),
);
});
}
///默认存在分页
bool isLoadMore() {
return true;
}
///加载数据
get blocStream;
///刷新回调
void onRefresh();
///加载回调
void onLoadMore();
///构建Item
Widget buildItem(E entity);
@override
void onErrorClick() {
super.onErrorClick();
controller.requestRefresh();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
Widget buildBottomBar() {
return null;
}
PreferredSize buildHeaderWidget() {
return null;
}
提供三个泛型来控制布局的相关的数据,B对应Bloc,E对应的列表的实体类,提供了blocStream 的Bloc 刷新回调onRefresh,onLoadMore加载更多回调,构建item的回调等
pageEventStream的监听主要处理下来刷新和加载更多的逻辑
bloc.pageEventStream.listen((PageStatusEvent event) {
if (event.isRefresh) {
controller.refreshCompleted();
//这句有必要的,实测不加上会导致加载更多无法回调
controller.loadComplete();
} else {
if (event.pageStatus == PageEnum.showEmpty) {
controller.loadNoData();
} else if (event.pageStatus == PageEnum.showError) {
controller.loadFailed();
} else {
controller.loadComplete();
}
}
});
5.3 普通的列表调用方式
只需要实现对应的方法即可,代码就比较清爽了
class _LoanVisitPageState
extends BaseListState {
final String labelId;
_LoanVisitPageState(this.labelId);
@override
void onRefresh() {
bloc.onRefresh(labelId: labelId);
}
@override
void onLoadMore() {
bloc.onLoadMore(labelId: labelId);
}
@override
String setEmptyMsg() {
return Ids.noVisitTask;
}
@override
get blocStream => bloc.loanVisitStream;
@override
Widget buildItem(TaskEntity entity) {
return new LoanVisitItem(entity);
}
}
5.4 list普通列表的bloc调用
class LoanVisitBloc extends BaseBloc {
BehaviorSubject> _loanVisit =
BehaviorSubject>();
get _loanVisitSink => _loanVisit.sink;
get loanVisitStream => _loanVisit.stream;
List _reposList = new List();
int _taskPage = 1;
//列表数据请求
Future getLoanVisitList(String labelId, int page) {
bool isRefresh;
if (page == 1) {
_reposList.clear();
isRefresh = true;
} else {
isRefresh = false;
}
return repository.getVisitList(NetApi.RETURN_VISIT, page, 20,
onSuccess: (list) {
_reposList.addAll(list);
_loanVisitSink.add(UnmodifiableListView(_reposList));
postPageEmpty2PageContent(isRefresh, list);
}, onFailure: (error) {
postPageError(isRefresh, error.errorDesc);
});
}
@override
void dispose() {
_loanVisit.close();
}
@override
Future getData({String labelId, int page}) {
return getLoanVisitList(labelId, page);
}
@override
Future onLoadMore({String labelId}) {
_taskPage +=1 ;
return getData(labelId: labelId, page: _taskPage);
}
@override
Future onRefresh({String labelId}) {
_taskPage = 1;
return getData(labelId: labelId, page: 1);
}
}
提供刷新和加载更多的方法,也是比较一般的请求
6.总结
借鉴了很多网上的文章,并且结合项目做了修改,bloc的好处避免了setState的损耗,对于页面的状态的管理是很好的,后续会提供一个demo点击链接