“終于懂了“系列:Jetpack AAC完整解析(四)MVVM - Android架構探索!

Jetpack AAC 系列文章:

“終于懂了“系列:Jetpack AAC完整解析(一)Lifecycle 完全掌握!

“終于懂了“系列:Jetpack AAC完整解析(二)LiveData 完全掌握!

“終于懂了“系列:Jetpack AAC完整解析(三)ViewModel 完全掌握!

“終于懂了“系列:Jetpack AAC完整解析(四)MVVM 架構探索!

“終于懂了“系列:Jetpack AAC完整解析(五)DataBinding 重新認知!

前面三篇介紹了Jetpack 架構組件中 最重要 的部分:生命周期組件-Lifecycle、感知生命周期的數據組件-LiveData、視圖模型組件-ViewModel。 這篇,就來探索下目前android開發中 最優秀、討論最多的架構模式—— MVVM

幾個月前,我所在項目完成了 MVVM 的架構改造。這篇在開始寫之前,我也閱讀了大量MVVM文章。所以,這篇盡量講清楚 開發架構模式和MVVM的本質,使得有一種 “哦,原來如此” 的豁然開朗。

注意,本篇完全 不會提 DataBinding、雙向綁定,文末會解釋為啥不提。

一、開發架構 是什么?

我們先來理解開發架構的本質是什么,維基百科對軟件架構的描述如下:

軟件架構是一個系統的草圖。軟件架構描述的對象是直接構成系統的抽象組件。各個組件之間的連接則明確和相對細致地描述組件之間的通訊。在實現階段,這些抽象組件被細化為實際的組件,比如具體某個類或者對象。在面向對象領域中,組件之間的連接通常用接口來實現。
拆分開來就是三條:

  1. 針對的是一個完整系統,此系統可以實現某種功能。
  2. 系統包含多個模塊,模塊間有一些關系和連接。
  3. 架構是實現此系統的實施描述:模塊責任、模塊間的連接。

為啥要做開發架構設計呢?

  1. 模塊化責任具體化,使得每個模塊專注自己內部
  2. 模塊間的關聯簡單化,減少耦合
  3. 易于使用、維護性好
  4. 提高開發效率

架構模式最終都是 服務于開發者。如果代碼職責和邏輯混亂,維護成本就會相應地上升。

宏觀上來說,開發架構是一種思想,每個領域都有一些成熟的架構模式,選擇適合自己項目即可。

二、Android開發中的架構

具體到Android開發中,開發架構就是描述 視圖層邏輯層數據層 三者之間的關系和實施:

  • 視圖層:用戶界面,即界面的展示、以及交互事件的響應。
  • 邏輯層:為了實現系統功能而進行的必要邏輯。
  • 數據層:數據的獲取和存儲,含本地、server。

正常的開發流程中,開始寫代碼之前 都會有架構設計這一過程。這就需要你選擇使用何種架構模式了。

我的Android開發之路完整地經過了 MVC、MVP、MVVM,相信很多開發者和我一樣都是這樣一個過程,先來回顧下三者。

2.1 MVC

MVC,Model-View-Controller,職責分類如下:

  • Model,模型層,即數據模型,用于獲取和存儲數據。
  • View,視圖層,即xml布局
  • Controller,控制層,負責業務邏輯。

View層 接收到用戶操作事件,通知到 Controller 進行對應的邏輯處理,然后通知 Model去獲取/更新數據,Model 再把新的數據 通知到 View 更新界面。這就是一個完整 MVC 的數據流向。

但在Android中,因為xml布局能力很弱,View的很多操作是在Activity/Fragment中的,而業務邏輯同樣也是寫在Activity/Fragment中

MVC

所以,MVC 的問題點 如下:

  1. Activity/Fragment 責任不明,同時負責View、Controller,就會導致其中代碼量大,不滿足單一職責。
  2. Model耦合View,View 的修改會導致 Controller 和 Model 都進行改動,不滿足最少知識原則。

2.2 MVP

MVP,Model-View-Presenter,職責分類如下:

  • Model,模型層,即數據模型,用于獲取和存儲數據。
  • View,視圖層,即Activity/Fragment
  • Presenter,控制層,負責業務邏輯。

MVP解決了MVC的問題:1.View責任明確,邏輯不再寫在Activity中,而是在Presenter中;2.Model不再持有View。

View層 接收到用戶操作事件,通知到Presenter,Presenter進行邏輯處理,然后通知Model更新數據,Model 把更新的數據給到Presenter,Presenter再通知到 View 更新界面。

MVP

MVP的實現思路:

  • UI邏輯抽象成IView接口,由具體的Activity實現類來完成。且調用Presenter進行邏輯操作。
  • 業務邏輯抽象成IPresenter接口,由具體的Presenter實現類來完成。邏輯操作完成后調用IView接口方法刷新UI。

MVP 本質是面向接口編程,實現了依賴倒置原則。MVP解決了View層責任不明的問題,但并沒有解決代碼耦合的問題,View和Presenter之間相互持有。

所以 MVP 有問題點 如下:

  1. 會引入大量的IView、IPresenter接口,增加實現的復雜度。
  2. View和Presenter相互持有,形成耦合。

2.3 MVVM

MVVM,Model-View-ViewModel,職責分類如下:

  • Model,模型層,即數據模型,用于獲取和存儲數據。
  • View,視圖,即Activity/Fragment
  • ViewModel,視圖模型,負責業務邏輯。

注意,MVVM這里的ViewModel就是一個名稱,可以理解為MVP中的Presenter。不等同于上一篇中的 ViewModel組件 ,Jetpack ViewModel組件是 對 MVVM的ViewModel 的具體實施方案。

MVVM 的本質是 數據驅動,把解耦做的更徹底,viewModel不持有view 。

View 產生事件,使用 ViewModel進行邏輯處理后,通知Model更新數據,Model把更新的數據給ViewModel,ViewModel自動通知View更新界面而不是主動調用View的方法

MVVM

MVVM在Android開發中是如何實現的呢?接著看~

到這里你會發現,所謂的架構模式本質上理解很簡單。比如MVP,甚至你都可以忽略這個名字,理解成 在更高的層面上 面向接口編程,實現了 依賴倒置 原則,就是這么簡單。

三、MVVM 的實現 - Jetpack MVVM

前面提到,架構模式選擇適合自己項目的即可。話雖如此,但Google官方推薦的架構模式 是適合大多數情況,是非常值得我們學習和實踐的。

好了,下面我們就來詳細介紹 Jetpack MVVM 架構。

3.1 Jetpack MVVM 理解

Jetpack MVVM 是 MVVM 模式在 Android 開發中的一個具體實現,是 Android中 Google 官方提供并推薦的 MVVM實現方式。

不僅通過數據驅動完成徹底解耦,還兼顧了 Android 頁面開發中其他不可預期的錯誤,例如Lifecycle 能在妥善處理 頁面生命周期 避免view空指針問題,ViewModel使得UI發生重建時 無需重新向后臺請求數據,節省了開銷,讓視圖重建時更快展示數據。

首先,請查看下圖,該圖顯示了所有模塊應如何彼此交互:

Jetpack MVVM標準推薦架構

各模塊對應MVVM架構:

  • View層:Activity/Fragment
  • ViewModel層:Jetpack ViewModel + Jetpack LivaData
  • Model層:Repository倉庫,包含 本地持久性數據 和 服務端數據

View層 包含了我們平時寫的Activity/Fragment/布局文件等與界面相關的東西。

ViewModel層 用于持有和UI元素相關的數據,以保證這些數據在屏幕旋轉時不會丟失,并且還要提供接口給View層調用以及和倉庫層進行通信。

倉庫層 要做的主要工作是判斷調用方請求的數據應該是從本地數據源中獲取還是從網絡數據源中獲取,并將獲取到的數據返回給調用方。本地數據源可以使用數據庫、SharedPreferences等持久化技術來實現,而網絡數據源則通常使用Retrofit訪問服務器提供的Webservice接口來實現。

另外,圖中所有的箭頭都是單向的,例如View層指向了ViewModel層,表示View層會持有ViewModel層的引用,但是反過來ViewModel層卻不能持有View層的引用。除此之外,引用也不能跨層持有,比如View層不能持有倉庫層的引用,謹記每一層的組件都只能與它相鄰層的組件進行交互。

這種設計打造了一致且愉快的用戶體驗。無論用戶上次使用應用是在幾分鐘前還是幾天之前,現在回到應用時都會立即看到應用在本地保留的數據。如果此數據已過期,則應用的Repository將開始在后臺更新數據。

3.2 實施

我們來舉個完整的例子 - 在頁面中顯示用戶信息列表,來說明 Jetpack MVVM 的具體實施。

3.2.1 構建界面

首先創建一個列表頁面 UserListActivity,并且知道頁面所需要的數據是,用戶信息列表。

那么 用戶信息列表 如何獲取呢?根據上面的架構圖,就是ViewModel了,所以我們創建 UserListViewModel 繼承自 ViewModel,并且把 用戶信息列表 以 LiveData呈現。

public class UserListViewModel extends ViewModel {
    //用戶信息
    private MutableLiveData<List<User>> userListLiveData;
    //進條度的顯示
    private MutableLiveData<Boolean> loadingLiveData;

    public UserListViewModel() {
        userListLiveData = new MutableLiveData<>();
        loadingLiveData = new MutableLiveData<>();
    }
    
    public LiveData<List<User>> getUserListLiveData() {
        return userListLiveData;
    }
    public LiveData<Boolean> getLoadingLiveData() {
        return loadingLiveData;
    }
    ...
}

LiveData 是一種可觀察的數據存儲器。應用中的其他組件可以使用此存儲器監控對象的更改,而無需在它們之間創建明確且嚴格的依賴路徑。LiveData 組件還遵循應用組件(如 Activity、Fragment 和 Service)的生命周期狀態,并包括清理邏輯以防止對象泄漏和過多的內存消耗。

將 UserListViewModel 中的字段類型更改為 MutableLiveData<List<User>>。現在,更新數據時,系統會通知 UserListActivity。此外,由于此 LiveData 字段具有生命周期感知能力,因此當不再需要引用時,會自動清理它們。

另外,注意到暴露的獲取LiveData的方法 返回的是LiveData類型,即不可變的,而不是MutableLiveData,好處是避免數據在外部被更改。(參見LivaData篇文章)

現在,我們修改 UserListActivity 以觀察數據并更新界面:

//UserListActivity.java
...
    //觀察ViewModel的數據,且此數據 是 View 直接需要的,不需要再做邏輯處理
    private void observeLivaData() {
        mUserListViewModel.getUserListLiveData().observe(this, new Observer<List<User>>() {
            @Override
            public void onChanged(List<User> users) {
                if (users == null) {
                    Toast.makeText(UserListActivity.this, "獲取user失敗!", Toast.LENGTH_SHORT).show();
                    return;
                }
                //刷新列表
                mUserAdapter.setNewInstance(users);
            }
        });

        mUserListViewModel.getLoadingLiveData().observe(this, new Observer<Boolean>() {
            @Override
            public void onChanged(Boolean aBoolean) {
                //顯示/隱藏加載進度條
                mProgressBar.setVisibility(aBoolean? View.VISIBLE:View.GONE);
            }
        });
    }

每次更新用戶列表信息數據時,系統都會調用 onChanged() 回調并刷新界面,而不需要 ViewModel主動調用View層方法刷新,這就是 數據驅動 了 —— 數據的更改 驅動 View 自動刷新。

因為LiveData具有生命周期感知能力,這意味著,除非 Activity 處于活躍狀態,否則它不會調用 onChanged() 回調。當調用 Activity 的 onDestroy() 方法時,LiveData 還會自動移除觀察者。

另外,我們也沒有添加任何邏輯來處理配置更改(例如,用戶旋轉設備的屏幕)。UserListViewModel 會在配置更改后自動恢復,所以一旦創建新的 Activity,它就會接收相同的 ViewModel 實例,并且會立即使用當前的數據調用回調。鑒于 ViewModel 對象應該比它們更新的相應 View 對象存在的時間更長,因此 ViewModel 實現中不得包含對 View 對象的直接引用,包括Context。

3.2.2 獲取數據

現在,我們已使用 LiveData 將 UserListViewModel 連接到UserListActivity,那么如何獲取用戶個人信息列表數據呢?

實現 ViewModel 的第一個想法可能是 使用Retrofit/Okhttp調用接口 來獲取數據,然后將該數據設置給 LiveData 對象。這種設計行得通,但如果采用這種設計,隨著應用的擴大,應用會變得越來越難以維護。這樣會使 UserListViewModel 類承擔太多的責任,這就違背了單一職責原則。

ViewModel 會將數據獲取過程委派給一個新的模塊,即Repository

Repository模塊會處理數據操作。它們會提供一個干凈的 API,以便應用內其余部分也可以輕松獲取該數據。數據更新時,它們知道從何處獲取數據以及進行哪些 API 調用。您可以將Repository視為不同數據源(如持久性模型、網絡服務和緩存)之間的媒介。

public class UserRepository {

    private static UserRepository mUserRepository;
    public static UserRepository getUserRepository(){
        if (mUserRepository == null) {
            mUserRepository = new UserRepository();
        }
        return mUserRepository;
    }

    //(假裝)從服務端獲取
    public void getUsersFromServer(Callback<List<User>> callback){
        new AsyncTask<Void, Void, List<User>>() {
            @Override
            protected void onPostExecute(List<User> users) {
                callback.onSuccess(users);
                //存本地數據庫
                saveUsersToLocal(users);
            }
            @Override
            protected List<User> doInBackground(Void... voids) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //假裝從服務端獲取的
                List<User> users = new ArrayList<>();
                for (int i = 0; i < 20; i++) {
                    User user = new User("user"+i, i);
                    users.add(user);
                }
                return users;
            }
        }.execute();
    }
    

雖然Repository模塊看起來不必要,但它起著一項重要的作用:它會從應用的其余部分中提取數據源。現在,UserListViewModel 是不知道數據來源的,因此我們可以為ViewModel提供從幾個不同的數據源獲取數據。

3.2.3 連接 ViewModel 與存儲區

我們在UserListViewModel 提供一個方法,用戶Activity獲取用戶信息。此方法就是調用Repository來執行,并且吧數據設置到LiveData。

public class UserListViewModel extends ViewModel {
    //用戶信息
    private MutableLiveData<List<User>> userListLiveData;
    //進條度的顯示
    private MutableLiveData<Boolean> loadingLiveData;

    public UserListViewModel() {
        userListLiveData = new MutableLiveData<>();
        loadingLiveData = new MutableLiveData<>();
    }
    
    /**
     * 獲取用戶列表信息
     * 假裝網絡請求 2s后 返回用戶信息
     */
    public void getUserInfo() {

        loadingLiveData.setValue(true);

        UserRepository.getUserRepository().getUsersFromServer(new Callback<List<User>>() {
            @Override
            public void onSuccess(List<User> users) {
                loadingLiveData.setValue(false);
                userListLiveData.setValue(users);
            }

            @Override
            public void onFailed(String msg) {
                loadingLiveData.setValue(false);
                userListLiveData.setValue(null);
            }
        });
    }

    //返回LiveData類型
    public LiveData<List<User>> getUserListLiveData() {
        return userListLiveData;
    }
    public LiveData<Boolean> getLoadingLiveData() {
        return loadingLiveData;
    }
}

在Activity中,就要獲取UserListViewModel實例,獲取用戶信息:

//UserListActivity.java
public class UserListActivity extends AppCompatActivity {
    private UserListViewModel mUserListViewModel;
    private ProgressBar mProgressBar;
    private RecyclerView mRvUserList;
    private UserAdapter mUserAdapter;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user_list);

        initView();
        initViewModel();
        getData();
        observeLivaData();
    }
    private void initView() {...}

    private void initViewModel() {
        ViewModelProvider viewModelProvider = new ViewModelProvider(this);
        mUserListViewModel = viewModelProvider.get(UserListViewModel.class);
    }

    /**
     * 獲取數據,調用ViewModel的方法獲取
     */
    private void getData() {
        mUserListViewModel.getUserInfo();
    }
    
    private void observeLivaData() {...}

3.2.4 緩存數據

現在UserRepository 有個問題是,它從后端獲取數據后,不會將緩存該數據。因此,如果用戶在離開頁面后再返回,則應用必須重新獲取數據,即使數據未發生更改也是如此。這就浪費了寶貴的網絡資源,迫使用戶等待新的查詢完成。
所以,我們向 UserRepository 添加了一個新的數據源,本地緩存。緩存實現 可以是 數據庫、SharedPreferences等持久化技術。(具體實現就不再寫了)

//UserRepository.java

    //從本地數據庫獲取
    public void getUsersFromLocal(){
        // TODO: 2021/1/24 從本地數據庫獲取
    }

    //存入本地數據庫 (從服務端獲取數據后可以調用)
    private void saveUsersToLocal(List<User> users){
        // TODO: 2021/1/24 存入本地數據庫
    }

到這里,Jetpack MVVM 就介紹完了。

實際上只要前面介紹的 Lifecycle、LivaData、ViewModel 熟練掌握的話,這里是十分好理解的。

3.3 注意點

  1. 在應用的各個模塊之間設定明確定義的職責界限

  2. ViewModel 不能持有 View層引用,包括Context也不能持有。

  3. 將一個數據源指定為單一可信來源。 每當需要訪問數據時,都應一律源于此單一可信來源。 例如 UserRepository會將網絡服務響應保存在數據庫中。這樣一來,對數據庫的更改將觸發對活躍 LiveData 對象的回調。數據庫會充當單一可信來源

  4. 保留盡可能多的相關數據和最新數據。 這樣,即使用戶的設備處于離線模式,他們也可以使用您應用的功能。請注意,并非所有用戶都能享受到穩定的高速連接。

  5. 顯示頁面狀態。 例如例子中的加載進度條,就是觀察 ViewModel中的MutableLiveData<Boolean> loadingLiveData 進行操作的。

3.4 MVP改造MVVM

了解了Jetpack MVVM的實現,再來改造 MVP 是很簡單的了。

步驟如下:

  1. 去除Presener 對View、context的引用。
  2. Presener 替換成ViewModel的實現,獲取的數據以 LivaData呈現
  3. 刪除定義的IView等接口,Activity/Fragment中 獲取ViewModel實例,調用其方法獲取數據。
  4. Activity/Fragment 觀察需要的 LivaData 然后刷新UI

這樣就已經成為了MVVM。當然也要檢查下 原MVP的 Model層的實現,是否滿足上面的要求。

四、總結

本篇介紹了 架構模式的含義,回顧和比較了Android中的架構模式MVC、MVP、MVVM,最好在 Jetpack架構組件 基礎上 介紹了 MVVM 的詳細實現方法、注意點,以及MVP的改造。

整篇下來,基本很簡單容易理解的。 例子是很簡單的,所以在實際開發中 需要深入理解 MVVM 數據驅動的本質,和MVP的區別。

有人可能會有疑惑:怎么完全沒有提 DataBinding、雙向綁定?

實際上,這也是我之前的疑惑。 沒有提 是因為:

  1. 我不想讓讀者 一提到 MVVM 就和DataBinding聯系起來
  2. 我想讓讀者 抓住 MVVM 數據驅動 的本質。
  3. 而DataBinding提供的雙向綁定,是用來完善Jetpack MVVM 的工具,其本身在業界又非常具有爭議性。
  4. 掌握本篇內容,已經是Google推薦的開發架構,就已經實現 MVVM 模式。在Google官方的 應用架構指南 中 也同樣絲毫沒有提到 DataBinding。

所以,下一篇,將繼續介紹 Jetpack AAC 的組件:數據綁定組件 DataBinding、數據庫組件 Room,作為 Jetpack MVVM 的完善補充點。 并且 也將是 Jetpack AAC 完整解析 系列的最后一篇。 敬請期待!

Demo 地址

.

感謝與參考:

ViewModel官方文檔

是讓人耳目一新的 Jetpack MVVM 精講啊!

Android 開發中的架構模式 -- MVC / MVP / MVVM

.

你的 點贊、評論,是對我的巨大鼓勵!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容