Android架构模式如何选择
OSC开源社区
2023-08-18 18:34:33
0

原标题:Android架构模式如何选择

作者: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无法做到彻底的解耦,但是MVI+Compose可以做到彻底的解耦,也就是本文的重点讲解部分。本文结合具体的代码和案例,复杂问题简单化,并且结合较多技术博客做了统一的总结,相信你读完会收获颇丰。

那么本篇文章编写的意义,就是为了能够深入浅出的讲解MVI+Compose,大家可以先试想下这样的业务场景,如果是你,你会选择哪种架构实现?

业务场景考虑

  1. 使用手机号进行登录
  2. 登录完之后验证是否指定的账号A
  3. 如果是账号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

publicclassMvcLoginActivityextendsAppCompatActivity{ privateEditText userNameEt; privateEditText passwordEt; privateUser user; @Override protectedvoidonCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_mvc_login); user = newUser; userNameEt = findViewById(R.id.user_name_et); passwordEt = findViewById(R.id.password_et); Button loginBtn = findViewById(R.id.login_btn); loginBtn.setOnClickListener( newView.OnClickListener { @Override publicvoidonClick(View view){ LoginUtil.getInstance.doLogin(userNameEt.getText.toString, passwordEt.getText.toString, newLoginCallBack { @Override publicvoidloginResult(@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

publicclassLoginService{ publicstaticLoginUtil getInstance( ) { returnnewLoginUtil; } publicvoiddoLogin( String userName, String password, LoginCallBack loginCallBack) { User user = newUser; 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在大部分简单业务场景下是够用的,主要优点如下:

  1. 结构清晰,职责划分清晰
  2. 降低耦合
  3. 有利于组件重用

但是随着时间的推移,你的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

publicclassUser { privateStringuserName; privateStringpassword; publicStringgetUserName { return... } publicvoidsetUserName( StringuserName) { ...; } }

③ 建立Model接口 (处理业务逻辑,这里指数据读写),先写接口方法,后写实现

MVP架构实现登录流程-model

publicinterfaceIUserBiz { booleanlogin( StringuserName, Stringpassword); }

④ 建立presenter(主导器,通过iView和iModel接口操作model和view),activity可以把所有逻辑给presenter处理,这样java逻辑就从activity中分离出来。

MVP架构实现登录流程-model

publicclassLoginPresenter{ privateUserBiz userBiz; privateIMvpLoginView iMvpLoginView; publicLoginPresenter(IMvpLoginView iMvpLoginView){ this.iMvpLoginView = iMvpLoginView; this.userBiz = newUserBiz; } publicvoidlogin{ String userName = iMvpLoginView.getUserName; String password = iMvpLoginView.getPassword; booleanisLoginSuccessful = userBiz.login(userName, password); iMvpLoginView.onLoginResult(isLoginSuccessful); } }

⑤ View视图建立view,用于更新ui中的view状态,这里列出需要操作当前view的方法,也是接口IMvpLoginView

MVP架构实现登录流程-model

publicinterfaceIMvpLoginView{ String getUserName( ) ; String getPassword( ) ; voidonLoginResult( Boolean isLoginSuccess) ; }

⑥ activity中实现IMvpLoginView接口,在其中操作view,实例化一个presenter变量。

MVP架构实现登录流程-model

publicclassMvpLoginActivityextendsAppCompatActivityimplementsIMvpLoginView{ privateEditText userNameEt; privateEditText passwordEt; privateLoginPresenter loginPresenter; @Override protectedvoidonCreate(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 = newLoginPresenter( this); loginBtn.setOnClickListener( newView.OnClickListener { @Override publicvoidonClick(View view){ loginPresenter.login; } }); } @Override publicString getUserName{ returnuserNameEt.getText.toString; } @Override publicString getPassword{ returnpasswordEt.getText.toString; } @Override publicvoidonLoginResult(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 同样将代码划分为三个部分:

  1. View:Activity 和 Layout XML 文件,与 MVP 中 View 的概念相同;
  2. Model:负责管理业务数据逻辑,如网络请求、数据库处理,与 MVP 中 Model 的概念相同;
  3. 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

publicclassLoginViewModelextendsViewModel{ privateUser user; privateMutableLiveData isLoginSuccessfulLD; publicLoginViewModel{ this.isLoginSuccessfulLD = newMutableLiveData<>; user = newUser; } publicMutableLiveData getIsLoginSuccessfulLD{ returnisLoginSuccessfulLD; } publicvoidsetIsLoginSuccessfulLD( booleanisLoginSuccessful) { isLoginSuccessfulLD.postValue(isLoginSuccessful); } publicvoidlogin(String userName, String password){ if(userName.equals( "123456") && password.equals( "123456")) { user.setUserName(userName); user.setPassword(password); setIsLoginSuccessfulLD( true); } else{ setIsLoginSuccessfulLD( false); } } publicString getUserName{ returnuser.getUserName; } }

(2)在activity中声明viewModel,并建立观察。点击按钮,触发 login(String userName, String password)。持续作用的观察者loginObserver。只要LoginViewModel 中的isLoginSuccessfulLD变化,就会对应的有响应

MVVM架构实现登录流程-model

publicclassMvvmLoginActivityextendsAppCompatActivity{ privateLoginViewModel loginVM; privateEditText userNameEt; privateEditText passwordEt; @Override protectedvoidonCreate(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( newView.OnClickListener { @Override publicvoidonClick(View view){ loginVM.login(userNameEt.getText.toString, passwordEt.getText.toString); } }); loginVM = newViewModelProvider( this).get(LoginViewModel.class); loginVM.getIsLoginSuccessfulLD.observe( this, loginObserver); } privateObserver loginObserver = newObserver { @Override publicvoidonChanged(@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

publicclassLoginMultiViewModel extendsViewModel { privateUser user; // 是否登录成功 privateMutableLiveData< Boolean> isLoginSuccessfulLD; // 是否为指定账号 privateMutableLiveData< Boolean> isMyAccountLD; // 如果是指定账号,进行点赞 privateMutableLiveData< Boolean> goThumbUp; publicLoginMultiViewModel { this.isLoginSuccessfulLD = newMutableLiveData<>; this.isMyAccountLD = newMutableLiveData<>; this.goThumbUp = newMutableLiveData<>; user = newUser; } publicMutableLiveData< Boolean> getIsLoginSuccessfulLD { returnisLoginSuccessfulLD; } publicMutableLiveData< Boolean> getIsMyAccountLD { returnisMyAccountLD; } publicMutableLiveData< Boolean> getGoThumbUpLD { returngoThumbUp; } ... publicvoidlogin( StringuserName, Stringpassword) { if(userName.equals( "123456") && password.equals( "123456")) { user.setUserName(userName); user.setPassword(password); setIsLoginSuccessfulLD( true); } else{ setIsLoginSuccessfulLD( false); } } publicvoidisMyAccount( @NonNullStringuserName) { try{ Thread.sleep( 1000); } catch(Exception ex) { } if(userName.equals( "123456")) { setIsMyAccountSuccessfulLD( true); } else{ setIsMyAccountSuccessfulLD( false); } } publicvoidgoThumbUp( booleanisMyAccount) { setGoThumbUpLD(isMyAccount); } publicStringgetUserName { returnuser.getUserName; } }

(2)再来看看你可能使用的一种处理逻辑,在判断登录成功之后,使用变量isLoginSuccessFul再去做 loginVM.isMyAccount(userNameEt.getText.toString);在账号验证成功之后,再去通过变量isMyAccount去做loginVM.goThumbUp(true);

MVVM架构实现登录流程-model

publicclassMvvmFaultLoginActivityextendsAppCompatActivity{ privateLoginMultiViewModel loginVM; privateEditText userNameEt; privateEditText passwordEt; @Override protectedvoid 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 publicvoid 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); } privateObserver< Boolean> loginObserver = new Observer< Boolean> { @Override publicvoid onChanged( @NullableBooleanisLoginSuccessFul) { if(isLoginSuccessFul) { Toast.makeText(MvvmFaultLoginActivity. this, "登录成功,开始校验账号", Toast.LENGTH_SHORT).show; loginVM.isMyAccount(userNameEt.getText.toString); } else{ Toast.makeText(MvvmFaultLoginActivity. this, "登录失败", Toast.LENGTH_SHORT) .show; } } }; privateObserver< Boolean> isMyAccountObserver = new Observer< Boolean> { @Override publicvoid onChanged( @NullableBooleanisMyAccount) { if(isMyAccount) { Toast.makeText(MvvmFaultLoginActivity. this, "校验成功,开始点赞", Toast.LENGTH_SHORT).show; loginVM.goThumbUp( true); } } }; privateObserver< Boolean> goThumbUpObserver = new Observer< Boolean> { @Override publicvoid onChanged( @NullableBooleanisThumbUpSuccess) { 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(一个 Java框架),并且在主流的 JS 框架 Redux 中大行其道,然后就被一些大佬移植到了 Android 上(比如最早期用Java写的 mosby)。

既然MVVM是目前android官方推荐的架构,又为什么要有MVI呢?其实应用架构指南中并没有提出MVI的概念,而是提到了单向数据流,唯一数据源,这也是区别MVVM的特性。

不过还是要说明一点,凡是MVI做到的,只要你使用MVVM去实现,基本上也能做得到。只是说在接下来要讲的内容里面,MVI具备的封装思路,是可以直接使用的,并且是便于单元测试的。

MVI的思想:靠数据驱动页面 (其实当你把这种思想应用在各个框架的时候,你的那个框架都会更加优雅)

MVI架构包括以下几个部分

  1. Model:主要指UI状态(State)。例如页面加载状态、控件位置等都是一种UI状态。
  2. 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

classLoginViewModel: ViewModel{ companionobject{ constvalTAG = "LoginViewModel" } privateval_repository = LoginRepository valloginActionIntent = Channel(Channel.UNLIMITED) privateval_loginActionState = MutableSharedFlow valstate: SharedFlow get= _loginActionState init{ // 可以用来初始化一些页面或者参数 initActionIntent } privatefuninitActionIntent{ viewModelScope.launch { loginActionIntent.consumeAsFlow.collect { when(it) { isLoginActionIntent.DoLogin -> { doLogin(it.username, it.password) } else-> { } } } } } }

(4)使用respository进行网络请求,更新state

MVI架构代码-Repository

classLoginRepository{ suspendfunrequestLoginData(username: String, password: String) : Boolean{ delay( 1000) if(username == "123456"&& password == "123456") { returntrue } returnfalse } suspendfunrequestIsMyAccount(username: String, password: String) : Boolean{ delay( 1000) if(username == "123456") { returntrue } returnfalse } suspendfunrequestThumbUp(username: String, password: String) : Boolean{ delay( 1000) if(username == "123456") { returntrue } returnfalse } }

MVI架构代码-更新state

privatefundoLogin(username: String, password: String) { viewModelScope.launch { if(username.isEmpty || password.isEmpty) { return@launch } // 设置页面正在加载 _loginActionState.emit(LoginActionState.LoginLoading(username, password)) // 开始请求数据 valloginResult = _repository.requestLoginData(username, password) if(!loginResult) { //登录失败 _loginActionState.emit(LoginActionState.LoginFailed(username, password)) return@launch } _loginActionState.emit(LoginActionState.LoginSuccessful(username, password)) //登录成功继续往下 valisMyAccount = _repository.requestIsMyAccount(username, password) if(!isMyAccount) { //校验账号失败 _loginActionState.emit(LoginActionState.IsMyAccountFailed(username, password)) return@launch } _loginActionState.emit(LoginActionState.IsMyAccountSuccessful(username, password)) //校验账号成功继续往下 valisThumbUpSuccess = _repository.requestThumbUp(username, password) if(!isThumbUpSuccess) { //点赞失败 _loginActionState.emit(LoginActionState.GoThumbUpFailed(username, password)) return@launch } //点赞成功继续往下 _loginActionState.emit(LoginActionState.GoThumbUpSuccessful( true)) } }

(5)在View中监听state的变化,做页面刷新

MVI架构代码-Repository

funobserveViewModel{ lifecycleScope.launch { loginViewModel.state.collect { when(it) { isLoginActionState.LoginLoading -> { Toast.makeText(baseContext, "登录中", Toast.LENGTH_SHORT).show } isLoginActionState.LoginFailed -> { Toast.makeText(baseContext, "登录失败", Toast.LENGTH_SHORT).show } isLoginActionState.LoginSuccessful -> { Toast.makeText(baseContext, "登录成功,开始校验账号", Toast.LENGTH_SHORT).show } isLoginActionState.IsMyAccountSuccessful -> { Toast.makeText(baseContext, "校验成功,开始点赞", Toast.LENGTH_SHORT).show } isLoginActionState.GoThumbUpSuccessful -> { resultView.text = "点赞成功" Toast.makeText(baseContext, "点赞成功", Toast.LENGTH_SHORT).show } else-> {} } } } }

通过这个流程,可以看到用户点击登录操作,一直到最后刷新页面,是一个串行的操作。在这种场景下,使用MVI架构,再合适不过

4.2 MVI的优缺点

(1)MVI的优点如下:

  • 可以更好的进行单元测试 针对上面的案例,使用MVI这种单向数据流的形式要比MVVM更加的合适,并且便于单元测试,每个节点都较为独立,没有代码上的耦合。
  • 不需要像MVVM那样管理多个LiveData,可以直接使用一个state进行管理,相比 MVVM 是新的特性。

但MVI 本身也存在一些缺点:

  • State 膨胀: 所有视图变化都转换为 ViewState,还需要管理不同状态下对应的数据。实践中应该根据状态之间的关联程度来决定使用单流还是多流;
  • 内存开销: ViewState 是不可变类,状态变更时需要创建新的对象,存在一定内存开销;
  • 局部刷新: View 根据 ViewState 响应,不易实现局部 Diff 刷新,可以使用 Flow#distinctUntilChanged 来刷新来减少不必要的刷新。

更关键的一点,即使单向数据流封装的很多,仍然避免不了来一个新人,不遵守这个单向数据流的写法,随便去处理view。这时候就要去引用Compose了。

五、不妨利用Compose升级MVI

这一章节是本文的重点。

2021年,谷歌发布Jetpack Compose1.0,2022年,又更新了文章应用架构指南,在进行界面层的搭建时,建议方案如下:

  1. 在屏幕上呈现数据的界面元素。您可以使用 View 或 Jetpack Compose 函数构建这些元素。
  2. 用于存储数据、向界面提供数据以及处理逻辑的状态容器(如 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架构代码

classMviComposeLoginActivity: ComponentActivity{ overridefunonCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launch { setContent { BoxWithConstraints( modifier = Modifier .background(colorResource(id = R.color.white)) .fillMaxSize ) { loginConstraintToDo } } } } @Composable funEditorTextField(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 internalfunloginConstraintToDo(model: ComposeLoginViewModel= viewModel ){ valstate bymodel.uiState.collectAsState valcontext = 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 funloginConstraintLayout(onLoginBtnClick: ( String, String) -> Unit, thumbUpSuccessful: Boolean){ ConstraintLayout { //通过createRefs创建三个引用 // 初始化声明两个元素,如果只声明一个,则可用 createRef 方法 // 这里声明的类似于 View 的 id val(firstText, secondText, button, text) = createRefs valfirstEditor = remember { TextFieldState } valsecondEditor = 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 框架的选择原理

  1. 如果你的页面相对来说比较简单些,比如就是一个网络请求,然后刷新列表,使用MVC就够了。
  2. 如果你有很多页面共用相同的逻辑,比如多个页面都有网络请求加载中、网络请求、网络请求加载完成、网络请求加载失败这种,使用MVP、MVVM、MVI,把接口封装好更好些。
  3. 如果你需要在多处监听数据源的变化,这时候需要使用LiveData或者Flow,也就是MVVM、MVI的架构好些。
  4. 如果你的操作是串行的,比如登录之后进行账号验证、账号验证完再进行点赞,这时候使用MVI更好些。当然,MVI+Compose可以保证你的架构不易被修改。

切勿混合使用架构模式,分析透彻页面结构之后,选择一种架构即可,不然会导致页面越来越复杂,无法维护

上面就是对所有框架模式的总结,大家根据实际情况进行选择。建议还是直接上手最新的MVI+Compose,虽然多了些学习成本,但是毕竟Compose的思想还是很值得借鉴的。

END

红帽回应质疑:授人以渔而非授人以鱼

这里有最新开源资讯、软件更新、技术干货等内容

点这里 ↓↓↓ 记得 关注✔ 标星⭐ 哦

相关内容

热门资讯

2分钟学会“开心泉州麻将1v1... 你好,你所搜查的 微信小程序微乐跑得快 这款游戏是可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌...
3分钟科普,微乐卡五星外卦是不... 你好,你所搜查的 微信小程序微乐跑得快 这款游戏是可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌...
真实了解,中至赣牌圈胜率有挂揭... 你好,你所搜查的 微信小程序微乐跑得快 这款游戏是可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌...
2分钟学会,微乐春天扑克有挂吗... 你好,你所搜查的 微信小程序微乐跑得快 这款游戏是可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌...
总算搞定“天天九州麻将有挂吗开... 你好,你所搜查的 微信小程序微乐跑得快 这款游戏是可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌...
必看教你“福建天天开心有外卦吗... 你好,你所搜查的 微信小程序微乐跑得快 这款游戏是可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌...
简单学会,广东雀神祈福有用吗详... 你好,你所搜查的 微信小程序微乐跑得快 这款游戏是可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌...
3分钟科普“四川游戏家园断勾卡... 你好,你所搜查的 微信小程序微乐跑得快 这款游戏是可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌...
2分钟科普,爱来麻将怎么样可以... 你好,你所搜查的 微信小程序微乐跑得快 这款游戏是可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌...
真实了解!佳友互娱有挂吗有挂是... 你好,你所搜查的 微信小程序微乐跑得快 这款游戏是可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌...
必学教你,心悦踢坑系统如何能给... 你好,你所搜查的 微信小程序微乐跑得快 这款游戏是可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌...
3分钟学会“爱来大菠萝怎么一直... 你好,你所搜查的 微信小程序微乐跑得快 这款游戏是可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌...
真实存在“开心泉州麻将专用神器... 你好,你所搜查的 微信小程序微乐跑得快 这款游戏是可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌...
必看教你!博乐填大坑有规律吗开... 你好,你所搜查的 微信小程序微乐跑得快 这款游戏是可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌...
真实了解!小程序雀神广东麻将的... 你好,你所搜查的 微信小程序微乐跑得快 这款游戏是可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌...
必看教你!微信小程序雀神广东麻... 你好,你所搜查的 微信小程序微乐跑得快 这款游戏是可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌...
必学教你,天天九州麻将怎么一直... 你好,你所搜查的 微信小程序微乐跑得快 这款游戏是可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌...
必学教你!钱塘十三水如何拿好牌... 你好,你所搜查的 微信小程序微乐跑得快 这款游戏是可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌...
4分钟学会“神殿娱乐是骗局吗确... 你好,你所搜查的 微信小程序微乐跑得快 这款游戏是可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌...
5分钟学会!边锋麻将怎么老是输... 你好,你所搜查的 微信小程序微乐跑得快 这款游戏是可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌...