How to choose Android architecture mode

Author: Vivo Internet Client Team-Xu Jie


The Android architectural model has evolved rapidly, and currently there are MVC, MVP, MVVM, and MVI. It is impossible to choose which one is most needed in your business scenario without a deep understanding. This article will explain these architectural patterns one by one. The focus will be on why Compose should be used in conjunction with MVI. I hope to know what it is, and then find an architectural model that suits my business


I. Introduction


I have to sigh that the evolution speed of android's architecture is really fast in recent years. Taking the architecture that the author has been in contact with in the past few years, there are already MVC, MVP, and MVVM. Just when the author was about to apply MVVM to my project, I found that Google quietly updated the developer documentation (Application Architecture Guide | Android Developers | Android Developers (google.cn)). This is an article that guides how to use MVI. So why is this article updated and what do you want to express? What is Compose mentioned in it? Are the existing MVC, MVP, and MVVM not enough? How is MVI different from these existing architectures?


Some people will say that no matter what the architecture is, it is implemented around " decoupling ". This statement is correct, but the high degree of coupling is just a phenomenon. What means are used to reduce the degree of coupling? Is the program after reducing the degree of coupling convenient for unit testing? If I do decoupling on the basis of MVC, MVP, and MVVM, can I do it thoroughly?


Let me tell you the answer first, MVC, MVP, and MVVM cannot achieve complete decoupling, but MVI+Compose can achieve complete decoupling, which is the key part of this article. This article combines specific codes and cases, simplifies complex issues, and combines many technical blogs to make a unified summary. I believe you will gain a lot after reading it.


Then the meaning of writing this article is to explain MVI+Compose in a simple way. You can first imagine such a business scenario. If it were you, which architecture would you choose to implement?


Business Scenario Considerations

  1. Login with mobile number

  2. After logging in, verify whether the specified account A

  3. If it is account A, perform the like operation


上面三个步骤是顺序执行的,手机号的登录、账号的验证、点赞都是与服务端进行交互之后,获取对应的返回结果,然后再做下一步。


在开始介绍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在大部分简单业务场景下是够用的,主要优点如下:

  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;    @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 同样将代码划分为三个部分:

  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;     @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<Boolean> loginObserver = new Observer<Boolean>() {        @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<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;     @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<Boolean> loginObserver = new Observer<Boolean>() {        @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<Boolean> isMyAccountObserver = new Observer<Boolean>() {        @Override        public 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>() {        @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架构包括以下几个部分

  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() = _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年,又更新了文章应用架构指南,在进行界面层的搭建时,建议方案如下:

  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 }, // 文字改变时,就赋值给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 框架的选择原理


  1. 如果你的页面相对来说比较简单些,比如就是一个网络请求,然后刷新列表,使用MVC就够了。

  2. 如果你有很多页面共用相同的逻辑,比如多个页面都有网络请求加载中、网络请求、网络请求加载完成、网络请求加载失败这种,使用MVP、MVVM、MVI,把接口封装好更好些。

  3. 如果你需要在多处监听数据源的变化,这时候需要使用LiveData或者Flow,也就是MVVM、MVI的架构好些。

  4. 如果你的操作是串行的,比如登录之后进行账号验证、账号验证完再进行点赞,这时候使用MVI更好些。当然,MVI+Compose可以保证你的架构不易被修改。


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


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


END

猜你喜欢

本文分享自微信公众号 - vivo互联网技术(vivoVMIC)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

比 Protocol Buffers 快无限倍,开源十年后 Cap'n Proto 1.0 终发布 华中科技大学博士后复现 LK-99 磁悬浮现象 龙芯中科研制成功新一代处理器龙芯 3A6000 miniblink 108 版本成功编译,全球最小 Chromium 内核 ChromeOS 将浏览器和操作系统拆分独立 特斯拉中国商城上架 1TB 固态硬盘,售价 2720 元 华为正式发布 HarmonyOS 4 火绒安全升级版本,导致所有基于 Electron 的应用卡顿 AWS 明年开始对 IPv4 公网地址收取费用 Nim v2.0 正式发布,命令式编程语言
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/vivotech/blog/10091929