Android架构模式飞速演进 到底哪一个才是自己最需要的?

android架构模式飞速演进,目前已经有mvc、mvp、mvvm、mvi。到底哪一个才是自己业务场景最需要的,不深入理解的话是无法进行选择的。这篇文章就针对这些架构模式逐一解读。重点会介绍compose为什么要结合mvi进行使用。希望知其然,然后找到适合自己业务的架构模式
一、前言
不得不感叹,近些年android的架构演进速度真的是飞快,拿笔者工作这几年接触的架构来说,就已经有了mvc、mvp、mvvm。正当笔者准备把mvvm应用到自己项目当中时,发现谷歌悄悄的更新了开发者文档(应用架构指南 | android 开发者 | android developers (google.cn))。这是一篇指导如何使用mvi的文章。那么这个文章到底为什么更新,想要表达什么?里面提到的compose又是什么?难道现在已经有的mvc、mvp、mvvm不够用吗?mvi跟已有的这些架构又有什么不同之处呢?
有人会说,不管什么架构,都是围绕着“解耦”来实现的,这种说法是正确的,但是耦合度高只是现象,采用什么手段降低耦合度?降低耦合度之后的程序方便单元测试吗?如果我在mvc、mvp、mvvm的基础上做解耦,可以做的很彻底吗?
先告诉你答案, mvc、mvp、mvvm无法做到彻底的解耦,但是mvi+compose可以做到彻底的解耦,也就是本文的重点讲解部分。本文结合具体的代码和案例,复杂问题简单化,并且结合较多技术博客做了统一的总结,相信你读完会收获颇丰。
那么本篇文章编写的意义,就是为了能够深入浅出的讲解mvi+compose,大家可以先试想下这样的业务场景,如果是你,你会选择哪种架构实现?
业务场景考虑
使用手机号进行登录
登录完之后验证是否指定的账号a
如果是账号a,则进行点赞操作
上面三个步骤是顺序执行的,手机号的登录、账号的验证、点赞都是与服务端进行交互之后,获取对应的返回结果,然后再做下一步。
在开始介绍mvi+compose之前,需要循序渐进,了解每个架构模式的缺点,才知道为什么google提出mvi+compose。
正式开始前,按照架构模式的提出时间来看下是如何演变的,每个模式的提出往往不是基于android提出,而是基于服务端或者前端演进而来,这也说明设计思路上都是大同小异的:
二、架构模式过去式?
2.1 mvc已经存在很久了
mvc模式提出时间太久了,早在1978年就被提出,所以一定不是用于android,android的mvc架构主要还是源于服务端的springmvc,在2007年到2017年之间,mvc占据着主导地位,目前我们android中看到的mvc架构模式是这样的。
mvc架构这几个部分的含义如下,网上随便找找就有一堆说明。
mvc架构分为以下几个部分
【模型层model】:主要负责网络请求,数据库处理,i/o的操作,即页面的数据来源
【视图层view】:对应于xml布局文件和java代码动态view部分
【控制层controller】:主要负责业务逻辑,在android中由activity承担
(1)mvc代码示例
我们举个登录验证的例子来看下mvc架构一般怎么实现。
这个是controller
mvc架构实现登录流程-controller
public class mvcloginactivity extends appcompatactivity { private edittext usernameet; private edittext passwordet; private user user; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_mvc_login); user = new user(); usernameet = findviewbyid(r.id.user_name_et); passwordet = findviewbyid(r.id.password_et); button loginbtn = findviewbyid(r.id.login_btn); loginbtn.setonclicklistener(new view.onclicklistener() { @override public void onclick(view view) { loginutil.getinstance().dologin(usernameet.gettext().tostring(), passwordet.gettext().tostring(), new logincallback() { @override public void loginresult(@nonnull com.example.mvcmvpmvvm.mvc.model.user success) { if (null != user) { // 这里免不了的,会有业务处理 //1、保存用户账号 //2、loading消失 //3、大量的变量判断 //4、再做进一步的其他网络请求 toast.maketext(mvcloginactivity.this, login successful, toast.length_short) .show(); } else { toast.maketext(mvcloginactivity.this, login failed, toast.length_short) .show(); } } }); } }); } }  
这个是model
mvc架构实现登录流程-model
public class loginservice { public static loginutil getinstance() { return new loginutil(); } public void dologin(string username, string password, logincallback logincallback) { user user = new user(); if (username.equals(123456) && password.equals(123456)) { user.setusername(username); user.setpassword(password); logincallback.loginresult(user); } else { logincallback.loginresult(null); } }}  
例子很简单,主要做了下面这些事情
写一个专门的工具类loginservice,用来做网络请求dologin,验证登录账号是否正确,然后把验证结果返回。
activity调用loginservice,并且把账号信息传递给dologin方法,当获取到结果后,进行对应的业务操作。
(2)mvc优缺点
mvc在大部分简单业务场景下是够用的,主要优点如下:
结构清晰,职责划分清晰
降低耦合
有利于组件重用
但是随着时间的推移,你的mvc架构可能慢慢的演化成了下面的模式。拿上面的例子来说,你只做登录比较简单,但是当你的页面把登录账号校验、点赞都实现的时候,方法会比较多,共享一个view的时候,或者共同操作一个数据源的时候,就会出现变量满天飞,view四处被调用,相信大家也深有体会。
不可避免的,mvc就存在了下面的问题
归根究底,在android里面使用mvc的时候,对于model、view、controller的划分范围,总是那么的不明确,因为本身他们之间就有无法直接分割的依赖关系。所以总是避免不了这样的问题:
view与model之间还存在依赖关系,甚至有时候为了图方便,把model和view互传,搞得view和model耦合度极高,低耦合是面向对象设计标准之一,对于大型项目来说,高耦合会很痛苦,这在开发、测试,维护方面都需要花大量的精力。
那么在controller层,activity有时既要管理view,又要控制与用户的交互,充当controller,可想而知,当稍微有不规范的写法,这个activity就会很复杂,承担的功能会越来越多。
花了一定篇幅介绍mvc,是让大家对mvc中model、view、controller应该各自完成什么事情能深入理解,这样才有后面架构不断演进的意义。
2.2 mvp架构的由来
(1)mvp要解决什么问题?
2016年10月, google官方提供了mvp架构的sample代码来展示这种模式的用法,成为最流行的架构。
相对于mvc,mvp将activity复杂的逻辑处理移至另外的一个类(presenter)中,此时activity就是mvp模式中的view,它负责ui元素的初始化,建立ui元素与presenter的关联(listener之类),同时自己也会处理一些简单的逻辑(复杂的逻辑交由 presenter处理)。
那么mvp 同样将代码划分为三个部分:
结构说明
view:对应于activity与xml,只负责显示ui,只与presenter层交互,与model层没有耦合;
model: 负责管理业务数据逻辑,如网络请求、数据库处理;
presenter:负责处理大量的逻辑操作,避免activity的臃肿。
来看看mvp的架构图:
与mvc的最主要区别
view与model并不直接交互,而是通过与presenter交互来与model间接交互。而在mvc中view可以与model直接交互。
通常view与presenter是一对一的,但复杂的view可能绑定多个presenter来处理逻辑。而controller回归本源,首要职责是加载应用的布局和初始化用户界面,并接受并处理来自用户的操作请求,它是基于行为的,并且可以被多个view共享,controller可以负责决定显示哪个view。
presenter与view的交互是通过接口来进行的,更有利于添加单元测试。
(2)mvp代码示意
① 先来看包结构图
② 建立bean
mvp架构实现登录流程-model
public class user { private string username; private string password; public string getusername() { return ... } public void setusername(string username) { ...; } }  
③ 建立model接口 (处理业务逻辑,这里指数据读写),先写接口方法,后写实现
mvp架构实现登录流程-model
public interface iuserbiz { boolean login(string username, string password);}  
④ 建立presenter(主导器,通过iview和imodel接口操作model和view),activity可以把所有逻辑给presenter处理,这样java逻辑就从activity中分离出来。
mvp架构实现登录流程-model
public class loginpresenter{ private userbiz userbiz; private imvploginview imvploginview; public loginpresenter(imvploginview imvploginview) { this.imvploginview = imvploginview; this.userbiz = new userbiz(); } public void login() { string username = imvploginview.getusername(); string password = imvploginview.getpassword(); boolean isloginsuccessful = userbiz.login(username, password); imvploginview.onloginresult(isloginsuccessful); }}  
⑤ view视图建立view,用于更新ui中的view状态,这里列出需要操作当前view的方法,也是接口imvploginview 
mvp架构实现登录流程-model
public interface imvploginview { string getusername(); string getpassword(); void onloginresult(boolean isloginsuccess);}  
⑥ activity中实现imvploginview接口,在其中操作view,实例化一个presenter变量。
mvp架构实现登录流程-model
public class mvploginactivity extends appcompatactivity implements imvploginview{ private edittext usernameet; private edittext passwordet; private loginpresenter loginpresenter; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_mvp_login); usernameet = findviewbyid(r.id.user_name_et); passwordet = findviewbyid(r.id.password_et); button loginbtn = findviewbyid(r.id.login_btn); loginpresenter = new loginpresenter(this); loginbtn.setonclicklistener(new view.onclicklistener() { @override public void onclick(view view) { loginpresenter.login(); } }); } @override public string getusername() { return usernameet.gettext().tostring(); } @override public string getpassword() { return passwordet.gettext().tostring(); } @override public void onloginresult(boolean isloginsuccess) { if (isloginsuccess) { toast.maketext(mvploginactivity.this, getusername() + login successful, toast.length_short) .show(); } else { toast.maketext(mvploginactivity.this, login failed, toast.length_short).show(); } }}  
(3)mvp优缺点
因此,activity及从mvc中的controller中解放出来了,这会activity主要做显示view的作用和用户交互。每个activity可以根据自己显示view的不同实现view视图接口iuserview。
通过对比同一实例的mvc与mvp的代码,可以证实mvp模式的一些优点:
在mvp中,activity的代码不臃肿;
在mvp中,model(iusermodel的实现类)的改动不会影响activity(view),两者也互不干涉,而在mvc中会;
在mvp中,iuserview这个接口可以实现方便地对presenter的测试;
在mvp中,userpresenter可以用于多个视图,但是在mvc中的activity就不行。
但还是存在一些缺点:
双向依赖:view 和 presenter 是双向依赖的,一旦 view 层做出改变,相应地 presenter 也需要做出调整。在业务语境下,view 层变化是大概率事件;
内存泄漏风险:presenter 持有 view 层的引用,当用户关闭了 view 层,但 model 层仍然在进行耗时操作,就会有内存泄漏风险。虽然有解决办法,但还是存在风险点和复杂度(弱引用 / ondestroy() 回收 presenter)。
三、mvvm其实够用了
3.1mvvm思想存在很久了
mvvm最初是在2005年由微软提出的一个ui架构概念。后来在2015年的时候,开始应用于android中。
mvvm 模式改动在于中间的 presenter 改为 viewmodel,mvvm 同样将代码划分为三个部分:
view:activity 和 layout xml 文件,与 mvp 中 view 的概念相同;
model:负责管理业务数据逻辑,如网络请求、数据库处理,与 mvp 中 model 的概念相同;
viewmodel:存储视图状态,负责处理表现逻辑,并将数据设置给可观察数据容器。
与mvp唯一的区别是,它采用双向数据绑定(data-binding):view的变动,自动反映在 viewmodel,反之亦然。
mvvm架构图如下所示:
可以看出mvvm与mvp的主要区别在于,你不用去主动去刷新ui了,只要model数据变了,会自动反映到ui上。换句话说,mvvm更像是自动化的mvp。
mvvm的双向数据绑定主要通过databinding实现,但是大部分人应该跟我一样,不使用databinding,那么大家最终使用的mvvm架构就变成了下面这样:
总结一下:
实际使用mvvm架构说明
view观察viewmodel的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定,所以mvvm的双向绑定这一大特性我这里并没有用到
view通过调用viewmodel提供的方法来与viewmdoel交互。
3.2 mvvm代码示例
(1)建立viewmodel,并且提供一个可供view调取的方法 login(string username, string 
password)
mvvm架构实现登录流程-model
public class loginviewmodel extends viewmodel { private user user; private mutablelivedata isloginsuccessfulld; public loginviewmodel() { this.isloginsuccessfulld = new mutablelivedata(); user = new user(); } public mutablelivedata getisloginsuccessfulld() { return isloginsuccessfulld; } public void setisloginsuccessfulld(boolean isloginsuccessful) { isloginsuccessfulld.postvalue(isloginsuccessful); } public void login(string username, string password) { if (username.equals(123456) && password.equals(123456)) { user.setusername(username); user.setpassword(password); setisloginsuccessfulld(true); } else { setisloginsuccessfulld(false); } } public string getusername() { return user.getusername(); }}  
(2)在activity中声明viewmodel,并建立观察。点击按钮,触发 login(string username, string password)。持续作用的观察者loginobserver。只要loginviewmodel 中的isloginsuccessfulld变化,就会对应的有响应
mvvm架构实现登录流程-model
public class mvvmloginactivity extends appcompatactivity { private loginviewmodel loginvm; private edittext usernameet; private edittext passwordet; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_mvvm_login); usernameet = findviewbyid(r.id.user_name_et); passwordet = findviewbyid(r.id.password_et); button loginbtn = findviewbyid(r.id.login_btn); loginbtn.setonclicklistener(new view.onclicklistener() { @override public void onclick(view view) { loginvm.login(usernameet.gettext().tostring(), passwordet.gettext().tostring()); } }); loginvm = new viewmodelprovider(this).get(loginviewmodel.class); loginvm.getisloginsuccessfulld().observe(this, loginobserver); } private observer loginobserver = new observer() { @override public void onchanged(@nullable boolean isloginsuccessful) { if (isloginsuccessful) { toast.maketext(mvvmloginactivity.this, 登录成功, toast.length_short) .show(); } else { toast.maketext(mvvmloginactivity.this, 登录失败, toast.length_short) .show(); } } };}  
3.3 mvvm优缺点
通过上面的代码,可以总结出mvvm的优点:
在实现细节上,view 和 presenter 从双向依赖变成 view 可以向 viewmodel 发指令,但 viewmodel 不会直接向 view 回调,而是让 view 通过观察者的模式去监听数据的变化,有效规避了 mvp 双向依赖的缺点。
但 mvvm 在某些情况下,也存在一些缺点:
(1)关联性比较强的流程,livedata太多,并且理解成本较高
当业务比较复杂的时候,在viewmodel中必然存在着比较多的livedata去管理。当然,如果你去管理好这些livedata,让他们去处理业务流程,问题也不大,只不过理解的成本会高些。
(2)不便于单元测试
viewmodel里面一般都是对数据库和网络数据进行处理,包含了业务逻辑在里面,当要去对某一流程进行测试时,并没有办法完全剥离数据逻辑的处理流程,单元测试也就增加了难度。
那么我们来看看缺点对应的具体场景是什么,便于我们后续进一步探讨mvi架构。
(1)在上面登录之后,需要验证账号信息,然后再自动进行点赞。那么,viewmodel里面对应的增加几个方法,每个方法对应一个livedata
mvvm架构实现登录流程-model
public class loginmultiviewmodel extends viewmodel { private user user; // 是否登录成功 private mutablelivedata isloginsuccessfulld; // 是否为指定账号 private mutablelivedata ismyaccountld; // 如果是指定账号,进行点赞 private mutablelivedata gothumbup; public loginmultiviewmodel() { this.isloginsuccessfulld = new mutablelivedata(); this.ismyaccountld = new mutablelivedata(); this.gothumbup = new mutablelivedata(); user = new user(); } public mutablelivedata getisloginsuccessfulld() { return isloginsuccessfulld; } public mutablelivedata getismyaccountld() { return ismyaccountld; } public mutablelivedata getgothumbupld() { return gothumbup; } ... public void login(string username, string password) { if (username.equals(123456) && password.equals(123456)) { user.setusername(username); user.setpassword(password); setisloginsuccessfulld(true); } else { setisloginsuccessfulld(false); } } public void ismyaccount(@nonnull string username) { try { thread.sleep(1000); } catch (exception ex) { } if (username.equals(123456)) { setismyaccountsuccessfulld(true); } else { setismyaccountsuccessfulld(false); } } public void gothumbup(boolean ismyaccount) { setgothumbupld(ismyaccount); } public string getusername() { return user.getusername(); }}  
(2)再来看看你可能使用的一种处理逻辑,在判断登录成功之后,使用变量isloginsuccessful再去做 loginvm.ismyaccount(usernameet.gettext().tostring());在账号验证成功之后,再去通过变量ismyaccount去做loginvm.gothumbup(true);
mvvm架构实现登录流程-model
public class mvvmfaultloginactivity extends appcompatactivity { private loginmultiviewmodel loginvm; private edittext usernameet; private edittext passwordet; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_mvvm_fault_login); usernameet = findviewbyid(r.id.user_name_et); passwordet = findviewbyid(r.id.password_et); button loginbtn = findviewbyid(r.id.login_btn); loginbtn.setonclicklistener(new view.onclicklistener() { @override public void onclick(view view) { loginvm.login(usernameet.gettext().tostring(), passwordet.gettext().tostring()); } }); loginvm = new viewmodelprovider(this).get(loginmultiviewmodel.class); loginvm.getisloginsuccessfulld().observe(this, loginobserver); loginvm.getismyaccountld().observe(this, ismyaccountobserver); loginvm.getgothumbupld().observe(this, gothumbupobserver); } private observer loginobserver = new observer() { @override public void onchanged(@nullable boolean isloginsuccessful) { if (isloginsuccessful) { toast.maketext(mvvmfaultloginactivity.this, 登录成功,开始校验账号, toast.length_short).show(); loginvm.ismyaccount(usernameet.gettext().tostring()); } else { toast.maketext(mvvmfaultloginactivity.this, 登录失败, toast.length_short) .show(); } } }; private observer ismyaccountobserver = new observer() { @override public void onchanged(@nullable boolean ismyaccount) { if (ismyaccount) { toast.maketext(mvvmfaultloginactivity.this, 校验成功,开始点赞, toast.length_short).show(); loginvm.gothumbup(true); } } }; private observer gothumbupobserver = new observer() { @override public void onchanged(@nullable boolean isthumbupsuccess) { if (isthumbupsuccess) { toast.maketext(mvvmfaultloginactivity.this, 点赞成功, toast.length_short) .show(); } else { toast.maketext(mvvmfaultloginactivity.this, 点赞失败, toast.length_short) .show(); } } };}  
毫无疑问,这种交互在实际开发中是可能存在的,页面比较复杂的时候,这种变量也就滋生了。这种场景,就有必要聊聊mvi架构了。
四、mvi有存在的必要性吗?
4.1 mvi的由来
mvi 模式来源于2014年的 cycle.js(一个 javascript框架),并且在主流的 js 框架 redux 中大行其道,然后就被一些大佬移植到了 android 上(比如最早期用java写的  mosby)。
既然mvvm是目前android官方推荐的架构,又为什么要有mvi呢?其实应用架构指南中并没有提出mvi的概念,而是提到了单向数据流,唯一数据源,这也是区别mvvm的特性。
不过还是要说明一点,凡是mvi做到的,只要你使用mvvm去实现,基本上也能做得到。只是说在接下来要讲的内容里面,mvi具备的封装思路,是可以直接使用的,并且是便于单元测试的。
mvi的思想:靠数据驱动页面 (其实当你把这种思想应用在各个框架的时候,你的那个框架都会更加优雅)
mvi架构包括以下几个部分
model:主要指ui状态(state)。例如页面加载状态、控件位置等都是一种ui状态。
view: 与其他mvx中的view一致,可能是一个activity或者任意ui承载单元。mvi中的view通过订阅model的变化实现界面刷新。
intent: 此intent不是activity的intent,用户的任何操作都被包装成intent后发送给model层进行数据请求。
看下交互流程图:
对流程图做下解释说明:
(1)用户操作以intent的形式通知model
(2)model基于intent更新state。这个里面包括使用viewmodel进行网络请求,更新state的操作
(3)view接收到state变化刷新ui。
4.2 mvi的代码示例
直接看代码吧
(1)先看下包结构
(2)用户点击按钮,发起登录流程
loginviewmodel.loginactionintent.send(loginactionintent.dologin(usernameet.text.tostring(), passwordet.text.tostring()))。
此处是发送了一个intent出去
mvi架构代码-view
loginbtn.setonclicklistener { lifecyclescope.launch { loginviewmodel.loginactionintent.send(loginactionintent.dologin(usernameet.text.tostring(), passwordet.text.tostring())) } }  
(3)viewmodel对intent进行监听
initactionintent()。在这里可以把按钮点击事件的intent消费掉
mvi架构代码-model
class loginviewmodel : viewmodel() { companion object { const val tag = loginviewmodel } private val _repository = loginrepository() val loginactionintent = channel(channel.unlimited) private val _loginactionstate = mutablesharedflow() val state: sharedflow get() = _loginactionstate init { // 可以用来初始化一些页面或者参数 initactionintent() } private fun initactionintent() { viewmodelscope.launch { loginactionintent.consumeasflow().collect { when (it) { is loginactionintent.dologin -> { dologin(it.username, it.password) } else -> { } } } } } }  
(4)使用respository进行网络请求,更新state
mvi架构代码-repository
class loginrepository { suspend fun requestlogindata(username: string, password: string) : boolean { delay(1000) if (username == 123456 && password == 123456) { return true } return false } suspend fun requestismyaccount(username: string, password: string) : boolean { delay(1000) if (username == 123456) { return true } return false } suspend fun requestthumbup(username: string, password: string) : boolean { delay(1000) if (username == 123456) { return true } return false }}  
mvi架构代码-更新state
private fun dologin(username: string, password: string) { viewmodelscope.launch { if (username.isempty() || password.isempty()) { return@launch } // 设置页面正在加载 _loginactionstate.emit(loginactionstate.loginloading(username, password)) // 开始请求数据 val loginresult = _repository.requestlogindata(username, password) if (!loginresult) { //登录失败 _loginactionstate.emit(loginactionstate.loginfailed(username, password)) return@launch } _loginactionstate.emit(loginactionstate.loginsuccessful(username, password)) //登录成功继续往下 val ismyaccount = _repository.requestismyaccount(username, password) if (!ismyaccount) { //校验账号失败 _loginactionstate.emit(loginactionstate.ismyaccountfailed(username, password)) return@launch } _loginactionstate.emit(loginactionstate.ismyaccountsuccessful(username, password)) //校验账号成功继续往下 val isthumbupsuccess = _repository.requestthumbup(username, password) if (!isthumbupsuccess) { //点赞失败 _loginactionstate.emit(loginactionstate.gothumbupfailed(username, password)) return@launch } //点赞成功继续往下 _loginactionstate.emit(loginactionstate.gothumbupsuccessful(true)) } }  
(5)在view中监听state的变化,做页面刷新
mvi架构代码-repository
fun observeviewmodel() { lifecyclescope.launch { loginviewmodel.state.collect { when(it) { is loginactionstate.loginloading -> { toast.maketext(basecontext, 登录中, toast.length_short).show() } is loginactionstate.loginfailed -> { toast.maketext(basecontext, 登录失败, toast.length_short).show() } is loginactionstate.loginsuccessful -> { toast.maketext(basecontext, 登录成功,开始校验账号, toast.length_short).show() } is loginactionstate.ismyaccountsuccessful -> { toast.maketext(basecontext, 校验成功,开始点赞, toast.length_short).show() } is loginactionstate.gothumbupsuccessful -> { resultview.text = 点赞成功 toast.maketext(basecontext, 点赞成功, toast.length_short).show() } else -> {} } } } }  
通过这个流程,可以看到用户点击登录操作,一直到最后刷新页面,是一个串行的操作。在这种场景下,使用mvi架构,再合适不过
4.2 mvi的优缺点
(1)mvi的优点如下:
可以更好的进行单元测试
针对上面的案例,使用mvi这种单向数据流的形式要比mvvm更加的合适,并且便于单元测试,每个节点都较为独立,没有代码上的耦合。
订阅一个 viewstate 就可以获取所有状态和数据
不需要像mvvm那样管理多个livedata,可以直接使用一个state进行管理,相比 mvvm 是新的特性。
但mvi 本身也存在一些缺点:
state 膨胀: 所有视图变化都转换为 viewstate,还需要管理不同状态下对应的数据。实践中应该根据状态之间的关联程度来决定使用单流还是多流;
内存开销: viewstate 是不可变类,状态变更时需要创建新的对象,存在一定内存开销;
局部刷新: view 根据 viewstate 响应,不易实现局部 diff 刷新,可以使用 flow#distinctuntilchanged() 来刷新来减少不必要的刷新。
更关键的一点,即使单向数据流封装的很多,仍然避免不了来一个新人,不遵守这个单向数据流的写法,随便去处理view。这时候就要去引用compose了。
五、不妨利用compose升级mvi
2021年,谷歌发布jetpack compose1.0,2022年,又更新了文章应用架构指南,在进行界面层的搭建时,建议方案如下:
在屏幕上呈现数据的界面元素。您可以使用 view 或 jetpack compose 函数构建这些元素。
用于存储数据、向界面提供数据以及处理逻辑的状态容器(如 viewmodel 类)。
为什么这里会提到compose?
使用compose的原因之一
即使你使用了mvi架构,但是当有人不遵守这个设计理念时,从代码层面是无法避免别人使用非mvi架构,久而久之,导致你的代码混乱。
意思就是说,你在使用mvi架构搭建页面之后,有个人突然又引入了mvc的架构,是无法避免的。compose可以完美解决这个问题。
接下来就是本文与其他技术博客不一样的地方,把compose如何使用,为什么这样使用做下说明,不要只看理论,最好实战。
5.1 compose的主要作用
compose可以做到界面view在一开始的时候就要绑定数据源,从而达到无法在其他地方被篡改的目的。
怎么理解?
当你有个textview被声明之后,按照之前的架构,可以获取这个textview,并且给它的text随意赋值,这就导致了textview就有可能不止是在mvi架构里面使用,也可能在mvc架构里面使用。
5.2 mvi+compose的代码示例
mvi+compose架构代码
class mvicomposeloginactivity : componentactivity() { override fun oncreate(savedinstancestate: bundle?) { super.oncreate(savedinstancestate) lifecyclescope.launch { setcontent { boxwithconstraints( modifier = modifier .background(colorresource(id = r.color.white)) .fillmaxsize() ) { loginconstrainttodo() } } } } @composable fun editortextfield(textfieldstate: textfieldstate, label : string, modifier: modifier = modifier) { // 定义一个可观测的text,用来在textfield中展示 textfield( value = textfieldstate.text, // 显示文本 onvaluechange = { textfieldstate.text = it }, // 文字改变时,就赋值给text modifier = modifier, label = { text(text = label) }, // label是input placeholder = @composable { text(text = 123456) }, // 不输入内容时的占位符 ) } @suppresslint(coroutinecreationduringcomposition) @composable internal fun loginconstrainttodo(model: composeloginviewmodel = viewmodel()){ val state by model.uistate.collectasstate() val context = localcontext.current loginconstraintlayout( onloginbtnclick = { text1, text2 -> lifecyclescope.launch { model.sendevent(todoevent.dologin(text1, text2)) } }, state.isthumbupsuccessful ) when { state.isloginsuccessful -> { toast.maketext(basecontext, 登录成功,开始校验账号, toast.length_short).show() model.sendevent(todoevent.verifyaccount(123456, 123456)) } state.isaccountsuccessful -> { toast.maketext(basecontext, 账号校验成功,开始点赞, toast.length_short).show() model.sendevent(todoevent.thumbup(123456, 123456)) } state.isthumbupsuccessful -> { toast.maketext(basecontext, 点赞成功, toast.length_short).show() } } } @composable fun loginconstraintlayout(onloginbtnclick: (string, string) -> unit, thumbupsuccessful: boolean){ constraintlayout() { //通过createrefs创建三个引用 // 初始化声明两个元素,如果只声明一个,则可用 createref() 方法 // 这里声明的类似于 view 的 id val (firsttext, secondtext, button, text) = createrefs() val firsteditor = remember { textfieldstate() } val secondeditor = remember { textfieldstate() } editortextfield(firsteditor,123456, modifier.constrainas(firsttext) { top.linkto(parent.top, margin = 16.dp) start.linkto(parent.start) centerhorizontallyto(parent) // 摆放在 constraintlayout 水平中间 }) editortextfield(secondeditor,123456, modifier.constrainas(secondtext) { top.linkto(firsttext.bottom, margin = 16.dp) start.linkto(firsttext.start) centerhorizontallyto(parent) // 摆放在 constraintlayout 水平中间 }) button( onclick = { onloginbtnclick(123456, 123456) }, // constrainas() 将 composable 组件与初始化的引用关联起来 // 关联之后就可以在其他组件中使用并添加约束条件了 modifier = modifier.constrainas(button) { // 熟悉 constraintlayout 约束写法的一眼就懂 // parent 引用可以直接用,跟 view 体系一样 top.linkto(secondtext.bottom, margin = 20.dp) start.linkto(secondtext.start, margin = 10.dp) } ){ text(login) } text(if (thumbupsuccessful) 点赞成功 else 点赞失败, modifier.constrainas(text) { top.linkto(button.bottom, margin = 36.dp) start.linkto(button.start) centerhorizontallyto(parent) // 摆放在 constraintlayout 水平中间 }) } }  
关键代码段就在于下面:
mvi+compose架构代码
text(if (thumbupsuccessful) 点赞成功 else 点赞失败, modifier.constrainas(text) { top.linkto(button.bottom, margin = 36.dp) start.linkto(button.start) centerhorizontallyto(parent) // 摆放在 constraintlayout 水平中间})  
textview的text在页面初始化的时候就跟数据源中的thumbupsuccessful变量进行了绑定,并且这个textview不可以在其他地方二次赋值,只能通过这个变量thumbupsuccessful进行修改数值。当然,使用这个方法,也解决了数据更新是无法diff更新的问题,堪称完美了。
5.3 mvi+compose的优缺点
mvi+compose的优点如下:
保证了框架的唯一性
由于每个view是在一开始的时候就被数据源赋值的,无法被多处调用随意修改,所以保证了框架不会被随意打乱。更好的保证了代码的低耦合等特点。
mvi+compose的也存在一些缺点:
不能称为缺点的缺点吧。
由于compose实现界面,是纯靠kotlin代码实现,没有借助xml布局,这样的话,一开始学习的时候,学习成本要高些。并且性能还未知,最好不要用在一级页面。
六、如何选择框架模式
6.1 架构选择的原理
通过上面这么多架构的对比,可以总结出下面的结论。
耦合度高是现象,关注点分离是手段,易维护性和易测试性是结果,模式是可复用的经验。
再来总结一下上面几个框架适用的场景:
6.2 框架的选择原理
如果你的页面相对来说比较简单些,比如就是一个网络请求,然后刷新列表,使用mvc就够了。
如果你有很多页面共用相同的逻辑,比如多个页面都有网络请求加载中、网络请求、网络请求加载完成、网络请求加载失败这种,使用mvp、mvvm、mvi,把接口封装好更好些。
如果你需要在多处监听数据源的变化,这时候需要使用livedata或者flow,也就是mvvm、mvi的架构好些。
如果你的操作是串行的,比如登录之后进行账号验证、账号验证完再进行点赞,这时候使用mvi更好些。当然,mvi+compose可以保证你的架构不易被修改。
切勿混合使用架构模式,分析透彻页面结构之后,选择一种架构即可,不然会导致页面越来越复杂,无法维护
上面就是对所有框架模式的总结,大家根据实际情况进行选择。建议还是直接上手最新的mvi+compose,虽然多了些学习成本,但是毕竟compose的思想还是很值得借鉴的。


单片机的多功能温度控制器的设计
把工作简单化,DSP与数据转换器协同工作需考虑这些因素
医院综合安防系统的组成、特点及方案设计
ADAS实现更安全的移动驾驶场景
浅析联电在半导体行业的崛起之路
Android架构模式飞速演进 到底哪一个才是自己最需要的?
华为mate10什么时候上市?华为mate10配置确认,小米mix2又来对标华为mate10,你看好谁?
如何部署物联网数据,云端架构或最适宜
电源管理芯片有什么特点?其常见故障如何检测?
刻录机如何实现刻录速度与质量兼得
4515DO-DS3BS002DP传感器检测洁净实验室压差的方法
如何制作带超声波传感器的无限远镜
电子激光射击分组对抗系统的原理以及系统特点是什么?
工信部苗圩:5G未来的应用场景主要体验在车联网和远程医疗方面
手环加速度传感器原理及选型技巧
RT-Thread操作系统的FreeRTOS兼容层
前谷歌自动驾驶明星工程师承认窃取谷歌商业机密 将面临长达30个月的监禁及以及1.79亿美元违约金
苹果手机对于充电接口是如何规划的
嵌入式系统的SD控制器设计实现
移轴镜头实拍技巧2 拍摄出长腿美人