Android App 開發實戰系列 Part 4. ViewModel + View

Engine Bai
9 min readNov 10, 2020

--

Part 4. 我們要來講解 ViewModel + View,同時會講解 MVVM 的大原則和核心概念。

上一個部份 Part 3. 我們完成了 Model 層 — MovieRepo,這 Part 4. 將要來介紹 ViewModel 來為 UI 提供所需要的資料,針對使用者操作做出對應的動作,以及介紹 View 如何使用 ViewModel 所提供的資料流來呈現 UI。

完整程式碼 https://github.com/enginebai/MovieHunt 已經釋出,可以下載程式碼邊看程式碼邊學習,歡迎給星 支持。這一系列文章是有連貫性的,如果還沒看過前面文章,建議先去看過前面的章節。傳送門:(Part 1.) (Part 2.) (Part 3.)

ViewModel 介紹

什麼是 ViewModel?? 在 MVVM 架構裡面,View 是不包含資料、也不直接操作資料的邏輯,這樣做是要讓職責更加明確,View 就是只有負責 UI 介面上的呈現或者純 UI 的邏輯,不負責資料處理或呈現的邏輯,我告訴你要呈現什麼資料,而資料怎麼來、怎麼處理不是 View 需要知道和負責的,在測試上可以有很明確測試的目的,我們對於 View 只需要提供正確的資料,就能預期呈現正確的畫面和操作。

那麼介面上的資料處理和呈現邏輯該由誰負責呢?這就是 ViewModel 的職責,它擔任 View 和 Model 的橋樑,負責為 View 提供所需要呈現的資料,也負責為 View 的操作互動提供對應的方法,譬如點擊儲存按鈕後要將資料寫到資料庫去或打 API。

ViewModel 實作

了解 ViewModel 職責之後,我們開始為電影列表頁面實作 ViewModel,這邊有幾個需求我們要滿足:

  • 進入頁面後會開始載入列表。
  • 列表提供分頁載入的機制。
  • 資料載入的時候需要呈現 ProgressBar。
  • 列表可以下拉更新。

我們從 fetchMovieList(category: MovieCategory) 來讓 View 呼叫可以開始載入列表,ViewModel 裡面宣告了一個 BehaviorSubject 來做資料載入的事件來源,當有載入事件觸發時,會帶動觸發 movieRepo.fetchMovieList(category) ,最後把載入的 Listing 資料流暫存起來,供後續使用。

我們在 Part 3. 有講解到 Listing 的實作,在 ViewModel 會將 Listing 的每個資料流 PagedList / refreshState / loadMoreState 轉成 UI 所要的欄位屬性,或者可以更簡單的方式直接提供 Listing 供 View 使用。

View 實作

View 這一層顧名思義就是實作 UI / Layout / Custom View … 等以及單純 UI 相關的邏輯,和 ViewModel 做互動,透過「 觀察者模式 」作為觀察者來觀察 ViewModel 的變化來更新 View 的狀態。

到目前為止,MVVM View / ViewModel / Model 分層都已經出現介紹到了,我們可以來介紹 MVVM 的核心概念,同時講解上述的程式邏輯。

MVVM 核心概念

關注點分離 Separation of Concerns

MVVM 的三個分層主要職責劃分是:

  1. View 負責畫面的包含各種 Android 相關的元件和實作。
  2. ViewModel 為 UI 提供相對應的資料流讓 View 觀察,主要從 Model 取資料、做相對應的轉換和處理邏輯後給發送出去。在 ViewModel 裡面不會有任何 Android 相關的元件,這個跟 MVP 的 Presenter 是一樣的概念,這樣做的原因也是讓 ViewModel 可以做單純 JVM 的單元測試,而不需要依賴任何 Android 的套件。
  3. Model 負責資料層,資料可能從 API 來、也可能從本機端的資料庫、檔案、RemoteConfig / SharedPreference / Socket / Push Notification… 等等來,可以將資料封裝成觀察者模式,對外提供資料流,只要資料有改變, 它就會送出新資料,通知所有觀察者。

資料變更統一來自於 Model

畫面的更新來自於資料源頭的改變:View 只觀察 ViewModel 的資料變化而更新畫面,而 ViewModel 的變化來自於 Model。 ViewModel 不會叫 View 來做任何事情,這是和 MVP 最大的不同,View 和 ViewModel 也不是一對一的關係,這樣的設計讓 ViewModel 相同的邏輯和資料流,可以讓不同的 View 共同使用

如果使用者操作會改變資料(例如:對一部電影按了收藏,收藏按鈕狀態要選取起來),則是去改變 Model 層的資料(MovieModel.isFavorite = true),Model 資料有變更會通知 ViewModel → ViewModel 有變更會通知 View,View 觀察 ViewModel 的變更而做畫面的更新(收藏按鈕選取起來),而我們不會直接去改 UI 的狀態,不會直接去把收藏按鈕選取起來。

這樣做可以讓 UI 上的顯示是和資料是完全同步,資料只有一份,就是從 Model 層來的,這樣做有什麼好處?

  1. 我們在其他地方不做額外的資料複製動作(例如在 ViewModel / View),這樣做會讓開發者付出更多額外的成本來維護資料的一致性和同步,除錯上更是增加困難度,因為你很難知道資料在哪裡被改掉了, UI 上也不知道現在是用哪一份資料。
  2. 資料如果在其他地方複製了,表示我們可以為了做一個功能而隨便改複製的資料來達成效果,但是埋下資料不一致的隱形炸彈。

想想剛剛提的收藏電影例子,假設今天我們在 View 那一層也複製了一個 isFavorite 來讓收藏按鈕可以變更選取的狀態,在 Model 那邊也有原本的 MovieModel.isFavorite 資料,過了一陣子換人接手這功能,他卻沒有注意到按鈕狀態是用 View.isFavorite,而其實真正改的狀態是 MovieModel.isFavorite,今天他按下了收藏按鈕,改了 View.isFavorite 讓按鈕選取起來,但是忘了改 MovieModel.isFavorite 造成資料的不同步,如果其他地方要用到這狀態也可能因此壞掉,產生更多 Bug,這讓開發者更難去使用這樣的程式碼(到底要用哪一份資料?)以及追這樣的問題(WTF 到底是哪邊改了資料?這 UI 是用哪一份資料?)。

回到我們的程式碼來說, MovieListFragment 是 View,觀察 ViewModel 的 PagedList / refreshState / loadMoreState 的狀態變更。

  • 當使用者開啟時,會觸發 MoviewListViewModel 去和 MovieRepo 拉電影列表 API 的資料,拉資料的時候會變更 refreshState 的狀態。
  • 因為 refreshState 狀態變更了,將更新的狀態從 MovieRepo 一路傳遞到 MovieListFragment 的觀察者身上,可以顯示 / 隱藏 ProgressBar。
  • 當列表 API 資料載入完成後,會將資料更新到 PagedList,一樣變更狀態一路從 MovieRepo 傳遞到 MovieListFragment 觀察者身上,可以顯示載入後的電影列表資料。

Dependency Rules

Source: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

從上面的程式架構來看,可以看到 View 使用 ViewModel、(ViewModel 使用 Model),也就是依賴方向是 View → ViewModel (→ Model),View 知道 ViewModel 的存在,但 ViewModel 不知道 View 的存在,不曉得是哪一個 View 正在使用它。

這樣的概念曾在 Clean Architecture 提到,A → B 代表 A 依賴(使用) B,A 知道 B、B 不知道 A 的存在、B 不知道是誰在使用它。 這樣的設計是我們要確保元件之間有適當的隔離, B 受到保護不受到「A 的改變」而影響到或需要更動,也可以讓 A 可以延遲被決定(UI 擺放位置可以延遲到最後再決定)而不影響到 B。

以舉例來說來說(上圖),Business Logic 會用到資料庫 Database Access,但是我們希望不會因為我們選用什麼資料庫而應該影響到 Business Logic,今天不會因為說我們從 MySQL 換到 PostgreSQL 而需要改 Business Logic,所以兩者之間需要適當的隔離。

中間我們劃了一條線,Business Logic 和 Database Access 中間我們墊一層 Database Interface,Business Logic 只透過 Database Interface 存取資料,而 Database Access 底層怎麼實作,Buiness Logic 不在乎也不受影響,這個跟設計模式裡面的 Strategy Pattern 十分類似。

因為我們的 View 可能會時常的變動,這樣簡單的 UI 變化不應該影響到 ViewModel 的邏輯,以我們呈現電影詳細頁面來說,我們 UI 從 v1 → v2 → v3 變化,ViewModel 都可以不用改動、可以一路使用下去。

結語

這篇我們點出了重頭戲 MVVM 的核心概念,我們這邊來做一個小總結:

  1. View 是單純 UI 的實作、ViewModel 則是為 View 提供所需資料、Model 是我們的底層資料或 Business Logic。
  2. Model → ViewModel → View 都是透過觀察者模式來做資料的綁定。
  3. 我們不會直接去改 UI 的狀態,所有 UI 上的變更都來自於 Model 的變更,要改 UI 狀態直接去改 Model。同時 Model 會提供資料的單一出口,不會在其他地方複製資料造成資料狀態不一致。

如果你有任何和此專案相關的疑問,歡迎留言給我交流或討論。完整程式碼: https://github.com/enginebai/MovieHunt 歡迎 Fork + Star ⭐ 支持。

--

--

Engine Bai

白昌永 (大白) Software Engineer, Athlete, Learner. Focused on Mobile / Backend / AI + ML