作者: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提出,而是依据服务端或许前端演进而来,这也阐明设计思路上都是大同小异的:

Android 架构模式如何选择

二、架构方式过去式?

2.1MVC现已存在很久了

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四处被调用,相信咱们也深有体会。

Android 架构模式如何选择

不可防止的,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的架构图:

Android 架构模式如何选择

与MVC的最首要差异

  • View与Model并不直接交互,而是经过与Presenter交互来与Model间接交互。而在MVC中View能够与Model直接交互。

  • 一般View与Presenter是1对1的,但杂乱的View或许绑定多个Presenter来处理逻辑。而Controller回归根源,首要责任是加载运用的布局和初始化用户界面,并接受并处理来自用户的操作恳求,它是依据行为的,而且能够被多个View同享,Controller能够担任决议显现哪个View。

  • Presenter与View的交互是经过接口来进行的,更有利于增加单元测验。

(2)MVP代码示意

① 先来看包结构图

Android 架构模式如何选择

②树立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架构图如下所示:

Android 架构模式如何选择

能够看出MVVM与MVP的首要差异在于,你不用去主动去改写UI了,只需Model数据变了,会主动反映到UI上。换句话说,MVVM更像是主动化的MVP。

MVVM的双向数据绑定首要经过DataBinding完结,可是大部分人应该跟我一样,不运用DataBinding,那么咱们终究运用的MVVM架构就变成了下面这样:

Android 架构模式如何选择

总结一下:

实践运用MVVM架构阐明

  • View观察ViewModel的数据改动并自我更新,这其实是单一数据源而不是双向数据绑定,所以MVVM的双向绑定这一大特性我这儿并没有用到

  • View经过调用ViewModel供给的办法来与ViewMdoel交互。

3.2 MVVM代码示例

(1)树立viewModel,而且供给一个可供view调取的办法 login(String userName,Stringpassword)

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.3MVVM优缺陷

经过上面的代码,能够总结出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层进行数据恳求。

看下交互流程图:

Android 架构模式如何选择

对流程图做下解释阐明:

(1)用户操作以Intent的方式通知Model(2)Model依据Intent更新State。这个里边包括运用ViewModel进行网络恳求,更新State的操作(3)View接收到State改动改写UI。

4.2 MVI的代码示例

直接看代码吧

(1)先看下包结构

Android 架构模式如何选择

(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.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 类)。

Android 架构模式如何选择

为什么这儿会说到Compose?

  • 运用Compose的原因之一

即使你运用了MVI架构,可是当有人不恪守这个设计理念时,从代码层面是无法防止他人运用非MVI架构,久而久之,导致你的代码紊乱。

意思便是说,你在运用MVI架构建立页面之后,有个人突然又引入了MVC的架构,是无法防止的。Compose能够完美解决这个问题。

接下来便是本文与其他技能博客不一样的当地,把Compose怎样运用,为什么这样运用做下阐明,不要只看理论,最好实战。

5.1Compose的首要效果

Compose能够做到界面view在一开端的时分就要绑定数据源,然后达到无法在其他当地被篡改的意图。

怎样了解?

当你有个TextView被声明之后,依照之前的架构,能够获取这个TextView,而且给它的text随意赋值,这就导致了TextView就有或许不止是在MVI架构里边运用,也或许在MVC架构里边运用。

5.2MVI+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.3MVI+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的思维仍是很值得学习的。