Flutter 基於Bloc框架的封裝

Rx_Re發表於2019-06-20

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

相關文章