Android架構(gòu)模式飛速演進(jìn),目前已經(jīng)有MVC、MVP、MVVM、MVI。到底哪一個(gè)才是自己業(yè)務(wù)場(chǎng)景最需要的,不深入理解的話是無(wú)法進(jìn)行選擇的。這篇文章就針對(duì)這些架構(gòu)模式逐一解讀。重點(diǎn)會(huì)介紹Compose為什么要結(jié)合MVI進(jìn)行使用。希望知其然,然后找到適合自己業(yè)務(wù)的架構(gòu)模式
一、前言
不得不感嘆,近些年android的架構(gòu)演進(jìn)速度真的是飛快,拿筆者工作這幾年接觸的架構(gòu)來(lái)說(shuō),就已經(jīng)有了MVC、MVP、MVVM。正當(dāng)筆者準(zhǔn)備把MVVM應(yīng)用到自己項(xiàng)目當(dāng)中時(shí),發(fā)現(xiàn)谷歌悄悄的更新了開(kāi)發(fā)者文檔(應(yīng)用架構(gòu)指南 | Android 開(kāi)發(fā)者 | Android Developers (google.cn))。這是一篇指導(dǎo)如何使用MVI的文章。那么這個(gè)文章到底為什么更新,想要表達(dá)什么?里面提到的Compose又是什么?難道現(xiàn)在已經(jīng)有的MVC、MVP、MVVM不夠用嗎?MVI跟已有的這些架構(gòu)又有什么不同之處呢?
有人會(huì)說(shuō),不管什么架構(gòu),都是圍繞著“解耦”來(lái)實(shí)現(xiàn)的,這種說(shuō)法是正確的,但是耦合度高只是現(xiàn)象,采用什么手段降低耦合度?降低耦合度之后的程序方便單元測(cè)試嗎?如果我在MVC、MVP、MVVM的基礎(chǔ)上做解耦,可以做的很徹底嗎?
先告訴你答案, MVC、MVP、MVVM無(wú)法做到徹底的解耦,但是MVI+Compose可以做到徹底的解耦,也就是本文的重點(diǎn)講解部分。本文結(jié)合具體的代碼和案例,復(fù)雜問(wèn)題簡(jiǎn)單化,并且結(jié)合較多技術(shù)博客做了統(tǒng)一的總結(jié),相信你讀完會(huì)收獲頗豐。
那么本篇文章編寫的意義,就是為了能夠深入淺出的講解MVI+Compose,大家可以先試想下這樣的業(yè)務(wù)場(chǎng)景,如果是你,你會(huì)選擇哪種架構(gòu)實(shí)現(xiàn)?
業(yè)務(wù)場(chǎng)景考慮
使用手機(jī)號(hào)進(jìn)行登錄
登錄完之后驗(yàn)證是否指定的賬號(hào)A
如果是賬號(hào)A,則進(jìn)行點(diǎn)贊操作
上面三個(gè)步驟是順序執(zhí)行的,手機(jī)號(hào)的登錄、賬號(hào)的驗(yàn)證、點(diǎn)贊都是與服務(wù)端進(jìn)行交互之后,獲取對(duì)應(yīng)的返回結(jié)果,然后再做下一步。
在開(kāi)始介紹MVI+Compose之前,需要循序漸進(jìn),了解每個(gè)架構(gòu)模式的缺點(diǎn),才知道為什么Google提出MVI+Compose。
正式開(kāi)始前,按照架構(gòu)模式的提出時(shí)間來(lái)看下是如何演變的,每個(gè)模式的提出往往不是基于android提出,而是基于服務(wù)端或者前端演進(jìn)而來(lái),這也說(shuō)明設(shè)計(jì)思路上都是大同小異的:

二、架構(gòu)模式過(guò)去式?
2.1MVC已經(jīng)存在很久了
MVC模式提出時(shí)間太久了,早在1978年就被提出,所以一定不是用于android,android的MVC架構(gòu)主要還是源于服務(wù)端的SpringMVC,在2007年到2017年之間,MVC占據(jù)著主導(dǎo)地位,目前我們android中看到的MVC架構(gòu)模式是這樣的。
MVC架構(gòu)這幾個(gè)部分的含義如下,網(wǎng)上隨便找找就有一堆說(shuō)明。
MVC架構(gòu)分為以下幾個(gè)部分
【模型層Model】:主要負(fù)責(zé)網(wǎng)絡(luò)請(qǐng)求,數(shù)據(jù)庫(kù)處理,I/O的操作,即頁(yè)面的數(shù)據(jù)來(lái)源
【視圖層View】:對(duì)應(yīng)于xml布局文件和java代碼動(dòng)態(tài)view部分
【控制層Controller】:主要負(fù)責(zé)業(yè)務(wù)邏輯,在android中由Activity承擔(dān)
(1)MVC代碼示例
我們舉個(gè)登錄驗(yàn)證的例子來(lái)看下MVC架構(gòu)一般怎么實(shí)現(xiàn)。
這個(gè)是controller
MVC架構(gòu)實(shí)現(xiàn)登錄流程-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) { // 這里免不了的,會(huì)有業(yè)務(wù)處理 //1、保存用戶賬號(hào) //2、loading消失 //3、大量的變量判斷 //4、再做進(jìn)一步的其他網(wǎng)絡(luò)請(qǐng)求 Toast.makeText(MvcLoginActivity.this, " Login Successful", Toast.LENGTH_SHORT) .show(); } else { Toast.makeText(MvcLoginActivity.this, "Login Failed", Toast.LENGTH_SHORT) .show(); } } }); } }); } }
這個(gè)是model
MVC架構(gòu)實(shí)現(xiàn)登錄流程-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);
}
}
}
例子很簡(jiǎn)單,主要做了下面這些事情
寫一個(gè)專門的工具類LoginService,用來(lái)做網(wǎng)絡(luò)請(qǐng)求doLogin,驗(yàn)證登錄賬號(hào)是否正確,然后把驗(yàn)證結(jié)果返回。
activity調(diào)用LoginService,并且把賬號(hào)信息傳遞給doLogin方法,當(dāng)獲取到結(jié)果后,進(jìn)行對(duì)應(yīng)的業(yè)務(wù)操作。
(2)MVC優(yōu)缺點(diǎn)
MVC在大部分簡(jiǎn)單業(yè)務(wù)場(chǎng)景下是夠用的,主要優(yōu)點(diǎn)如下:
結(jié)構(gòu)清晰,職責(zé)劃分清晰
降低耦合
有利于組件重用
但是隨著時(shí)間的推移,你的MVC架構(gòu)可能慢慢的演化成了下面的模式。拿上面的例子來(lái)說(shuō),你只做登錄比較簡(jiǎn)單,但是當(dāng)你的頁(yè)面把登錄賬號(hào)校驗(yàn)、點(diǎn)贊都實(shí)現(xiàn)的時(shí)候,方法會(huì)比較多,共享一個(gè)view的時(shí)候,或者共同操作一個(gè)數(shù)據(jù)源的時(shí)候,就會(huì)出現(xiàn)變量滿天飛,view四處被調(diào)用,相信大家也深有體會(huì)。

不可避免的,MVC就存在了下面的問(wèn)題
歸根究底,在android里面使用MVC的時(shí)候,對(duì)于Model、View、Controller的劃分范圍,總是那么的不明確,因?yàn)楸旧硭麄冎g就有無(wú)法直接分割的依賴關(guān)系。所以總是避免不了這樣的問(wèn)題:
View與Model之間還存在依賴關(guān)系,甚至有時(shí)候?yàn)榱藞D方便,把Model和View互傳,搞得View和Model耦合度極高,低耦合是面向?qū)ο笤O(shè)計(jì)標(biāo)準(zhǔn)之一,對(duì)于大型項(xiàng)目來(lái)說(shuō),高耦合會(huì)很痛苦,這在開(kāi)發(fā)、測(cè)試,維護(hù)方面都需要花大量的精力。
那么在Controller層,Activity有時(shí)既要管理View,又要控制與用戶的交互,充當(dāng)Controller,可想而知,當(dāng)稍微有不規(guī)范的寫法,這個(gè)Activity就會(huì)很復(fù)雜,承擔(dān)的功能會(huì)越來(lái)越多。
花了一定篇幅介紹MVC,是讓大家對(duì)MVC中Model、View、Controller應(yīng)該各自完成什么事情能深入理解,這樣才有后面架構(gòu)不斷演進(jìn)的意義。
2.2 MVP架構(gòu)的由來(lái)
(1)MVP要解決什么問(wèn)題?
2016年10月, Google官方提供了MVP架構(gòu)的Sample代碼來(lái)展示這種模式的用法,成為最流行的架構(gòu)。
相對(duì)于MVC,MVP將Activity復(fù)雜的邏輯處理移至另外的一個(gè)類(Presenter)中,此時(shí)Activity就是MVP模式中的View,它負(fù)責(zé)UI元素的初始化,建立UI元素與Presenter的關(guān)聯(lián)(Listener之類),同時(shí)自己也會(huì)處理一些簡(jiǎn)單的邏輯(復(fù)雜的邏輯交由 Presenter處理)。
那么MVP 同樣將代碼劃分為三個(gè)部分:
結(jié)構(gòu)說(shuō)明
View:對(duì)應(yīng)于Activity與XML,只負(fù)責(zé)顯示UI,只與Presenter層交互,與Model層沒(méi)有耦合;
Model: 負(fù)責(zé)管理業(yè)務(wù)數(shù)據(jù)邏輯,如網(wǎng)絡(luò)請(qǐng)求、數(shù)據(jù)庫(kù)處理;
Presenter:負(fù)責(zé)處理大量的邏輯操作,避免Activity的臃腫。
來(lái)看看MVP的架構(gòu)圖:

與MVC的最主要區(qū)別
View與Model并不直接交互,而是通過(guò)與Presenter交互來(lái)與Model間接交互。而在MVC中View可以與Model直接交互。
通常View與Presenter是一對(duì)一的,但復(fù)雜的View可能綁定多個(gè)Presenter來(lái)處理邏輯。而Controller回歸本源,首要職責(zé)是加載應(yīng)用的布局和初始化用戶界面,并接受并處理來(lái)自用戶的操作請(qǐng)求,它是基于行為的,并且可以被多個(gè)View共享,Controller可以負(fù)責(zé)決定顯示哪個(gè)View。
Presenter與View的交互是通過(guò)接口來(lái)進(jìn)行的,更有利于添加單元測(cè)試。
(2)MVP代碼示意
① 先來(lái)看包結(jié)構(gòu)圖

②建立Bean
MVP架構(gòu)實(shí)現(xiàn)登錄流程-model
public class User {
private String userName;
private String password;
public String getUserName() {
return ...
}
public void setUserName(String userName) {
...;
}
}
③建立Model接口 (處理業(yè)務(wù)邏輯,這里指數(shù)據(jù)讀寫),先寫接口方法,后寫實(shí)現(xiàn)
MVP架構(gòu)實(shí)現(xiàn)登錄流程-model
public interface IUserBiz {
boolean login(String userName, String password);
}
④建立presenter(主導(dǎo)器,通過(guò)iView和iModel接口操作model和view),activity可以把所有邏輯給presenter處理,這樣java邏輯就從activity中分離出來(lái)。
MVP架構(gòu)實(shí)現(xiàn)登錄流程-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狀態(tài),這里列出需要操作當(dāng)前view的方法,也是接口IMvpLoginView
MVP架構(gòu)實(shí)現(xiàn)登錄流程-model
public interface IMvpLoginView {
String getUserName();
String getPassword();
void onLoginResult(Boolean isLoginSuccess);
}
⑥ activity中實(shí)現(xiàn)IMvpLoginView接口,在其中操作view,實(shí)例化一個(gè)presenter變量。
MVP架構(gòu)實(shí)現(xiàn)登錄流程-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優(yōu)缺點(diǎn)
因此,Activity及從MVC中的Controller中解放出來(lái)了,這會(huì)Activity主要做顯示View的作用和用戶交互。每個(gè)Activity可以根據(jù)自己顯示View的不同實(shí)現(xiàn)View視圖接口IUserView。
通過(guò)對(duì)比同一實(shí)例的MVC與MVP的代碼,可以證實(shí)MVP模式的一些優(yōu)點(diǎn):
在MVP中,Activity的代碼不臃腫;
在MVP中,Model(IUserModel的實(shí)現(xiàn)類)的改動(dòng)不會(huì)影響Activity(View),兩者也互不干涉,而在MVC中會(huì);
在MVP中,IUserView這個(gè)接口可以實(shí)現(xiàn)方便地對(duì)Presenter的測(cè)試;
在MVP中,UserPresenter可以用于多個(gè)視圖,但是在MVC中的Activity就不行。
但還是存在一些缺點(diǎn):
雙向依賴:View 和 Presenter 是雙向依賴的,一旦 View 層做出改變,相應(yīng)地 Presenter 也需要做出調(diào)整。在業(yè)務(wù)語(yǔ)境下,View 層變化是大概率事件;
內(nèi)存泄漏風(fēng)險(xiǎn):Presenter 持有 View 層的引用,當(dāng)用戶關(guān)閉了 View 層,但 Model 層仍然在進(jìn)行耗時(shí)操作,就會(huì)有內(nèi)存泄漏風(fēng)險(xiǎn)。雖然有解決辦法,但還是存在風(fēng)險(xiǎn)點(diǎn)和復(fù)雜度(弱引用 / onDestroy() 回收 Presenter)。
三、MVVM其實(shí)夠用了
3.1MVVM思想存在很久了
MVVM最初是在2005年由微軟提出的一個(gè)UI架構(gòu)概念。后來(lái)在2015年的時(shí)候,開(kāi)始應(yīng)用于android中。
MVVM 模式改動(dòng)在于中間的 Presenter 改為 ViewModel,MVVM 同樣將代碼劃分為三個(gè)部分:
View:Activity 和 Layout XML 文件,與 MVP 中 View 的概念相同;
Model:負(fù)責(zé)管理業(yè)務(wù)數(shù)據(jù)邏輯,如網(wǎng)絡(luò)請(qǐng)求、數(shù)據(jù)庫(kù)處理,與 MVP 中 Model 的概念相同;
ViewModel:存儲(chǔ)視圖狀態(tài),負(fù)責(zé)處理表現(xiàn)邏輯,并將數(shù)據(jù)設(shè)置給可觀察數(shù)據(jù)容器。
與MVP唯一的區(qū)別是,它采用雙向數(shù)據(jù)綁定(data-binding):View的變動(dòng),自動(dòng)反映在 ViewModel,反之亦然。
MVVM架構(gòu)圖如下所示:

可以看出MVVM與MVP的主要區(qū)別在于,你不用去主動(dòng)去刷新UI了,只要Model數(shù)據(jù)變了,會(huì)自動(dòng)反映到UI上。換句話說(shuō),MVVM更像是自動(dòng)化的MVP。
MVVM的雙向數(shù)據(jù)綁定主要通過(guò)DataBinding實(shí)現(xiàn),但是大部分人應(yīng)該跟我一樣,不使用DataBinding,那么大家最終使用的MVVM架構(gòu)就變成了下面這樣:

總結(jié)一下:
實(shí)際使用MVVM架構(gòu)說(shuō)明
View觀察ViewModel的數(shù)據(jù)變化并自我更新,這其實(shí)是單一數(shù)據(jù)源而不是雙向數(shù)據(jù)綁定,所以MVVM的雙向綁定這一大特性我這里并沒(méi)有用到
View通過(guò)調(diào)用ViewModel提供的方法來(lái)與ViewMdoel交互。
3.2 MVVM代碼示例
(1)建立viewModel,并且提供一個(gè)可供view調(diào)取的方法 login(String userName,String
password)
MVVM架構(gòu)實(shí)現(xiàn)登錄流程-model
public class LoginViewModel extends ViewModel {
private User user;
private MutableLiveData isLoginSuccessfulLD;
public LoginViewModel() {
this.isLoginSuccessfulLD = new MutableLiveData<>();
user = new User();
}
public MutableLiveData 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,并建立觀察。點(diǎn)擊按鈕,觸發(fā) login(String userName, String password)。持續(xù)作用的觀察者loginObserver。只要LoginViewModel 中的isLoginSuccessfulLD變化,就會(huì)對(duì)應(yīng)的有響應(yīng)
MVVM架構(gòu)實(shí)現(xiàn)登錄流程-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 loginObserver = new Observer() {
@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優(yōu)缺點(diǎn)
通過(guò)上面的代碼,可以總結(jié)出MVVM的優(yōu)點(diǎn):
在實(shí)現(xiàn)細(xì)節(jié)上,View 和 Presenter 從雙向依賴變成 View 可以向 ViewModel 發(fā)指令,但 ViewModel 不會(huì)直接向 View 回調(diào),而是讓 View 通過(guò)觀察者的模式去監(jiān)聽(tīng)數(shù)據(jù)的變化,有效規(guī)避了 MVP 雙向依賴的缺點(diǎn)。
但 MVVM 在某些情況下,也存在一些缺點(diǎn):
(1)關(guān)聯(lián)性比較強(qiáng)的流程,liveData太多,并且理解成本較高
當(dāng)業(yè)務(wù)比較復(fù)雜的時(shí)候,在viewModel中必然存在著比較多的LiveData去管理。當(dāng)然,如果你去管理好這些LiveData,讓他們?nèi)ヌ幚順I(yè)務(wù)流程,問(wèn)題也不大,只不過(guò)理解的成本會(huì)高些。
(2)不便于單元測(cè)試
viewModel里面一般都是對(duì)數(shù)據(jù)庫(kù)和網(wǎng)絡(luò)數(shù)據(jù)進(jìn)行處理,包含了業(yè)務(wù)邏輯在里面,當(dāng)要去對(duì)某一流程進(jìn)行測(cè)試時(shí),并沒(méi)有辦法完全剝離數(shù)據(jù)邏輯的處理流程,單元測(cè)試也就增加了難度。
那么我們來(lái)看看缺點(diǎn)對(duì)應(yīng)的具體場(chǎng)景是什么,便于我們后續(xù)進(jìn)一步探討MVI架構(gòu)。
(1)在上面登錄之后,需要驗(yàn)證賬號(hào)信息,然后再自動(dòng)進(jìn)行點(diǎn)贊。那么,viewModel里面對(duì)應(yīng)的增加幾個(gè)方法,每個(gè)方法對(duì)應(yīng)一個(gè)LiveData
MVVM架構(gòu)實(shí)現(xiàn)登錄流程-model
public class LoginMultiViewModel extends ViewModel {
private User user;
// 是否登錄成功
private MutableLiveData isLoginSuccessfulLD;
// 是否為指定賬號(hào)
private MutableLiveData isMyAccountLD;
// 如果是指定賬號(hào),進(jìn)行點(diǎn)贊
private MutableLiveData goThumbUp;
public LoginMultiViewModel() {
this.isLoginSuccessfulLD = new MutableLiveData<>();
this.isMyAccountLD = new MutableLiveData<>();
this.goThumbUp = new MutableLiveData<>();
user = new User();
}
public MutableLiveData getIsLoginSuccessfulLD() {
return isLoginSuccessfulLD;
}
public MutableLiveData getIsMyAccountLD() {
return isMyAccountLD;
}
public MutableLiveData 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)再來(lái)看看你可能使用的一種處理邏輯,在判斷登錄成功之后,使用變量isLoginSuccessFul再去做 loginVM.isMyAccount(userNameEt.getText().toString());在賬號(hào)驗(yàn)證成功之后,再去通過(guò)變量isMyAccount去做loginVM.goThumbUp(true);
MVVM架構(gòu)實(shí)現(xiàn)登錄流程-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 loginObserver = new Observer() {
@Override
public void onChanged(@Nullable Boolean isLoginSuccessFul) {
if (isLoginSuccessFul) {
Toast.makeText(MvvmFaultLoginActivity.this, "登錄成功,開(kāi)始校驗(yàn)賬號(hào)", Toast.LENGTH_SHORT).show();
loginVM.isMyAccount(userNameEt.getText().toString());
} else {
Toast.makeText(MvvmFaultLoginActivity.this,
"登錄失敗",
Toast.LENGTH_SHORT)
.show();
}
}
};
private Observer isMyAccountObserver = new Observer() {
@Override
public void onChanged(@Nullable Boolean isMyAccount) {
if (isMyAccount) {
Toast.makeText(MvvmFaultLoginActivity.this, "校驗(yàn)成功,開(kāi)始點(diǎn)贊", Toast.LENGTH_SHORT).show();
loginVM.goThumbUp(true);
}
}
};
private Observer goThumbUpObserver = new Observer() {
@Override
public void onChanged(@Nullable Boolean isThumbUpSuccess) {
if (isThumbUpSuccess) {
Toast.makeText(MvvmFaultLoginActivity.this,
"點(diǎn)贊成功",
Toast.LENGTH_SHORT)
.show();
} else {
Toast.makeText(MvvmFaultLoginActivity.this,
"點(diǎn)贊失敗",
Toast.LENGTH_SHORT)
.show();
}
}
};
}
毫無(wú)疑問(wèn),這種交互在實(shí)際開(kāi)發(fā)中是可能存在的,頁(yè)面比較復(fù)雜的時(shí)候,這種變量也就滋生了。這種場(chǎng)景,就有必要聊聊MVI架構(gòu)了。
四、MVI有存在的必要性嗎?
4.1 MVI的由來(lái)
MVI 模式來(lái)源于2014年的 Cycle.js(一個(gè) JavaScript框架),并且在主流的 JS 框架 Redux 中大行其道,然后就被一些大佬移植到了 Android 上(比如最早期用Java寫的 mosby)。
既然MVVM是目前android官方推薦的架構(gòu),又為什么要有MVI呢?其實(shí)應(yīng)用架構(gòu)指南中并沒(méi)有提出MVI的概念,而是提到了單向數(shù)據(jù)流,唯一數(shù)據(jù)源,這也是區(qū)別MVVM的特性。
不過(guò)還是要說(shuō)明一點(diǎn),凡是MVI做到的,只要你使用MVVM去實(shí)現(xiàn),基本上也能做得到。只是說(shuō)在接下來(lái)要講的內(nèi)容里面,MVI具備的封裝思路,是可以直接使用的,并且是便于單元測(cè)試的。
MVI的思想:靠數(shù)據(jù)驅(qū)動(dòng)頁(yè)面 (其實(shí)當(dāng)你把這種思想應(yīng)用在各個(gè)框架的時(shí)候,你的那個(gè)框架都會(huì)更加優(yōu)雅)
MVI架構(gòu)包括以下幾個(gè)部分
Model:主要指UI狀態(tài)(State)。例如頁(yè)面加載狀態(tài)、控件位置等都是一種UI狀態(tài)。
View: 與其他MVX中的View一致,可能是一個(gè)Activity或者任意UI承載單元。MVI中的View通過(guò)訂閱Model的變化實(shí)現(xiàn)界面刷新。
Intent: 此Intent不是Activity的Intent,用戶的任何操作都被包裝成Intent后發(fā)送給Model層進(jìn)行數(shù)據(jù)請(qǐng)求。
看下交互流程圖:

對(duì)流程圖做下解釋說(shuō)明:
(1)用戶操作以Intent的形式通知Model
(2)Model基于Intent更新State。這個(gè)里面包括使用ViewModel進(jìn)行網(wǎng)絡(luò)請(qǐng)求,更新State的操作
(3)View接收到State變化刷新UI。
4.2 MVI的代碼示例
直接看代碼吧
(1)先看下包結(jié)構(gòu)

(2)用戶點(diǎn)擊按鈕,發(fā)起登錄流程
loginViewModel.loginActionIntent.send(LoginActionIntent.DoLogin(userNameEt.text.toString(), passwordEt.text.toString()))。
此處是發(fā)送了一個(gè)Intent出去
MVI架構(gòu)代碼-View
loginBtn.setOnClickListener {
lifecycleScope.launch {
loginViewModel.loginActionIntent.send(LoginActionIntent.DoLogin(userNameEt.text.toString(), passwordEt.text.toString()))
}
}
(3)ViewModel對(duì)Intent進(jìn)行監(jiān)聽(tīng)
initActionIntent()。在這里可以把按鈕點(diǎn)擊事件的Intent消費(fèi)掉
MVI架構(gòu)代碼-Model
class LoginViewModel : ViewModel() {
companion object {
const val TAG = "LoginViewModel"
}
private val _repository = LoginRepository()
val loginActionIntent = Channel(Channel.UNLIMITED)
private val _loginActionState = MutableSharedFlow()
val state: SharedFlow
get() = _loginActionState
init {
// 可以用來(lái)初始化一些頁(yè)面或者參數(shù)
initActionIntent()
}
private fun initActionIntent() {
viewModelScope.launch {
loginActionIntent.consumeAsFlow().collect {
when (it) {
is LoginActionIntent.DoLogin -> {
doLogin(it.username, it.password)
}
else -> {
}
}
}
}
}
}
(4)使用respository進(jìn)行網(wǎng)絡(luò)請(qǐng)求,更新state
MVI架構(gòu)代碼-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架構(gòu)代碼-更新state
private fun doLogin(username: String, password: String) {
viewModelScope.launch {
if (username.isEmpty() || password.isEmpty()) {
return@launch
}
// 設(shè)置頁(yè)面正在加載
_loginActionState.emit(LoginActionState.LoginLoading(username, password))
// 開(kāi)始請(qǐng)求數(shù)據(jù)
val loginResult = _repository.requestLoginData(username, password)
if (!loginResult) {
//登錄失敗
_loginActionState.emit(LoginActionState.LoginFailed(username, password))
return@launch
}
_loginActionState.emit(LoginActionState.LoginSuccessful(username, password))
//登錄成功繼續(xù)往下
val isMyAccount = _repository.requestIsMyAccount(username, password)
if (!isMyAccount) {
//校驗(yàn)賬號(hào)失敗
_loginActionState.emit(LoginActionState.IsMyAccountFailed(username, password))
return@launch
}
_loginActionState.emit(LoginActionState.IsMyAccountSuccessful(username, password))
//校驗(yàn)賬號(hào)成功繼續(xù)往下
val isThumbUpSuccess = _repository.requestThumbUp(username, password)
if (!isThumbUpSuccess) {
//點(diǎn)贊失敗
_loginActionState.emit(LoginActionState.GoThumbUpFailed(username, password))
return@launch
}
//點(diǎn)贊成功繼續(xù)往下
_loginActionState.emit(LoginActionState.GoThumbUpSuccessful(true))
}
}
(5)在View中監(jiān)聽(tīng)state的變化,做頁(yè)面刷新
MVI架構(gòu)代碼-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, "登錄成功,開(kāi)始校驗(yàn)賬號(hào)", Toast.LENGTH_SHORT).show()
}
is LoginActionState.IsMyAccountSuccessful -> {
Toast.makeText(baseContext, "校驗(yàn)成功,開(kāi)始點(diǎn)贊", Toast.LENGTH_SHORT).show()
}
is LoginActionState.GoThumbUpSuccessful -> {
resultView.text = "點(diǎn)贊成功"
Toast.makeText(baseContext, "點(diǎn)贊成功", Toast.LENGTH_SHORT).show()
}
else -> {}
}
}
}
}
通過(guò)這個(gè)流程,可以看到用戶點(diǎn)擊登錄操作,一直到最后刷新頁(yè)面,是一個(gè)串行的操作。在這種場(chǎng)景下,使用MVI架構(gòu),再合適不過(guò)
4.2 MVI的優(yōu)缺點(diǎn)
(1)MVI的優(yōu)點(diǎn)如下:
可以更好的進(jìn)行單元測(cè)試
針對(duì)上面的案例,使用MVI這種單向數(shù)據(jù)流的形式要比MVVM更加的合適,并且便于單元測(cè)試,每個(gè)節(jié)點(diǎn)都較為獨(dú)立,沒(méi)有代碼上的耦合。
訂閱一個(gè) ViewState 就可以獲取所有狀態(tài)和數(shù)據(jù)
不需要像MVVM那樣管理多個(gè)LiveData,可以直接使用一個(gè)state進(jìn)行管理,相比 MVVM 是新的特性。
但MVI 本身也存在一些缺點(diǎn):
State 膨脹: 所有視圖變化都轉(zhuǎn)換為 ViewState,還需要管理不同狀態(tài)下對(duì)應(yīng)的數(shù)據(jù)。實(shí)踐中應(yīng)該根據(jù)狀態(tài)之間的關(guān)聯(lián)程度來(lái)決定使用單流還是多流;
內(nèi)存開(kāi)銷: ViewState 是不可變類,狀態(tài)變更時(shí)需要?jiǎng)?chuàng)建新的對(duì)象,存在一定內(nèi)存開(kāi)銷;
局部刷新: View 根據(jù) ViewState 響應(yīng),不易實(shí)現(xiàn)局部 Diff 刷新,可以使用 Flow#distinctUntilChanged() 來(lái)刷新來(lái)減少不必要的刷新。
更關(guān)鍵的一點(diǎn),即使單向數(shù)據(jù)流封裝的很多,仍然避免不了來(lái)一個(gè)新人,不遵守這個(gè)單向數(shù)據(jù)流的寫法,隨便去處理view。這時(shí)候就要去引用Compose了。
五、不妨利用Compose升級(jí)MVI
2021年,谷歌發(fā)布Jetpack Compose1.0,2022年,又更新了文章應(yīng)用架構(gòu)指南,在進(jìn)行界面層的搭建時(shí),建議方案如下:
在屏幕上呈現(xiàn)數(shù)據(jù)的界面元素。您可以使用 View 或 Jetpack Compose 函數(shù)構(gòu)建這些元素。
用于存儲(chǔ)數(shù)據(jù)、向界面提供數(shù)據(jù)以及處理邏輯的狀態(tài)容器(如 ViewModel 類)。
為什么這里會(huì)提到Compose?
使用Compose的原因之一
即使你使用了MVI架構(gòu),但是當(dāng)有人不遵守這個(gè)設(shè)計(jì)理念時(shí),從代碼層面是無(wú)法避免別人使用非MVI架構(gòu),久而久之,導(dǎo)致你的代碼混亂。
意思就是說(shuō),你在使用MVI架構(gòu)搭建頁(yè)面之后,有個(gè)人突然又引入了MVC的架構(gòu),是無(wú)法避免的。Compose可以完美解決這個(gè)問(wèn)題。
接下來(lái)就是本文與其他技術(shù)博客不一樣的地方,把Compose如何使用,為什么這樣使用做下說(shuō)明,不要只看理論,最好實(shí)戰(zhàn)。
5.1Compose的主要作用
Compose可以做到界面view在一開(kāi)始的時(shí)候就要綁定數(shù)據(jù)源,從而達(dá)到無(wú)法在其他地方被篡改的目的。
怎么理解?
當(dāng)你有個(gè)TextView被聲明之后,按照之前的架構(gòu),可以獲取這個(gè)TextView,并且給它的text隨意賦值,這就導(dǎo)致了TextView就有可能不止是在MVI架構(gòu)里面使用,也可能在MVC架構(gòu)里面使用。
5.2MVI+Compose的代碼示例
MVI+Compose架構(gòu)代碼
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) {
// 定義一個(gè)可觀測(cè)的text,用來(lái)在TextField中展示
TextField(
value = textFieldState.text, // 顯示文本
onValueChange = { textFieldState.text = it }, // 文字改變時(shí),就賦值給text
modifier = modifier,
label = { Text(text = label) }, // label是Input
placeholder = @Composable { Text(text = "123456") }, // 不輸入內(nèi)容時(shí)的占位符
)
}
@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, "登錄成功,開(kāi)始校驗(yàn)賬號(hào)", Toast.LENGTH_SHORT).show()
model.sendEvent(TodoEvent.VerifyAccount("123456", "123456"))
}
state.isAccountSuccessful -> {
Toast.makeText(baseContext, "賬號(hào)校驗(yàn)成功,開(kāi)始點(diǎn)贊", Toast.LENGTH_SHORT).show()
model.sendEvent(TodoEvent.ThumbUp("123456", "123456"))
}
state.isThumbUpSuccessful -> {
Toast.makeText(baseContext, "點(diǎn)贊成功", Toast.LENGTH_SHORT).show()
}
}
}
@Composable
fun loginConstraintLayout(onLoginBtnClick: (String, String) -> Unit, thumbUpSuccessful: Boolean){
ConstraintLayout() {
//通過(guò)createRefs創(chuàng)建三個(gè)引用
// 初始化聲明兩個(gè)元素,如果只聲明一個(gè),則可用 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 組件與初始化的引用關(guān)聯(lián)起來(lái)
// 關(guān)聯(lián)之后就可以在其他組件中使用并添加約束條件了
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) "點(diǎn)贊成功" else "點(diǎn)贊失敗", Modifier.constrainAs(text) {
top.linkTo(button.bottom, margin = 36.dp)
start.linkTo(button.start)
centerHorizontallyTo(parent) // 擺放在 ConstraintLayout 水平中間
})
}
}
關(guān)鍵代碼段就在于下面:
MVI+Compose架構(gòu)代碼
Text(if (thumbUpSuccessful) "點(diǎn)贊成功" else "點(diǎn)贊失敗", Modifier.constrainAs(text) {
top.linkTo(button.bottom, margin = 36.dp)
start.linkTo(button.start)
centerHorizontallyTo(parent) // 擺放在 ConstraintLayout 水平中間
})
TextView的text在頁(yè)面初始化的時(shí)候就跟數(shù)據(jù)源中的thumbUpSuccessful變量進(jìn)行了綁定,并且這個(gè)TextView不可以在其他地方二次賦值,只能通過(guò)這個(gè)變量thumbUpSuccessful進(jìn)行修改數(shù)值。當(dāng)然,使用這個(gè)方法,也解決了數(shù)據(jù)更新是無(wú)法diff更新的問(wèn)題,堪稱完美了。
5.3MVI+Compose的優(yōu)缺點(diǎn)
MVI+Compose的優(yōu)點(diǎn)如下:
保證了框架的唯一性
由于每個(gè)view是在一開(kāi)始的時(shí)候就被數(shù)據(jù)源賦值的,無(wú)法被多處調(diào)用隨意修改,所以保證了框架不會(huì)被隨意打亂。更好的保證了代碼的低耦合等特點(diǎn)。
MVI+Compose的也存在一些缺點(diǎn):
不能稱為缺點(diǎn)的缺點(diǎn)吧。
由于Compose實(shí)現(xiàn)界面,是純靠kotlin代碼實(shí)現(xiàn),沒(méi)有借助xml布局,這樣的話,一開(kāi)始學(xué)習(xí)的時(shí)候,學(xué)習(xí)成本要高些。并且性能還未知,最好不要用在一級(jí)頁(yè)面。
六、如何選擇框架模式
6.1 架構(gòu)選擇的原理
通過(guò)上面這么多架構(gòu)的對(duì)比,可以總結(jié)出下面的結(jié)論。
耦合度高是現(xiàn)象,關(guān)注點(diǎn)分離是手段,易維護(hù)性和易測(cè)試性是結(jié)果,模式是可復(fù)用的經(jīng)驗(yàn)。
再來(lái)總結(jié)一下上面幾個(gè)框架適用的場(chǎng)景:
6.2框架的選擇原理
如果你的頁(yè)面相對(duì)來(lái)說(shuō)比較簡(jiǎn)單些,比如就是一個(gè)網(wǎng)絡(luò)請(qǐng)求,然后刷新列表,使用MVC就夠了。
如果你有很多頁(yè)面共用相同的邏輯,比如多個(gè)頁(yè)面都有網(wǎng)絡(luò)請(qǐng)求加載中、網(wǎng)絡(luò)請(qǐng)求、網(wǎng)絡(luò)請(qǐng)求加載完成、網(wǎng)絡(luò)請(qǐng)求加載失敗這種,使用MVP、MVVM、MVI,把接口封裝好更好些。
如果你需要在多處監(jiān)聽(tīng)數(shù)據(jù)源的變化,這時(shí)候需要使用LiveData或者Flow,也就是MVVM、MVI的架構(gòu)好些。
如果你的操作是串行的,比如登錄之后進(jìn)行賬號(hào)驗(yàn)證、賬號(hào)驗(yàn)證完再進(jìn)行點(diǎn)贊,這時(shí)候使用MVI更好些。當(dāng)然,MVI+Compose可以保證你的架構(gòu)不易被修改。
切勿混合使用架構(gòu)模式,分析透徹頁(yè)面結(jié)構(gòu)之后,選擇一種架構(gòu)即可,不然會(huì)導(dǎo)致頁(yè)面越來(lái)越復(fù)雜,無(wú)法維護(hù)
上面就是對(duì)所有框架模式的總結(jié),大家根據(jù)實(shí)際情況進(jìn)行選擇。建議還是直接上手最新的MVI+Compose,雖然多了些學(xué)習(xí)成本,但是畢竟Compose的思想還是很值得借鑒的。
審核編輯:劉清
-
處理器
+關(guān)注
關(guān)注
68文章
20086瀏覽量
243944 -
耦合器
+關(guān)注
關(guān)注
8文章
740瀏覽量
63443 -
Android系統(tǒng)
+關(guān)注
關(guān)注
0文章
57瀏覽量
14045 -
SpringMVC
+關(guān)注
關(guān)注
0文章
18瀏覽量
6094 -
mvp模式
+關(guān)注
關(guān)注
0文章
2瀏覽量
1236
原文標(biāo)題:Android架構(gòu)模式如何選擇
文章出處:【微信號(hào):OSC開(kāi)源社區(qū),微信公眾號(hào):OSC開(kāi)源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
想要用一個(gè)藍(lán)牙模塊與 DLP NIRscan Nano連接,怎么知道哪一個(gè)UUID才是該設(shè)備藍(lán)牙連接的UUID?
協(xié)調(diào)器在啟動(dòng)后,到底使用了哪一個(gè)信道呢?
標(biāo)準(zhǔn)藍(lán)牙接口與專有射頻協(xié)議到底哪一個(gè)更好?
正確的AR8031的驅(qū)動(dòng)程序是哪一個(gè)
Android中的alpha動(dòng)畫(huà)是這個(gè)AlphaAnimation, 鴻蒙的alpha動(dòng)畫(huà)是哪一個(gè)?
軟件架構(gòu)設(shè)計(jì)之常用架構(gòu)模式
vivoX20和一加5到底該買哪一個(gè)?
10種常見(jiàn)的軟件體系架構(gòu)模式分析以及它們的用法、優(yōu)缺點(diǎn)
詳解SOA五種基本架構(gòu)模式
零線和火線應(yīng)該先接哪一個(gè)
關(guān)于邏輯和物理架構(gòu)模型開(kāi)發(fā)之間的迭代
嵌入式7種架構(gòu)模式分析
架構(gòu)模式的基礎(chǔ)知識(shí)
嵌入式軟件最常見(jiàn)的架構(gòu)模式

Android架構(gòu)模式飛速演進(jìn) 到底哪一個(gè)才是自己最需要的?
評(píng)論