当前位置: 首页 > news >正文

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

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方法,当获取到结果后,进行对应的业务操作。

(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

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;@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();}}
}

(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

public class LoginViewModel extends ViewModel {private User user;private MutableLiveData<Boolean> isLoginSuccessfulLD;public LoginViewModel() {this.isLoginSuccessfulLD = new MutableLiveData<>();user = new User();}public MutableLiveData<Boolean> 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 Observer<Boolean> loginObserver = new Observer<Boolean>() {@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 MutableLiveData<Boolean> isLoginSuccessfulLD;// 是否为指定账号private MutableLiveData<Boolean> isMyAccountLD;// 如果是指定账号,进行点赞private MutableLiveData<Boolean> goThumbUp;public LoginMultiViewModel() {this.isLoginSuccessfulLD = new MutableLiveData<>();this.isMyAccountLD = new MutableLiveData<>();this.goThumbUp = new MutableLiveData<>();user = new User();}public MutableLiveData<Boolean> getIsLoginSuccessfulLD() {return isLoginSuccessfulLD;}public MutableLiveData<Boolean> getIsMyAccountLD() {return isMyAccountLD;}public MutableLiveData<Boolean> 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 Observer<Boolean> loginObserver = new Observer<Boolean>() {@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 Observer<Boolean> isMyAccountObserver = new Observer<Boolean>() {@Overridepublic void onChanged(@Nullable Boolean isMyAccount) {if (isMyAccount) {Toast.makeText(MvvmFaultLoginActivity.this, "校验成功,开始点赞", Toast.LENGTH_SHORT).show();loginVM.goThumbUp(true);}}};private Observer<Boolean> goThumbUpObserver = new Observer<Boolean>() {@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架构包括以下几个部分

  1. Model:主要指UI状态(State)。例如页面加载状态、控件位置等都是一种UI状态。

  2. View: 与其他MVX中的View一致,可能是一个Activity或者任意UI承载单元。MVI中的View通过订阅Model的变化实现界面刷新。

  3. 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<LoginActionIntent>(Channel.UNLIMITED)private val _loginActionState = MutableSharedFlow<LoginActionState>()val state: SharedFlow<LoginActionState>get() = _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()) {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.3 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年,又更新了文章应用架构指南,在进行界面层的搭建时,建议方案如下:

  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架构代码

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 水平中间})}
}

关键代码段就在于下面:

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的思想还是很值得借鉴的。

相关文章:

Android 架构模式如何选择

作者&#xff1a;vivo 互联网客户端团队-Xu Jie Android架构模式飞速演进&#xff0c;目前已经有MVC、MVP、MVVM、MVI。到底哪一个才是自己业务场景最需要的&#xff0c;不深入理解的话是无法进行选择的。这篇文章就针对这些架构模式逐一解读。重点会介绍Compose为什么要结合MV…...

深入了解 LoRaWAN® B 类设备

介绍 在 LoRaWAN 网络中,终端设备以三种模式之一运行:LoRaWAN A 类、B 类和 C 类。网络只能将消息(下行链路)发送到终端设备在两个短接收窗口之一期间处于 A 类模式,该接收窗口在设备向网络发送消息(上行链路)后立即打开。然而,这些上行链路不是预先安排的,并且可以由…...

KK集团再闯港交所:引领潮流零售市场,2023年一季度业绩增势显著

撰稿|行星 来源|贝多财经 7月31日&#xff0c;KK Group Company Holdings Limited&#xff08;下称“KK集团”&#xff09;在港交所更新招股书&#xff0c;补充了截至2023年3月31日的财务数据等信息&#xff0c;继续推进上市事宜&#xff0c;摩根士丹利和瑞信为其联席保荐人。…...

Vue中的组件渲染

在Vue中&#xff0c;组件的被渲染意味着将组件的内容转换为真实的DOM元素并添加到页面上。当Vue应用启动时&#xff0c;根组件会被渲染&#xff0c;并递归地渲染其子组件。 当组件被渲染时&#xff0c;Vue会将组件的模板解析成虚拟DOM&#xff08;Virtual DOM&#xff09;的形…...

docker 保存和载入镜像

查看本机docker镜像 docker images保存镜像 docker save -o /home/space/work1/docker_qnx7.1.tar.gz a01ee6d74c36复制镜像到其他服务器 scp /home/space/work1/docker_qnx7.1.tar.gz XXXIP:/home/dell/work1/登录新 服务器操作 docker load -i docker_qnx7.1.tar.gz载入后…...

Java框架(九)--Spring Boot入门(1)

SpringBoot 2.x入门简介 学前基础 Maven Spring MVC理念 开发环境 Spring Boot官网版本介绍 https://spring.io/projects/spring-boot#learn 我们点击 Reference Doc. &#xff0c;再点击Getting Started&#xff0c;就可以看到官网系统环境说明了 官网系统环境说明 Sp…...

2023年第四届“华数杯”数学建模思路 - 案例:随机森林

## 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 什么是随机森林&#xff1f; 随机森林属于 集成学习 中的 Bagging&#xff08;Bootstrap AGgregation 的简称&#xff09; 方法。如果用图来表示他们之…...

Redis中缓存穿透、击穿、雪崩以及解决方案

Redis中缓存穿透、击穿、雪崩以及解决方案 Redis作为一个高效的内存数据库&#xff0c;提供了缓存能力使得我们能够快速访问数据。然而&#xff0c;在使用Redis作为缓存时&#xff0c;我们可能会面临缓存穿透、缓存击穿和缓存雪崩的问题。接下来&#xff0c;我将详细解释这些现…...

系统架构设计师-软件架构设计(6)

目录 一、物联网分层架构 二、大数据分层架构 三、基于服务的架构&#xff08;SOA&#xff09; 1、SOA的特征 2、服务构件与传统构件的区别 四、Web Service&#xff08;WEB服务&#xff09; 1、Web Services 和 SOA的关系 五、REST(表述性状态转移) 六、ESB&#xff08;…...

Knife4j系列--解决不显示文件上传的问题

原文网址&#xff1a;Knife4j系列--解决不显示文件上传的问题_IT利刃出鞘的博客-CSDN博客 简介 本文介绍使用Knife4j时无法上传文件的问题。 问题复现 依赖 <dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-…...

深入学习Mysql引擎InnoDB、MylSAM

目录 一、什么是MySQL 二、什么是InnoDB 三、什么是MyISAM 四、MySQL不同引擎有什么区别 一、什么是MySQL MySQL是一种广泛使用的开源关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;它是由瑞典MySQL AB公司开发并推广&#xff0c;后来被Sun Microsystems收…...

第七章:SpringMVC中

第七章&#xff1a;SpringMVC中 7.1&#xff1a;SpringMVC的视图 ​ SpringMVC中的视图是View接口&#xff0c;视图的作用渲染数据&#xff0c;将模型Model中的数据展示给用户SpringMVC视图的种类很多&#xff0c;默认有转发视图和重定向视图。 ​ 当工程引入jstl的依赖&…...

MySQL数据库——DQL操作——基本查询

文章目录 前言事前准备——测试数据整表查询指定列查找别名查询MySQL运算符条件查询模糊查询排序查询聚合查询分组查询分组之后的条件筛选 分页查询将整张表的数据插入到另一张表中 前言 MySQL数据库常见的操作是增删查改&#xff0c;而其中数据的查询是使用最多&#xff0c;也…...

Electron 开发,报handshake failed; returned -1, SSL error code 1,错误

代码说明 在preload.js代码中&#xff0c;暴露参数给渲染线程renderer.js访问&#xff0c; renderer.js 报&#xff1a;ERROR:ssl_client_socket_impl.cc(978)] failed; returned -1, SSL error code 1,错误 问题原因 如题所说&#xff0c;跨进程传递消息&#xff0c;这意味…...

知识区博主转型——兼做知识区和改造区博主!!!!!

想脱单的进来&#xff0c;一起交流如何能脱单&#xff01;&#xff01;&#xff01; 为什么——我太羡慕有对象的人了哭死&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 你是不是很羡慕别人怎么都有女朋友 别人家的女朋友怎么都那么好&#xff…...

Resnet与Pytorch花图像分类

1、介绍 1.1数据集介绍 flower_data├── train│ └── 1-102&#xff08;102个文件夹&#xff09;│ └── XXX.jpg&#xff08;每个文件夹含若干张图像&#xff09;├── valid│ └── 1-102&#xff08;102个文件夹&#xff09;└── ─── └── XXX.jp…...

【NLP概念源和流】 03-基于计数的嵌入,GloVe(第 3/20 部分)

接续上文 【NLP概念源和流】 02-稠密文档表示(第 2/20 部分)...

【React】关于组件之间的通讯

&#x1f31f;组件化&#xff1a;把一个项目拆成一个一个的组件&#xff0c;为了便与开发与维护 组件之间互相独立且封闭&#xff0c;一般而言&#xff0c;每个组件只能使用自己的数据&#xff08;组件状态私有&#xff09;。 如果组件之间相互传参怎么办&#xff1f; 那么就要…...

item_get-小红薯-商品详情

一、接口参数说明&#xff1a; smallredbook.item_get&#xff0c;点击更多API调试&#xff0c;请移步注册API账号点击获取测试key和secret 公共参数 请求地址: https://api-gw.onebound.cn/smallredbook/item_get 名称类型必须描述keyString是调用key&#xff08;http://o0…...

网络安全进阶学习第十课——MySQL手工注入

文章目录 一、MYSQL数据库常用函数二、MYSQL默认的4个系统数据库以及重点库和表三、判断数据库类型四、联合查询注入1、具体步骤&#xff08;靶场演示&#xff09;&#xff1a;1&#xff09;首先判断注入点2&#xff09;判断是数字型还是字符型3&#xff09;要判断注入点的列数…...

进程地址空间(比特课总结)

一、进程地址空间 1. 环境变量 1 &#xff09;⽤户级环境变量与系统级环境变量 全局属性&#xff1a;环境变量具有全局属性&#xff0c;会被⼦进程继承。例如当bash启动⼦进程时&#xff0c;环 境变量会⾃动传递给⼦进程。 本地变量限制&#xff1a;本地变量只在当前进程(ba…...

STM32标准库-DMA直接存储器存取

文章目录 一、DMA1.1简介1.2存储器映像1.3DMA框图1.4DMA基本结构1.5DMA请求1.6数据宽度与对齐1.7数据转运DMA1.8ADC扫描模式DMA 二、数据转运DMA2.1接线图2.2代码2.3相关API 一、DMA 1.1简介 DMA&#xff08;Direct Memory Access&#xff09;直接存储器存取 DMA可以提供外设…...

vue3 字体颜色设置的多种方式

在Vue 3中设置字体颜色可以通过多种方式实现&#xff0c;这取决于你是想在组件内部直接设置&#xff0c;还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法&#xff1a; 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...

C# SqlSugar:依赖注入与仓储模式实践

C# SqlSugar&#xff1a;依赖注入与仓储模式实践 在 C# 的应用开发中&#xff0c;数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护&#xff0c;许多开发者会选择成熟的 ORM&#xff08;对象关系映射&#xff09;框架&#xff0c;SqlSugar 就是其中备受…...

10-Oracle 23 ai Vector Search 概述和参数

一、Oracle AI Vector Search 概述 企业和个人都在尝试各种AI&#xff0c;使用客户端或是内部自己搭建集成大模型的终端&#xff0c;加速与大型语言模型&#xff08;LLM&#xff09;的结合&#xff0c;同时使用检索增强生成&#xff08;Retrieval Augmented Generation &#…...

在Ubuntu24上采用Wine打开SourceInsight

1. 安装wine sudo apt install wine 2. 安装32位库支持,SourceInsight是32位程序 sudo dpkg --add-architecture i386 sudo apt update sudo apt install wine32:i386 3. 验证安装 wine --version 4. 安装必要的字体和库(解决显示问题) sudo apt install fonts-wqy…...

推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材)

推荐 github 项目:GeminiImageApp(图片生成方向&#xff0c;可以做一定的素材) 这个项目能干嘛? 使用 gemini 2.0 的 api 和 google 其他的 api 来做衍生处理 简化和优化了文生图和图生图的行为(我的最主要) 并且有一些目标检测和切割(我用不到) 视频和 imagefx 因为没 a…...

苹果AI眼镜:从“工具”到“社交姿态”的范式革命——重新定义AI交互入口的未来机会

在2025年的AI硬件浪潮中,苹果AI眼镜(Apple Glasses)正在引发一场关于“人机交互形态”的深度思考。它并非简单地替代AirPods或Apple Watch,而是开辟了一个全新的、日常可接受的AI入口。其核心价值不在于功能的堆叠,而在于如何通过形态设计打破社交壁垒,成为用户“全天佩戴…...

uniapp 小程序 学习(一)

利用Hbuilder 创建项目 运行到内置浏览器看效果 下载微信小程序 安装到Hbuilder 下载地址 &#xff1a;开发者工具默认安装 设置服务端口号 在Hbuilder中设置微信小程序 配置 找到运行设置&#xff0c;将微信开发者工具放入到Hbuilder中&#xff0c; 打开后出现 如下 bug 解…...

nnUNet V2修改网络——暴力替换网络为UNet++

更换前,要用nnUNet V2跑通所用数据集,证明nnUNet V2、数据集、运行环境等没有问题 阅读nnU-Net V2 的 U-Net结构,初步了解要修改的网络,知己知彼,修改起来才能游刃有余。 U-Net存在两个局限,一是网络的最佳深度因应用场景而异,这取决于任务的难度和可用于训练的标注数…...