在做Android或iOS开发时,经常会了解到MVC,MVP和MVVM。MVVM在移动端一度被非常推崇,虽然也有不少反对的声音,不过MVVM确实是不错的设计架构。
在做flutter开发时,刚学习时写的很随意,什么东西都写一起,也不去考虑解耦等问题。但是实际生产开发是不能这样做的,否则项目稍大就无法维护。自己空想一个架构是很难而且不一定好用的,不过借助MVVM,我们就可以很清晰的组织代码。
Too many good posts, don’t want to write another one.
MVVM有三个角色需要扮演:View - ViewModel - Model。
Model好说,普通对象嘛,顶多处理一下序列化的问题。
在Flutter中,一切UI皆Widget,那么View层也很明确了,就是Widget部分。
但是ViewModel就需要考虑了,因为MVVM一个很重要的特性就是双向绑定,Model中数据的更新会及时的反馈到View上,View上的更新也会及时的反馈给Model。
做好了角色分配,我们现在要处理数据绑定的问题。在android中,有DataBinding技术,直接将XML和ViewModel绑定起来。iOS里,也可以通过ReactiveCocoa来实现数据的双向绑定。
而在Flutter中,我们可以借助Stream&Sink来实现数据变更的通知,StreamBuilder来做View层的绑定。
Stream和Sink是Dart中两个类型,原理不是本文的重点,我们可以先这样简单的去理解Stream和Sink:
Sink就是水槽,你可以往里面注水(放入数据),这水(数据)从水槽中流出来,就是Stream。
从编码的角度来说,就是Sink对象中add数据,然后对应的Stream对象就会收到这些数据。
其实就是一个轻量级的数据通知机制,有了这两个类支持,我们就可以做数据的响应式传输了。
Dart提供了StreamController类,通过这个类可以很好的将Sink和Stream对应起来,操作也很方便,下文的实例中可以看具体的用法。
上述的Stream和Sink还只是纯数据层面的,要想和UI相关的Widget关连起来,还有需要StreamBuilder的帮助。
StreamBuilder也是一个Widget,其作用就是监听指定的Stream,一旦这个Stream中有数据来了,就调用builder中的闭包,用新的数据,重新构建这个widget。
StreamBuilder<List<StoryModel>>(
stream: storyListViewModel.outStoryList,
builder: (context, snapshot) {
// return widget
}
有了StreamBuilder,我们就可以开始MVVM的尝试了。
本文中,尝试用MVVM结构,实现仿知乎日报的列表页面。
实现的效果如下:
请求就是使用官方的http库发起,具体可以看源码。
知乎日报的API网上一搜即可,本文不再赘述。
日报这里的网络回包是json格式的,我们选择用json_serializable来做自动序列化/反序列化。
因为只是做一个列表页,模型层其实就是很简单的两个对象。
@JsonSerializable(nullable: false)
class StoryModel {
final List<String> images;
final String id;
final String title;
StoryModel({this.images, this.id, this.title});
factory StoryModel.fromJson(Map<String, dynamic> json) => _$StoryModelFromJson(json);
Map<String, dynamic> toJson() => _$StoryModelToJson(this);
}
@JsonSerializable(nullable: false)
class StoryListModel {
final List<StoryModel> stories;
StoryListModel(this.stories);
factory StoryListModel.fromJson(Map<String, dynamic> json) => _$StoryListModelFromJson(json);
Map<String, dynamic> toJson() => _$StoryListModelToJson(this);
}
先抛实现:
class StoryListViewModel {
var _storyListController = StreamController<StoryListModel>.broadcast(); // (1)
List<StoryModel> storyList = List();
var offset = 0;
Sink get inStoryListController => _storyListController; // (2)
// (3)
Stream<List<StoryModel>> get outStoryList => _storyListController.stream.map((stories) {
storyList.addAll(stories.stories);
return storyList;
});
//(4)
refreshStoryList() async {
offset = 0;
storyList.clear();
StoryListModel model = await NetWorkRepo.requestNewsList(offset);
inStoryListController.add(model);
}
// (5)
loadNextPage() async {
offset++;
StoryListModel model = await NetWorkRepo.requestNewsList(offset);
inStoryListController.add(model);
}
depose() {
_storyListController.close();
}
}
很简单的逻辑,注释(1)处是StreamController创建的Sink,之所以用broadcast,是方便之后拓展,可能不只一个Stream监听这里的数据变化,使用broadcast可以让多个流监听同一个Sink。
注释(2)处是对外暴露的Sink属性,网络请求回来后通过这里塞数据到流里。
注释(3)处是Stream,这里会对传入的数据做处理,然后返回给实际需要的数据。
注释(4)(5)这两个方法是网络请求,分别实现了刷新和加载下一页的逻辑。可以看到,这里请求回来后,做的就是把结果add到inStoryListController
这个Sink对象中。这样实际上outStoryList
会收到数据回调,而这就到了View层了,我们来看一下View层的实现。
View层这里就只用看实现ListView这个部分即可。
StreamBuilder<List<StoryModel>>( // (1)
stream: storyListViewModel.outStoryList,
builder: (context, snapshot) {
List stories = snapshot.data; // (2)
return RefreshIndicator(
onRefresh: () { // (3)
return storyListViewModel.refreshStoryList();
},
child: ListView.builder(
itemCount: (stories?.length ?? 0) + 1,
itemBuilder: (context, index) {
if (index >= (stories?.length ?? 0)) { // (4)
storyListViewModel.loadNextPage();
return _buildLoadMoreView();
}
return _buildRow(stories[index]); // (5)
}),
);
})
上述代码就是View层的主要代码,我们依次来看注释的5个点
注释(1)处,一个StreamBuilder,在stream参数给上我们ViewModel的output stream,也就是说当ViewModel中的Sink对象被add数据后,StreamBuilder会监听到这个变化,然后重新通过builder参数中传入的闭包来重新构建这个widget。
注释(2)处,这里是获取到数据后,构建随之更新widget的方法。snapshot.data
就是监听的数据,更新后的新数据。
注释(3)处,RefreshIndicator
是一个下拉刷新的widget,onRefresh
方法里调用了刷新方法。
注释(4)处,不像下拉刷新有一个特定的widget来做上拉加载更多,官方推荐的做法是,itemCount加1,然后再itemBuilder里面发现到底底部了,开始加载更多的逻辑。(如果是有限数目的,需要设置一个临界值,这里暂时不用)
注释(5)处,这里就是构建普通的每行视图了。
前面流水账一样的介绍了Model-ViewModel-View的写法,可以发现,Model的写法很传统。主要就是引入了StreamWidget,StreamBuilder,然后更新了一下ViewModel和View的数据绑定方式,总体来说还是比较简单的。
需要注意的是,这里虽然只用了一个StreamBuilder,但是不代表一个页面只能用一个StreamBuilder,每个想要单独监听某个Stream的widget外面都是wrap一个StreamBuilder,然后对更新的数据做相关的操作。
附上上述日报Demo源码,demo内实现了详情页,有兴趣可以尝试一下。
Wrote by Kevin(a2V2aW56aGFuMDQxN0BvdXRsb29rLmNvbQ==)
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有