转载注明出处:https://blog.csdn.net/skysukai

1、背景

1.1 MVVM

MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。当然这些事 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑。通俗地来说,MVVM要达到的目的就是数据发生变化自动更新UI显示,而不用显式的调用UI刷新的代码。

1.2 Bloc

BLoC表示为业务逻辑组件 (Business Logic Component)。为什么会想到使用Bloc呢?因为项目中有大量的UI刷新,随着业务的增加,setstate的数量不断上升。flutter的生命周期管理变得混乱起来,这时候就考虑了引入Bloc。来达到不使用或减少使用setstate的目的。

2、实践1——搜索首界面使用BLoC

最先想到了在项目中的搜索模块引入Bloc,这个部分逻辑简单,如果出错代码回退比较容易。先来看下搜索的设计稿:

这是搜索首界面的设计稿,初次进入页面时,会给用户推荐热搜关键字及热搜专辑。

2.1 setstate实现

比如搜索首页,用白线框出了内容展示的地方。按一般的思路,只需要请求一次数据,再调用setstate刷新界面即可。而对于搜索结果页,则每个tab页单独获取数据即可。给出一小段示例代码:

  void _loadFromServer() {//请求热搜关键字if (_hotWordList.length == 0) {Request.getHotWordList(HotRecommendListParam(pageNum: 1, pageSize: 10),//请求成功回调(dynamic result) {HotwordListResult hotwordListResult = result as HotwordListResult;if (hotwordListResult.count > 0) {if(mounted) {//刷新界面setState(() {if(_hotWordList.length == 0) {_hotWordList.addAll(hotwordListResult.data);}});}}}, //请求失败回调(int code, String desc) {Log.d(TAG, "error: $code, $desc");if(mounted) {setState(() {_loadingHotWord = false;_loadHotWordError = true;});}});}……}

代码里边,请求成功和失败都调用了setstate来刷新界面。

2.2 BLoC实现

BLoC的实现参考了大神的架构传送门,即设立一个全局统一的BLoC,其他BLoC继承于这个顶层BLoC。搜索模块设置一个BLoC:

class SearchMainBloc implements BlocBase {BehaviorSubject<List<String>> _hotWordController =BehaviorSubject<List<String>>();Sink<List<String>> get _hotWordSink => _hotWordController.sink;Stream<List<String>> get hotWordStream => _hotWordController.stream;@overridevoid dispose() {_hotWordController.close();}void getHotWord() {Request.getHotWordList(HotRecommendListParam(pageNum: 1, pageSize: 10),(dynamic result) {HotwordListResult hotwordListResult =result as HotwordListResult;//刷新界面_hotWordSink.add(UnmodifiableListView<String>(hotwordListResult.data));}, (int code, String desc) {});}
……

再来看widget实现:

 ……Widget _buildStreamBody(BuildContext context) {final SearchMainBloc bloc = BlocProvider.of<SearchMainBloc>(context);bloc.getHotWord();return Column(mainAxisAlignment: MainAxisAlignment.start,children: <Widget>[StreamBuilder<List<String>>(stream: bloc.hotWordStream,builder: (BuildContext context,AsyncSnapshot<List<String>> snapshot) {return Flexible(child: GridView.builder(gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2,childAspectRatio: 5.4,),itemBuilder: (BuildContext context, int index) => _SearchHomeWordItem(words: snapshot.data[index],index: index,),itemCount: (snapshot.data == null ? 0 : snapshot.data.length),));},),……],);}……

可以看到,代码当中没有直接调用setstate达到了刷新界面的目的。

3、 实践2——搜索结果页使用BLoC


这是搜索结果页的设计稿,搜索结果分成了四个部分:小视频、专辑、照片及用户。之所以把搜索结果页单独提出来讲是因为,通常在flutter中,我们会用TabBarView来做页签,这里涉及到一个问题就是父Widget和子Widget的状态交互。在设计稿的顶部输入框输入搜索内容,需要通知子tab刷新界面;切换到其他tab页时,也需根据当前搜索内容来发起搜索请求。

3.1 setstate实现

给出一小段实现代码:

 //停止刷新回调typedef void StopRefresh();@overrideWidget build(BuildContext context) {return Scaffold(appBar: PreferredSize(child: AppBar(bottom: PreferredSize(child: SearchBarInSearchPage(//输入框有输入回调handleSearch: _handleSearch,//textControllertextController: _setTextController,),……),),body: Column(children: <Widget>[Container(child: TabBar(controller: _tabController,……tabs: <Widget>[//Tab标题Tab(text: S.of(context).headingSmallVideo),……],),),Expanded(child: Padding(child: TabBarView(controller: _tabController,children: <Widget>[//Tab实现SearchSmallVideoList(searchText: _searchText, textChanged: _notifyChange, stopRefresh: _resetFlag),……],),)),],),);}//触发一次搜索_handleSearch(BuildContext context, String searchText) {setState(() {_searchText = searchText;_notifyChange = true;});}//停止刷新界面_resetFlag() {setState(() {_notifyChange = false;});}

小视频搜索页的具体实现:

  @overrideWidget build(BuildContext context) {//输入框内容变化,发起一次新的搜索,重置请求参数及数据if(widget.textChanged) {_searchVideoList.clear();_pageNum = 1;_count = 0;……}}……_loadFromServer(onSuccess, onFail) {//请求成功回调RequestSuccess onRequestSuccess = (result) {//通知父Widget停止刷新widget.stopRefresh();if (searchVideoListResult.count > 0) {if(mounted) {setState(() {……});}}};//请求失败回调RequestFailure onRequestFailure = (code, desc) {//通知父Widget停止刷新widget.stopRefresh();……};Request.getVideoSearchList(SearchTypeListParam(keywords: _searchText, type: SEARCH_SMALL_VIDEO, pageNum: _pageNum, pageSize: _pageSize),onRequestSuccess, onRequestFailure);}

不同于搜索首界面的交互,搜索结果页的交互变得复杂起来。输入框有内容输入时,触发重建UI;在建立子Widget的时候,将停止刷新作为一个参数传递widget.stopRefresh(),请求完成时,停止刷新。再来看一下BLoC的实现。

3.2 BLoC实现

采用setstate的方法,对于父Widget和子Widget的状态交互管理变得有些复杂,那采用BLoC是否能解决这个问题呢?仔细分析,搜索结果的触发条件应该是搜索内容,搜索内容的改变引起搜索结果的改变。那应该可以给搜索内容设置一个BLoC,这个BLoC的改变触发搜索结果的更新。给出代码:

  @overrideWidget build(BuildContext context) {final SearchMainBloc bloc = BlocProvider.of<SearchMainBloc>(context);//有内容输入_handleSearch(BuildContext context, String searchText) {_searchText = searchText;//触发UI刷新bloc.searchTextSink.add(searchText);}return Scaffold(appBar: PreferredSize(child: AppBar(bottom: PreferredSize(child: SearchBarInSearchPage(handleSearch: _handleSearch,textController: _setTextController,),)),),),),body: StreamBuilder<String>(//为搜索内容设置BLoC,以此为触发刷新的依据stream: bloc.searchTextStream,builder: (BuildContext context, AsyncSnapshot<String> snapshot) {return Column(children: <Widget>[Container(height: ScreenUtils.px2dp(267),child: TabBar(……tabs: <Widget>[Tab(text: S.of(context).headingSmallVideo),……],),),Expanded(child: Padding(child: TabBarView(controller: _tabController,children: <Widget>[SearchSmallVideoList(searchText: snapshot.data),……],),)),],);},),);}

页签的具体实现:

    _loadFromStream(onSuccess, onFail) {//请求成功回调RequestSuccess onRequestSuccess = (result) {VideoListResult searchVideoListResult = result as VideoListResult;if(searchVideoListResult.data != null) {_smallVideoList.addAll(searchVideoListResult.data);}//触发界面刷新bloc.searchSmallVideoSink.add(UnmodifiableListView<VideoInfo>(_smallVideoList));……};//请求失败回调RequestFailure onRequestFailure = (code, desc) {……};Request.getVideoSearchList(SearchTypeListParam(keywords: _searchText, type: SEARCH_SMALL_VIDEO, pageNum: _pageNum, pageSize: _pageSize),onRequestSuccess, onRequestFailure);}

看上去,采用BLoC之后,省去了各种回调接口,代码变得简单清晰了。这里设置了两个BLoC,一个是主BLoC即输入框的BLoC,另一个是数据的BLoC,用于显示页面。

4、实践3——更复杂的交互


这是添加好友界面的设计稿,依然有一个搜索框。除此之外,列表支持下拉刷新,ListView里的Item还有三种状态:常态、已发送过好友申请、申请被拒。

4.1 setstate实现

给出请求关键代码:

  _loadFromServer(onSuccess, onFail) {String searchStr = "";//请求成功回调RequestSuccess onRequestSuccess = (result) {……setState(() {……});};//请求失败回调RequestFailure onRequestFailure = (code, desc) {……};if(_textController.text != null && _textController.text != "") {searchStr = _textController.text;}Request.getStrangerList(AddFriendParam(keywords: searchStr,pagenum: _pageNum,pagesize: _pageSize),onRequestSuccess,onRequestFailure);}

给输入框设置TextEditingController,以此来作为搜索条件,初次进入时,设置搜索条件为“”初始化界面。ListItem还有专门的点击事件,用于区别当前推荐好友是否已经发送过申请,于是需要设置GestureDetector监听点击事件:

  void _sendMessageDialog(BuildContext context, StrangerInfo info) {showDialog<Null>(context: context,builder: (BuildContext context) {return TextFieldDialog(……sendMessage:(String content) {……Request.addFriend(param,//请求成功回调(result) {setState(() {……});},//请求失败回调(code, desc) {……});});});}

发生过点击事件服务器响应成功之后,刷新界面。

4.2 BLoC实现

有了搜索结果页的实践之后,这个界面的实现就有了思路:搜索框设置一个BLoC,数据展示设置一个BLoC,而这个页面还需给ListItem设置一个BLoC用于ListItem的状态更新:

  @overrideWidget build(BuildContext context) {final FriendBloc bloc = BlocProvider.of<FriendBloc>(context);return Scaffold(body: RefreshIndicator(//搜索框设置的BLoCchild: StreamBuilder<String>(stream: bloc.searchFriendStream,builder: (BuildContext context,AsyncSnapshot<String> snapshot) {return _getBody(bloc, snapshot.data);}),onRefresh: _refreshList));}Future<void> _refreshList() async{await Future.delayed(Duration(seconds: 0), () {……});return Future.value(true);}Widget _getBody(FriendBloc bloc, String searchCondition) {//加载数据设置的BLoCreturn StreamBuilder<List<StrangerInfo>>(stream: bloc.strangerListStream,builder: (BuildContext context,AsyncSnapshot<List<StrangerInfo>> snapshot) {if(snapshot.hasData) {return SliverList(……delegate: SliverChildBuilderDelegate((BuildContext context, int i) {//构建ListItemreturn _getListItem(context, snapshot.data[index], bloc);},childCount: snapshot.data.length));}return EmptyView();},);}

加载数据关键代码:

 //将搜索框BLoC的snapshot.data作为搜索条件searchCondition传入_loadFromStream(LoadMoreOnSuccess onSuccess, LoadMoreOnFailure onFail, FriendBloc bloc, String searchCondition) {//请求成功回调RequestSuccess onRequestSuccess = (result) {……bloc.initList(_strangerList);bloc.strangerListSink.add(UnmodifiableListView<StrangerInfo>(_strangerList));};//请求失败回调RequestFailure onRequestFailure = (code, desc) {……};Request.getStrangerList(AddFriendParam(keywords: searchCondition != null ? searchCondition : "",pagenum: _pageNum,pagesize: _pageSize),onRequestSuccess,onRequestFailure);}

发送好友请求关键代码:

  void _sendMessageDialog(BuildContext context, StrangerInfo info, FriendBloc bloc) {showDialog(builder: (BuildContext context) {return TextFieldDialog(……sendMessage:(String content) {……Request.addFriend(param,(result) {……//更新ListItem状态,触发这个被点击的Item更新info.status = 0;bloc.sendApplySink.add(info);},(code, desc) {……});});});}

再来看一下FriendBloc,这个BLoC的作用就是好友模块单独的BLoC:

class FriendBloc implements BlocBase {List<StrangerInfo> _strangerList = [];/// search stranger controllerBehaviorSubject<String> _searchFriendController =BehaviorSubject<String>(seedValue: "");Sink<String> get searchFriendSink => _searchFriendController.sink;Stream<String> get searchFriendStream => _searchFriendController.stream;/// stranger list controllerBehaviorSubject<List<StrangerInfo>> _strangerController =BehaviorSubject<List<StrangerInfo>>();Sink<List<StrangerInfo>> get strangerListSink => _strangerController.sink;Stream<List<StrangerInfo>> get strangerListStream => _strangerController.stream;……FriendBloc () {_sendApplyController.listen(_handleSendApply);}//点击发送申请按钮时触发界面刷新void _handleSendApply(StrangerInfo info) {for(StrangerInfo strangerInfo in _strangerList) {if(strangerInfo.uid == info.uid) {strangerInfo.status = info.status;}}strangerListSink.add(UnmodifiableListView<StrangerInfo>(_strangerList));}void initList(List<StrangerInfo> list) {_strangerList.addAll(list);}
}

这样,添加好友界面也实现了BLoC。仔细分析,由于发送申请,导致ListItem刷新这个功能的加入,导致代码量也跟着增多起来。这套代码虽然没有setstate的调用,复杂度确上升了不少。BLoC也设置了三个,有没有更简洁的代码呢?

4.3 BLoC的其他尝试

4.3.1 搜索BLoC和数据BLoC合一

添加好友列表页数据展示其实是以搜索框的输入内容为准,即搜索框的输入内容决定了页面的展示内容。那给搜索框设置的BLoC和数据的BLoC是否能合二为一呢?答案是肯定的。
页面实现:

  @overrideWidget build(BuildContext context) {final FriendBlocTest bloc = FriendBlocTest();return Scaffold(body: StreamBuilder<List<StrangerInfo>>(stream: bloc.strangerList,builder: (BuildContext context,AsyncSnapshot<List<StrangerInfo>> snapshot) {return _getBody(bloc, snapshot.data);}));}Widget _getBody(FriendBlocTest bloc, List<StrangerInfo> strangerList) {if(strangerList == null) {return Center(child: EmptyView(),);} else {return ListView.builder(itemCount: strangerList.length,itemBuilder: (context, index) {return _getListItem(context, strangerList[index], bloc);},);}}

搜索框触发搜索的代码:

onSubmitted: (searchStr) => bloc.onTextChanged.add(searchStr),

BLoC实现:

class FriendBlocTest implements BlocBase {static List<StrangerInfo> _strangerList = [];final Sink<String> onTextChanged;final Stream<List<StrangerInfo>> strangerList;factory FriendBlocTest () {final onTextChanged = PublishSubject<String>();final strangerList = onTextChanged.distinct().switchMap((String term) => _search(term)).startWith(null);return FriendBlocTest._(onTextChanged, strangerList);}FriendBlocTest._(this.onTextChanged, this.strangerList);static Stream<List<StrangerInfo>> _search(String term) async* {try {Dio dio = new Dio(BaseOptions(……));Response<RequestResult> response = await dio.post(……queryParameters: AddFriendParam(……).toJson());//请求成功if(response.data.resultInfo.resultCode == "200") {var result = StrangerListResult.fromJson(response.data.result);StrangerListResult strangerListResult = result as StrangerListResult;_strangerList.addAll(strangerListResult.data);}//返回请求结果yield _strangerList;} on DioError catch(e) {……}}
}

通过这种方式就是实现了只设置一个BLoC来达到搜索框输入改变触发界面刷新,代码简洁了不少。不过,这中方式必须等待网络请求的结果,返回数据来到达刷新界面的效果,而不能通过回调的方式。项目中封装的网络请求都是通过回调的方式来做的,为了维持代码风格的统一,这种方案未提交进代码而作为了一种有益尝试。

4.3.1 为ListItem设置子BLoC

在4.2中,我们为了刷新ListItem,在FriendBloc中设置了一个BLoC用于Item刷新。这部分代码跟界面数据显示关系不大,根据大神的示范可以设置一个子BLoC。这样也带来一个问题,需要把ListItem单独封装成一个类,代码复杂度也上升了。关于怎么给ListItem里面的Widget设置BLoC以更新界面,我在网上查了很多资料,并没有一个简单的方法可以做到,至少我没有找到。这里就直接贴出大神的代码。
ListItem关键代码:

FavoriteMovieBloc _bloc;void _createBloc() {//初始化ListItem子BLoC_bloc = FavoriteMovieBloc(widget.movieCard);//依赖注入,初始化ListItem子BLoC里的所有数据的数据流_subscription = widget.favoritesStream.listen(_bloc.inFavorites.add);}@overrideWidget build(BuildContext context) {final FavoriteBloc bloc = BlocProvider.of<FavoriteBloc>(context);List<Widget> children = <Widget>[……];children.add(StreamBuilder<bool>(stream: _bloc.outIsFavorite,initialData: false,builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {if (snapshot.data == true) {return Positioned(……child: Container(child: InkWell(……onTap: () {//点击触发刷新bloc.inRemoveFavorite.add(widget.movieCard);},)),);}return Container();}),);return InkWell(……),);}

ListItem子BLoC关键代码:

class FavoriteMovieBloc implements BlocBase {//子BLoC主体final BehaviorSubject<bool> _isFavoriteController = BehaviorSubject<bool>();Stream<bool> get outIsFavorite => _isFavoriteController.stream;//所有数据的数据流,用于传入数据final StreamController<List<MovieCard>> _favoritesController = StreamController<List<MovieCard>>();Sink<List<MovieCard>> get inFavorites => _favoritesController.sink;FavoriteMovieBloc(MovieCard movieCard){_favoritesController.stream//遍历所有数据,得出当前Item是否被点击.map((list) => list.any((MovieCard item) => item.id == movieCard.id))//当前Item被点击,触发界面更新.listen((isFavorite) => _isFavoriteController.add(isFavorite));}
……
}

5、总结

经过以上一些尝试,对Flutter下的BLoC有了一定程度的认识。MVVM模式可以使开发者专注于业务流程的开发,比较好用。当然了,Flutter下的BLoC还有其他封装,如果感兴趣可以自行阅读源码。

相关参考:https://www.jianshu.com/p/e7e1bced6890
相关参考:https://www.didierboelens.com/2018/08/reactive-programming---streams---bloc/
相关参考:https://felangel.github.io/bloc/#/gettingstarted
相关参考:https://www.didierboelens.com/2018/12/reactive-programming---streams---bloc---practical-use-cases/
相关参考:https://qiita.com/sensuikan1973/items/64f1a6235bd8ecaf9067
相关参考:https://www.jianshu.com/p/024b19dea138
相关参考:https://juejin.im/post/5be42e9e5188256ccc192c68
相关参考:https://javascript.ctolib.com/Sky24n-flutter_wanandroid.html

Flutter下MVVM——Bloc的探索相关推荐

  1. 阮征:互联网金融下的智能客户服务探索

    2019独角兽企业重金招聘Python工程师标准>>> 阮征:互联网金融下的智能客户服务探索 如何在大规模soa化,拥有海量数据的蚂蚁金服整体架构体系内,及时有效获取数据?如何有效结 ...

  2. 腾讯朱华:数据中心下一个风向的探索

    导读:朱华,腾讯数据中心技术发展中心总监,中国工程建设标准化协会数据中心技术委员会副主任委员,中国通信标准化协会开放数据中心委员会数据中心工作组组长,荣获中国工程建设标准化协会颁发的2018数据中心青 ...

  3. 分享轮子-flutter下拉刷新上拉加载

    flutter下拉上拉组件轮子 什么是flutter? 首先说下flutter,估计这个应该挺多人没听过flutter这个框架,它是一个google推出的跨平台的移动应用UI框架,和React Nat ...

  4. 旅行场景下的推荐算法探索

    今天给大家分享阿里巴巴集团高级算法温鸿所做的分享<旅行场景下的推荐算法探索.pdf>,关注推荐算法及其实践的伙伴们别错过啦!(到省时查报告小程序中搜索"推荐".&quo ...

  5. VisionMobile:电信运营商创新工具箱(八)第六章 不确定下的处理:探索式规划

    第六章 不确定下的处理:探索式规划 高度不确定性要求完全不同的规划方法.探索式规划不是将大量假设当作事实,而是系统地将假设转化为知识. 今天,运营商在连接业务黄金岁月中发展的传统规划方法,在不可预知的 ...

  6. Flutter下实现低延迟的跨平台RTSP/RTMP播放

    为什么要用Flutter? Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面. Flutter可以与现有的代码一起工作.在全世界,Flutter正在被越来 ...

  7. 内容理解、内容生成、内容推荐分发,在广告场景下的实践和探索(京东张政)

    内容理解.内容生成.内容推荐分发,在广告场景下的实践和探索(京东张政) 提示:广告也好,商品也罢,内容们,需要精准地推荐给用户,使其点击观看或者够买啥的,都需要走通一个重要流程:内容理解与推荐分发,更 ...

  8. flutter下Aes加密算法CFB128的nopadding实现修改源码

    有网友问关于cfb128的nopadding实现 java下应该是: Cipher.getInstance("AES/CFB/NoPadding"); python下应该是: ci ...

  9. flutter 解耦框架BLoC在网络请求中的应用

    BLoC BLoC表示为业务逻辑组件 (Business Logic Component),由谷歌在2018的 DartConf 首次提出------其诞生可以说是为了解耦. 在了解该框架之前需要了解 ...

最新文章

  1. Apache 2配置域名绑定的步骤
  2. zabbix监控java线程池,linux线程数限制与zabbix监控
  3. Scala - 快速学习08 - 函数式编程:高阶函数
  4. es解决只能查询10000条数据方案
  5. oracle10 ins tcx,安装Oracle10g遭遇ins_ctx.mk问题-Oracle
  6. 若依如何修改超级管理员登录密码?
  7. ionic 侧栏菜单用法
  8. let,with,run,apply,also函数区别
  9. JavaWeb开发Session管理
  10. 关于孟德斯鸠的出卖官职
  11. 专用计算机房属于中危险等级,普通住宅属哪种危险等级的灭火器配置场所
  12. 瑞红淘宝商城旗舰店开张 正式进军B2C市场
  13. win10计算机停止工作,360重装Win10系统后如何应对已停止工作提示的办法
  14. 如何利用Python在网上接单,兼职也能月薪过万
  15. 如何将Chrome设为iPhone和iPad上的默认Web浏览器
  16. Spring SpringBoot中使用Mybatis-plusDemo1
  17. 学习笔记:python爬虫(第一次写笔记,多多包涵)
  18. 百度Q2净利润同比增长45% 百家号成信息流营收源动力
  19. java 操作 cfs_Lucene 打开cfs文件 并获取数据
  20. 2.5寸12v5v服务器硬盘盒,3.5英寸硬盘盒装2.5英寸硬盘可以吗?外接电源还需不需要接呢...

热门文章

  1. 【项目】Thinkphp5.1制作博客CMS
  2. 命名需谨慎!科技产品荒谬命名大盘点
  3. 创建银行账户,实现存款,取款,转账(正解)
  4. caffe代码阅读8: Data_layers的实现细节(各个数据读取层的实现细节) 2016.3.25-28
  5. TOM企业邮箱安全卫士告诉你,如何告别邮箱被盗
  6. Android修改ro.debuggable 的四种方法
  7. OpenCV~图像处理API(逆光、模糊、亮度、雾霾)
  8. 考研网络100基础知识
  9. java计算乘地铁费用_蓝桥杯-地铁换乘
  10. java做百度语言识别_java实现百度云文字识别接口代码