Flutter 实践 MVVM

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示意图

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

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

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

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

StreamBuilder

上述的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结构,实现仿知乎日报的列表页面。

实例

实现的效果如下:

App截图

网络层

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

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

Model

日报这里的网络回包是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);
}

View-Model

先抛实现:

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这个部分即可。

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==)

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Android开发指南

Android优化指南

5197
来自专栏技术记录

前端插件——头像截图上传插件的使用(带后台)

效果图:实现上传头像,右边是预览,有三个大小,可以对头像进行裁剪 ? HTML: toParentData 和 img 返回的是图片裁剪后的base64编码。其...

1.1K5
来自专栏Scott_Mr 个人专栏

RxSwift 实战操作【注册登录】

3946
来自专栏Spark学习技巧

Spark源码系列之spark2.2的StructuredStreaming使用及源码介绍

一,概述 Structured Streaming是一个可扩展和容错的流处理引擎,并且是构建于sparksql引擎之上。你可以用处理静态数据的方式去处理你的流计...

1.3K7
来自专栏腾讯Bugly的专栏

深入浅出 Retrofit,这么牛逼的框架你们还不来看看?

Android 开发中,从原生的 HttpUrlConnection 到经典的 Apache 的 HttpClient,再到对前面这些网络基础框架的封装,比如 ...

3896
来自专栏求索之路

Android数据层架构的实现 下篇

接上篇:Android数据层架构的实现 上篇 4.外观模式实现数据处理引擎框架暴露出来的api 我们在使用各种开源框架的时候,大多数时候都不会对框架内部...

3505
来自专栏潇涧技术专栏

Pury Project Analysis

Pury的源码:https://github.com/NikitaKozlov/Pury

922
来自专栏JackieZheng

探秘Tomcat——启动篇

tomcat作为一款web服务器本身很复杂,代码量也很大,但是模块化很强,最核心的模块还是连接器Connector和容器Container。具体请看下图: ? ...

4977
来自专栏向治洪

Android 几种网络请求的区别与联系

HttpUrlConnection 最开始学android的时候用的网络请求是HttpUrlConnection,当时很多东西还不知道,但是在android...

2355
来自专栏杂烩

websocket 原

     WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。   

1962

扫码关注云+社区

领取腾讯云代金券