前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter 实践 MVVM

Flutter 实践 MVVM

原创
作者头像
DSoon
修改2019-01-04 11:19:37
9.7K0
修改2019-01-04 11:19:37
举报
文章被收录于专栏:Flutter知识集Flutter知识集

Flutter 实践 MVVM

在做Android或iOS开发时,经常会了解到MVC,MVP和MVVM。MVVM在移动端一度被非常推崇,虽然也有不少反对的声音,不过MVVM确实是不错的设计架构。

在做flutter开发时,刚学习时写的很随意,什么东西都写一起,也不去考虑解耦等问题。但是实际生产开发是不能这样做的,否则项目稍大就无法维护。自己空想一个架构是很难而且不一定好用的,不过借助MVVM,我们就可以很清晰的组织代码。

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

Stream和Sink是Dart中两个类型,原理不是本文的重点,我们可以先这样简单的去理解Stream和Sink:

Stream&Sink示意图
Stream&Sink示意图

Sink就是水槽,你可以往里面注水(放入数据),这水(数据)从水槽中流出来,就是Stream。

从编码的角度来说,就是Sink对象中add数据,然后对应的Stream对象就会收到这些数据。

其实就是一个轻量级的数据通知机制,有了这两个类支持,我们就可以做数据的响应式传输了。

Dart提供了StreamController类,通过这个类可以很好的将Sink和Stream对应起来,操作也很方便,下文的实例中可以看具体的用法。

StreamBuilder

上述的Stream和Sink还只是纯数据层面的,要想和UI相关的Widget关连起来,还有需要StreamBuilder的帮助。

StreamBuilder也是一个Widget,其作用就是监听指定的Stream,一旦这个Stream中有数据来了,就调用builder中的闭包,用新的数据,重新构建这个widget。

代码语言:txt
复制
StreamBuilder<List<StoryModel>>(
    stream: storyListViewModel.outStoryList,
    builder: (context, snapshot) {
        // return widget
    }

有了StreamBuilder,我们就可以开始MVVM的尝试了。

本文中,尝试用MVVM结构,实现仿知乎日报的列表页面。

实例

实现的效果如下:

App截图
App截图

网络层

请求就是使用官方的http库发起,具体可以看源码。

知乎日报的API网上一搜即可,本文不再赘述。

Model

日报这里的网络回包是json格式的,我们选择用json_serializable来做自动序列化/反序列化。

因为只是做一个列表页,模型层其实就是很简单的两个对象。

代码语言:txt
复制
@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);
}

View-Model

先抛实现:

代码语言:txt
复制
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

View层这里就只用看实现ListView这个部分即可。

代码语言:txt
复制
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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Flutter 实践 MVVM
    • MVVM简介
      • 角色分配
        • 语言支持
          • Stream & Sink
          • StreamBuilder
        • 实例
          • 网络层
          • Model
          • View-Model
          • View
        • 写法梳理
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档