狀態主要有:loading,error,empty,以及展現內容的showContentgit
enum PageEnum {
showLoading,
showError,
showEmpty,
showContent,
}
複製代碼
bloc流供baseWidget作狀態的變化github
class PageStatusEvent {
String errorDesc; //錯誤數據,主要是展現錯誤頁面
bool isRefresh;//主要用於list列表數據
PageEnum pageStatus; 頁面狀態
PageStatusEvent({this.errorDesc,this.isRefresh, this.pageStatus});
}
複製代碼
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 主要是的頁面狀態調用方法api
@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視圖的展現與否bash
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));
}
}
複製代碼
另外還須要提供子類調用的四個狀態更改的方法app
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展現網絡失敗的佈局框架
@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),
),
],
),
),
);
});
}
複製代碼
展現其實直接調用就能夠了,不須要管理頁面相關的狀態邏輯,都是在父類幫忙完成了ide
移動端開發很大一部分都是和ListView列表有點關,最好統一封裝一下佈局
基於框架 pullToRefreshpost
//下拉刷新和上拉加載的回調
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的封裝
/// 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();
}
}
});
複製代碼
只須要實現對應的方法便可,代碼就比較清爽了
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);
}
}
複製代碼
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);
}
}
複製代碼
提供刷新和加載更多的方法,也是比較通常的請求
借鑑了不少網上的文章,而且結合項目作了修改,bloc的好處避免了setState的損耗,對於頁面的狀態的管理是很好的,後續會提供一個demo供參考 點擊 Github