Flutter 基于Bloc框架的封装

Posted by lizubing1992 on 06-20,2019

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


0评论