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<PageStatusEvent> _pageEvent =
BehaviorSubject<PageStatusEvent>();
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<B>(context);
return new StreamBuilder(
stream: bloc.pageEventStream,
builder:
(BuildContext context, AsyncSnapshot<PageStatusEvent> 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: <Widget>[
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<String> _qrCodeController = BehaviorSubject<String>();
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<String> snapshot) {
return Scaffold(
body: Container(
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
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<StatefulWidget> createState() {
return new RefreshScaffoldState();
}
}
/// with AutomaticKeepAliveClientMixin 用於保持列表的狀態
class RefreshScaffoldState extends State<RefreshScaffold>
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<T extends BaseListWidget, B extends BaseBloc,
E extends Object> extends BaseState<T, B> {
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<List<E>> 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<LoanVisitPage, LoanVisitBloc, TaskEntity> {
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<List<TaskEntity>> _loanVisit =
BehaviorSubject<List<TaskEntity>>();
get _loanVisitSink => _loanVisit.sink;
get loanVisitStream => _loanVisit.stream;
List<TaskEntity> _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<TaskEntity>(_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供參考 點選 Github