動態

詳情 返回 返回

Android 架構模式如何選擇 - 動態 詳情

作者:vivo 互聯網客户端團隊-Xu Jie

Android架構模式飛速演進,目前已經有MVC、MVP、MVVM、MVI。到底哪一個才是自己業務場景最需要的,不深入理解的話是無法進行選擇的。這篇文章就針對這些架構模式逐一解讀。重點會介紹Compose為什麼要結合MVI進行使用。希望知其然,然後找到適合自己業務的架構模式

一、前言

不得不感嘆,近些年android的架構演進速度真的是飛快,拿筆者工作這幾年接觸的架構來説,就已經有了MVC、MVP、MVVM。正當筆者準備把MVVM應用到自己項目當中時,發現谷歌悄悄的更新了開發者文檔(應用架構指南 | Android 開發者 | Android Developers (google.cn))。這是一篇指導如何使用MVI的文章。那麼這個文章到底為什麼更新,想要表達什麼?裏面提到的Compose又是什麼?難道現在已經有的MVC、MVP、MVVM不夠用嗎?MVI跟已有的這些架構又有什麼不同之處呢?

有人會説,不管什麼架構,都是圍繞着“解耦”來實現的,這種説法是正確的,但是耦合度高只是現象,採用什麼手段降低耦合度?降低耦合度之後的程序方便單元測試嗎?如果我在MVC、MVP、MVVM的基礎上做解耦,可以做的很徹底嗎?

先告訴你答案, MVC、MVP、MVVM無法做到徹底的解耦,但是MVI+Compose可以做到徹底的解耦,也就是本文的重點講解部分。本文結合具體的代碼和案例,複雜問題簡單化,並且結合較多技術博客做了統一的總結,相信你讀完會收穫頗豐。

那麼本篇文章編寫的意義,就是為了能夠深入淺出的講解MVI+Compose,大家可以先試想下這樣的業務場景,如果是你,你會選擇哪種架構實現?

業務場景考慮

  1. 使用手機號進行登錄
  2. 登錄完之後驗證是否指定的賬號A
  3. 如果是賬號A,則進行點贊操作

上面三個步驟是順序執行的,手機號的登錄、賬號的驗證、點贊都是與服務端進行交互之後,獲取對應的返回結果,然後再做下一步。

在開始介紹MVI+Compose之前,需要循序漸進,瞭解每個架構模式的缺點,才知道為什麼Google提出MVI+Compose。

正式開始前,按照架構模式的提出時間來看下是如何演變的,每個模式的提出往往不是基於android提出,而是基於服務端或者前端演進而來,這也説明設計思路上都是大同小異的:

圖片

二、架構模式過去式?

2.1 MVC已經存在很久了

MVC模式提出時間太久了,早在1978年就被提出,所以一定不是用於android,android的MVC架構主要還是源於服務端的SpringMVC,在2007年到2017年之間,MVC佔據着主導地位,目前我們android中看到的MVC架構模式是這樣的。

MVC架構這幾個部分的含義如下,網上隨便找找就有一堆説明。

MVC架構分為以下幾個部分

  • 【模型層Model】:主要負責網絡請求,數據庫處理,I/O的操作,即頁面的數據來源
  • 【視圖層View】:對應於xml佈局文件和java代碼動態view部分
  • 【控制層Controller】:主要負責業務邏輯,在android中由Activity承擔

(1)MVC代碼示例

我們舉個登錄驗證的例子來看下MVC架構一般怎麼實現。

這個是controller

MVC架構實現登錄流程-controller

public class MvcLoginActivity extends AppCompatActivity {
    private EditText userNameEt;
    private EditText passwordEt;
    private User user;
 
    @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()
            }
        }
    }

}


@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的思想還是很值得借鑑的。

user avatar zaotalk 頭像 linlinma 頭像 yuzhihui 頭像 user_ze46ouik 頭像 tuhooo 頭像 columsys 頭像 debuginn 頭像 yaochujiadetiebanshao 頭像 lewyon 頭像 fecify 頭像 zread_ai 頭像 jueqiangqingtongsan 頭像
點贊 31 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.