写点什么

Android 架构模式如何选择

  • 2023-07-31
    广东
  • 本文字数:16915 字

    阅读完需:约 55 分钟

作者: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;     @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.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()            }        }    }
}

@Composablefun 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")@Composableinternal 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() } }
}

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

发布于: 刚刚阅读数: 4
用户头像

官方公众号:vivo互联网技术,ID:vivoVMIC 2020-07-10 加入

分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。

评论

发布
暂无评论
Android 架构模式如何选择_mvc_vivo互联网技术_InfoQ写作社区