1. Bloc encapsulation of page state

1.1 Define a base class for Bloc to handle page state

Loading, Error, empty, and showContent are the main states

enum PageEnum {
  showLoading,
  showError,
  showEmpty,
  showContent,
}

Copy the code

1.2 Define an enumeration to represent the page state, in addition to defining the event class, pass some necessary data

The Bloc stream is used by the baseWidget for state changes

class PageStatusEvent { String errorDesc; Bool isRefresh; // Mainly used for list data PageEnum pageStatus; Page state PageStatusEvent ({enclosing errorDesc, enclosing isRefresh, enclosing pageStatus}); }Copy the code

1.3 BaseBloc encapsulation

class BaseBloc {
  void dispose() { _pageEvent.close(); } /// request a private class 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)); }}Copy the code

Main Stream provides the state of the page, provide a subclass, postPageEmpty2PageContent, main yes postPageError page state method is called

2. BaseWidget encapsulation

@override
  Widget build(BuildContext context) {
    return_buildWidgetDefault(); } /// Build the default view Widget_buildWidgetDefault() {
    returnWillPopScope( child: Scaffold( appBar: buildAppBar(), body: _buildBody(), ), ); BuildWidget buildWidget(BuildContext context); /// Build the content area 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; }}returnThe Container (/ / / content area background color color: Colours. ColorPrimaryWindowBg, child: Stack (children: <Widget>[ buildWidget(context), Offstage( offstage: isShowContent, child: getErrorWidget(status), ), ], ), ); }); }Copy the code

PageEventStream is used to handle the state of the page. Loading state is displayed by default. The loading page and the real business layout are initialized by using Stack similar to the Framelayout Framelayout in Android. Use isShowContent to control whether the ErrorWidget view is displayed

 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();
  }
Copy the code

Error page construction, can be customized, detailed code is not posted, according to the status to return the corresponding view

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; Bloc. Pageeventsink. add(PageStatusEvent(errorDesc:"", isRefresh: true, pageStatus: PageEnum.showLoading)); }}Copy the code

You also need to provide four state-changing methods that the subclass calls

Bloc page call

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(); }}Copy the code

This is a common qr code page display, according to the API interface return data, direct call postPageEmpty2PageContent used to show the layout, as well as postPageError shows the layout of network failure

4. Basic page usage logic

 @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),
                  ),
                ],
              ),
            ),
          );
        });
  }
Copy the code

In fact, the display can be directly called, there is no need to manage the page-related state logic, is done in the parent class for help

5.List related encapsulation

A large part of mobile development is related to ListView lists, so it is best to encapsulate them

5.1 Pull-up load pull-down refresh control package

Based on frame 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; // Bottom button Final Widget bottomBar; // Fixed headerWidget Final PreferredSize headerWidget; final int itemCount; final IndexedWidgetBuilder itemBuilder; @override State<StatefulWidget>createState() {
    returnnew RefreshScaffoldState(); }} / / / with AutomaticKeepAliveClientMixin used to maintain the State of the list class RefreshScaffoldState extends the State < RefreshScaffold > with AutomaticKeepAliveClientMixin { @override voidinitState() {
    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;
}

Copy the code

The main thing is to add wantKeepAlive callbacks that hold the state of the page, as well as corresponding wrapper for some page headers or Bottom

5.2 BaseListWidget encapsulation

/// B: BLoc data loaded /// E: List data 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];
                returnbuildItem(model); }, ), bottomBar: buildBottomBar(), headerWidget: buildHeaderWidget(), ); }); } /// Paging bool exists by defaultisLoadMore() {
    return true; } // get blocStream; /// Refresh callback void onRefresh(); // load callback void onLoadMore(); // buildItem Widget buildItem(E entity); @override voidonErrorClick() {
    super.onErrorClick();
    controller.requestRefresh();
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  Widget buildBottomBar() {
    return null;
  }

  PreferredSize buildHeaderWidget() {
    return null;
  }
Copy the code

Provides three generic types to control layout related data, B for Bloc,E for list entity class, Bloc refresh callback for blocStream onRefresh, onLoadMore to load more callbacks, build item callback, etc

PageEventStream’s listener mainly deals with refreshing and loading more logic

 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(); }}});Copy the code

5.3 Common List Invocation mode

Just implement the corresponding methods, and the code is clean

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) {
    returnnew LoanVisitItem(entity); }}Copy the code

5.4 List Generic list bloc call

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;
    returngetData(labelId: labelId, page: 1); }}Copy the code

Providing ways to refresh and load more is also a fairly generic request

6. Summary

I have learned from many online articles and made modifications in combination with the project. The benefits of Bloc have avoided the loss of setState, and the state management of the page is very good. A demo will be provided for reference later, click Github