大连网站建设讯息,零基础学软件开发难吗,哪个网站是专门做男人衣服的,个人主页网址怎么写作者#xff1a;vivo 互联网客户端团队-Xu Jie Android架构模式飞速演进#xff0c;目前已经有MVC、MVP、MVVM、MVI。到底哪一个才是自己业务场景最需要的#xff0c;不深入理解的话是无法进行选择的。这篇文章就针对这些架构模式逐一解读。重点会介绍Compose为什么要结合MV… 作者vivo 互联网客户端团队-Xu Jie 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无法做到彻底的解耦但是MVICompose可以做到彻底的解耦也就是本文的重点讲解部分。本文结合具体的代码和案例复杂问题简单化并且结合较多技术博客做了统一的总结相信你读完会收获颇丰。
那么本篇文章编写的意义就是为了能够深入浅出的讲解MVICompose大家可以先试想下这样的业务场景如果是你你会选择哪种架构实现
业务场景考虑 使用手机号进行登录 登录完之后验证是否指定的账号A 如果是账号A则进行点赞操作
上面三个步骤是顺序执行的手机号的登录、账号的验证、点赞都是与服务端进行交互之后获取对应的返回结果然后再做下一步。
在开始介绍MVICompose之前需要循序渐进了解每个架构模式的缺点才知道为什么Google提出MVICompose。
正式开始前按照架构模式的提出时间来看下是如何演变的每个模式的提出往往不是基于android提出而是基于服务端或者前端演进而来这也说明设计思路上都是大同小异的 二、架构模式过去式
2.1 MVC已经存在很久了
MVC模式提出时间太久了早在1978年就被提出所以一定不是用于androidandroid的MVC架构主要还是源于服务端的SpringMVC在2007年到2017年之间MVC占据着主导地位目前我们android中看到的MVC架构模式是这样的。
MVC架构这几个部分的含义如下网上随便找找就有一堆说明。
MVC架构分为以下几个部分 【模型层Model】主要负责网络请求数据库处理I/O的操作即页面的数据来源 【视图层View】对应于xml布局文件和java代码动态view部分 【控制层Controller】主要负责业务逻辑在android中由Activity承担
1MVC代码示例
我们举个登录验证的例子来看下MVC架构一般怎么实现。
这个是controller
MVC架构实现登录流程-controller
public class MvcLoginActivity extends AppCompatActivity {private EditText userNameEt;private EditText passwordEt;private User user;Overrideprotected 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() {Overridepublic void onClick(View view) {LoginUtil.getInstance().doLogin(userNameEt.getText().toString(), passwordEt.getText().toString(), new LoginCallBack() {Overridepublic 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方法当获取到结果后进行对应的业务操作。
2MVC优缺点
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架构的由来
1MVP要解决什么问题
2016年10月 Google官方提供了MVP架构的Sample代码来展示这种模式的用法成为最流行的架构。
相对于MVCMVP将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的交互是通过接口来进行的更有利于添加单元测试。 2MVP代码示意
① 先来看包结构图 ② 建立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和viewactivity可以把所有逻辑给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;Overrideprotected 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() {Overridepublic void onClick(View view) {loginPresenter.login();}});}Overridepublic String getUserName() {return userNameEt.getText().toString();}Overridepublic String getPassword() {return passwordEt.getText().toString();}Overridepublic 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();}}
}
3MVP优缺点
因此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 改为 ViewModelMVVM 同样将代码划分为三个部分 ViewActivity 和 Layout XML 文件与 MVP 中 View 的概念相同 Model负责管理业务数据逻辑如网络请求、数据库处理与 MVP 中 Model 的概念相同 ViewModel存储视图状态负责处理表现逻辑并将数据设置给可观察数据容器。
与MVP唯一的区别是它采用双向数据绑定data-bindingView的变动自动反映在 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 MutableLiveDataBoolean isLoginSuccessfulLD;public LoginViewModel() {this.isLoginSuccessfulLD new MutableLiveData();user new User();}public MutableLiveDataBoolean 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;Overrideprotected 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() {Overridepublic 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 ObserverBoolean loginObserver new ObserverBoolean() {Overridepublic 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 MutableLiveDataBoolean isLoginSuccessfulLD;// 是否为指定账号private MutableLiveDataBoolean isMyAccountLD;// 如果是指定账号进行点赞private MutableLiveDataBoolean goThumbUp;public LoginMultiViewModel() {this.isLoginSuccessfulLD new MutableLiveData();this.isMyAccountLD new MutableLiveData();this.goThumbUp new MutableLiveData();user new User();}public MutableLiveDataBoolean getIsLoginSuccessfulLD() {return isLoginSuccessfulLD;}public MutableLiveDataBoolean getIsMyAccountLD() {return isMyAccountLD;}public MutableLiveDataBoolean 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;Overrideprotected 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() {Overridepublic 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 ObserverBoolean loginObserver new ObserverBoolean() {Overridepublic 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 ObserverBoolean isMyAccountObserver new ObserverBoolean() {Overridepublic void onChanged(Nullable Boolean isMyAccount) {if (isMyAccount) {Toast.makeText(MvvmFaultLoginActivity.this, 校验成功开始点赞, Toast.LENGTH_SHORT).show();loginVM.goThumbUp(true);}}};private ObserverBoolean goThumbUpObserver new ObserverBoolean() {Overridepublic 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的形式通知Model2Model基于Intent更新State。这个里面包括使用ViewModel进行网络请求更新State的操作3View接收到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()))}}
3ViewModel对Intent进行监听
initActionIntent()。在这里可以把按钮点击事件的Intent消费掉
MVI架构代码-Model
class LoginViewModel : ViewModel() {companion object {const val TAG LoginViewModel}private val _repository LoginRepository()val loginActionIntent ChannelLoginActionIntent(Channel.UNLIMITED)private val _loginActionState MutableSharedFlowLoginActionState()val state: SharedFlowLoginActionStateget() _loginActionStateinit {// 可以用来初始化一些页面或者参数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()) {returnlaunch}// 设置页面正在加载_loginActionState.emit(LoginActionState.LoginLoading(username, password))// 开始请求数据val loginResult _repository.requestLoginData(username, password)if (!loginResult) {//登录失败_loginActionState.emit(LoginActionState.LoginFailed(username, password))returnlaunch}_loginActionState.emit(LoginActionState.LoginSuccessful(username, password))//登录成功继续往下val isMyAccount _repository.requestIsMyAccount(username, password)if (!isMyAccount) {//校验账号失败_loginActionState.emit(LoginActionState.IsMyAccountFailed(username, password))returnlaunch}_loginActionState.emit(LoginActionState.IsMyAccountSuccessful(username, password))//校验账号成功继续往下val isThumbUpSuccess _repository.requestThumbUp(username, password)if (!isThumbUpSuccess) {//点赞失败_loginActionState.emit(LoginActionState.GoThumbUpFailed(username, password))returnlaunch}//点赞成功继续往下_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.3 MVI的优缺点
1MVI的优点如下 可以更好的进行单元测试
针对上面的案例使用MVI这种单向数据流的形式要比MVVM更加的合适并且便于单元测试每个节点都较为独立没有代码上的耦合。 订阅一个 ViewState 就可以获取所有状态和数据
不需要像MVVM那样管理多个LiveData可以直接使用一个state进行管理相比 MVVM 是新的特性。
但MVI 本身也存在一些缺点 State 膨胀 所有视图变化都转换为 ViewState还需要管理不同状态下对应的数据。实践中应该根据状态之间的关联程度来决定使用单流还是多流 内存开销 ViewState 是不可变类状态变更时需要创建新的对象存在一定内存开销 局部刷新 View 根据 ViewState 响应不易实现局部 Diff 刷新可以使用 Flow#distinctUntilChanged() 来刷新来减少不必要的刷新。
更关键的一点即使单向数据流封装的很多仍然避免不了来一个新人不遵守这个单向数据流的写法随便去处理view。这时候就要去引用Compose了。
五、不妨利用Compose升级MVI
这一章节是本文的重点。
2021年谷歌发布Jetpack Compose1.02022年又更新了文章应用架构指南在进行界面层的搭建时建议方案如下 在屏幕上呈现数据的界面元素。您可以使用 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 MVICompose的代码示例
MVICompose架构代码
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 }, // 文字改变时就赋值给textmodifier modifier,label { Text(text label) }, // label是Inputplaceholder Composable { Text(text 123456) }, // 不输入内容时的占位符)
}SuppressLint(CoroutineCreationDuringComposition)
Composable
internal fun loginConstraintToDo(model: ComposeLoginViewModel viewModel()){val state by model.uiState.collectAsState()val context LocalContext.currentloginConstraintLayout(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 的 idval (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 水平中间})}
}关键代码段就在于下面
MVICompose架构代码
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 MVICompose的优缺点
MVICompose的优点如下 保证了框架的唯一性
由于每个view是在一开始的时候就被数据源赋值的无法被多处调用随意修改所以保证了框架不会被随意打乱。更好的保证了代码的低耦合等特点。
MVICompose的也存在一些缺点
不能称为缺点的缺点吧。 由于Compose实现界面是纯靠kotlin代码实现没有借助xml布局这样的话一开始学习的时候学习成本要高些。并且性能还未知最好不要用在一级页面。
六、如何选择框架模式
6.1 架构选择的原理
通过上面这么多架构的对比可以总结出下面的结论。
耦合度高是现象关注点分离是手段易维护性和易测试性是结果模式是可复用的经验。
再来总结一下上面几个框架适用的场景
6.2 框架的选择原理 如果你的页面相对来说比较简单些比如就是一个网络请求然后刷新列表使用MVC就够了。 如果你有很多页面共用相同的逻辑比如多个页面都有网络请求加载中、网络请求、网络请求加载完成、网络请求加载失败这种使用MVP、MVVM、MVI把接口封装好更好些。 如果你需要在多处监听数据源的变化这时候需要使用LiveData或者Flow也就是MVVM、MVI的架构好些。 如果你的操作是串行的比如登录之后进行账号验证、账号验证完再进行点赞这时候使用MVI更好些。当然MVICompose可以保证你的架构不易被修改。
切勿混合使用架构模式分析透彻页面结构之后选择一种架构即可不然会导致页面越来越复杂无法维护。
上面就是对所有框架模式的总结大家根据实际情况进行选择。建议还是直接上手最新 MVICompose虽然多了些学习成本但是毕竟Compose的思想还是很值得借鉴的。