Flutter 基於Bloc框架的封裝

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供參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章