MF99 coding 💻

keep learning; keep coding;

MVVM 經驗分享

關於 MVVM 的基本概念,可以回顧一下上一篇

mouseface99.hatenablog.com

這篇直接就我之前工作上的一個實際案例,分享一下我們當初使用 MVVM 建構這個產品的一些經驗

簡單介紹一下我們這個產品:

基本上是一個體育向的新聞+社群平台,平台中提供許多體育賽事的資訊、比數,以及相關的球隊/球員新聞報導。 另外也提供用戶在平台中相互討論、互動。

大致的介面大概像這樣

f:id:mouseface99:20200524225103p:plain

大致就是主頁有幾種大功能分類,然後在某些子頁面(比方說球員/球隊)有細部的數據及資料呈現。 接著就開始進行開發前的架構設計動作

App 架構設計

當收到這樣的需求的時候,可以先參考上一篇 MVVM 中 Android MVVM 的基本架構

f:id:mouseface99:20200524155927p:plain

這時候跟幾位 Developer 討論過後,我們就開始建構起 App 的MVVM 架構,大致規劃完後,會產出類似以下的架構設計圖

f:id:mouseface99:20200524225336p:plain

這個架構圖其實蠻重要的,有幾個重點:

  1. 裡面明確規劃了 View 的部分該如何劃分,哪些可以複用
  2. 裡面定義了各個頁面/功能元件或甚至是項目的「統一名稱」(*)
  3. 規劃了 repository 的分類,可能根據不同 Server 或是不同的功能類型做區分,盡可能避免整個 App 共用同一個 repository (誰也不想在 IDE 中,按下 . 之後看到 300 項 function 列表.... 另外在 function name 的取名字上也會很頭痛)
  4. 也稍微規劃了哪些 View 可以共用 ViewModel,哪些需要切分開

(*)「統一名稱」這一點蠻重要的,盡可能同一個功能/物件,在整個 codebase 甚至整個系統中使用同一個命名,像是「比賽」這件事,在整個 App 裡面就統一使用 Game,而不要有些地方用 Game 有些地方又用 Match 或是其他名稱,這樣容易誤導其他開發者。

Codebase 結構設計

架構出來後,再來通常會讓開發團隊有點頭痛的就是 package 的分類方式,由於這部分比較自由,所以很多團隊都會有不同類型的分類法。

有根據功能分類、架構分類、甚至是 Owner 分類(我真的看過....)

不過,根據我們這幾年的團隊合作經驗中,我們選擇了最外層根據架構分類,底下再根據功能分類。

所以,根據目前這個 App 架構,初步的 codebase 結構設計如下(這也是一個蠻常見且蠻通用的架構)

f:id:mouseface99:20200524234746p:plain

這裡面有幾個重點:

  1. 最外層結構,由 viewrepository 區分 View 與 Model,內層再根據不同功能拆分
  2. ViewModel 由於特性,分類上不太需要跟 View 硬分開,有時候強制分開的話,第二層功能拆分的內容會跟 View 太接近,而且在找 code 的時候也會不好找,所以習慣性會跟 View 屬性的 Activity / Fragment 放在一起
  3. 外層還有一個 entity ,用來集中管理 App 中所運用到的 Entity model (*)
  4. 另外可以把程式進入點的 MainActivityMyApplication 放在最外層,方便未來 trace

(*)在使用 Retrofit 或是 Room 的時候,我們常常會使用一些 DTO/DAO (aka POJO) 物件,用來 mapping APIJSON 或是資料庫中的數據,但是不建議直接拿這些 POJO 物件在 App 內部流通,因為這些物件通常會跟 API 或是 DB 格式高度相依,所以一但資料結構有調整,可能會影響到整個 ViewModel / View。
所以習慣上,我會根據 View 的特性,建立一套獨立的 entity 物件區,主要就是把 model 來的資料,整理成 UI 層方便使用的格式與架構。至於 POJO 怎麼轉化成 Entity,那就是在 repository 層要去處理的。 這樣也能夠有效的降低 Model 與 View 之間的耦合關係。
至於 POJO,就開一個 package 然後統一隱身在 repository 層之下就可以了。

可以開始動手了!

有了 App 架構以及 codebase 結構後,就可以開始動手了!

這時候會開始引入幾個重要的元素: Data binding 與 LiveData

之前講 MVVM 的概念時有提到, ViewModel 的目的為呈現當下數據的情況,然後資料有更新的時候,最好 View 能夠直接反應 ViewModel 的狀態變化。 所以這時候 Android 的 LiveData 與 Data binding 就派上用場了。

過去我們的開發習慣,大多都是操作某個動作後,然後用非同步的方式去實作這個動作的 callback,然後在 callback 中試圖去更新對應的 UI 元件。

但是這樣的做法有一些危險性

  1. 你的 Callback 是跟操作的動作有關,但是最後卻是更新 UI 元件。 UI 元件應該只跟「資料」有關
  2. 如果同一個欄位,會有兩個不同的動作觸發改變的話,就要寫兩份 callback
  3. 非同步的實作,你永遠都不知道 callback thread 回來時,你的 UI 元件是否還存在畫面上(很容易造成非預期的 crash)

這幾點,透過 LiveData 就提供了另外一種 Observer 的思路。 與其把「動作」與「更新介面」的關係綁定,還不如把兩者切開。

用戶做了某些操作,的確會通知 ViewModel 觸發某些非同步動作,但是 UI 層這階段就到此為止。

然後畫面上的 UI 元件(TextView or ImageView)則透過 Observer 的關係來綁定在某個 LiveData,當這個資料有更新時,就會自動更新 UI 上的數據。 並且 LiveData 的機制中引入了 LifeCycleOwner 的機制,會確保目標對象在適當的 LifeCycle status 才會被更新。

這邊舉一個實際的 call flow 流程,可能會更容易理解

f:id:mouseface99:20200525005712p:plain

(初始化)畫面切換到 StatsFragment,並在 onCreate 中取得 ViewModel 物件,並且建立 Data Binding 的連結

  1. 畫面完成後, Fragment 通知 ViewModel 的 initial data fetch
  2. ViewModel 通知 repository 取得資料
  3. repository 先從 DB 中取得了 cache 的資料,並轉換成對應的 Entity 後回傳到 ViewModel (同時也觸發了 API request)
  4. ViewModel 更新 repository 來的數據(不管當前畫面有沒有需要,只要有更新就會 update LiveData)
  5. Fragment / View 層 Observer 的 LiveData 有更新,便即時更新畫面
  6. Server 最新數據回來了
  7. repository 更新給 ViewModel 的同時,也更新 cache 的 DB
  8. ViewModel 再次根據 repository 來的數據,更新 LiveData,並且 UI 再次更新到最新的數據

這中間有幾點值得留意:

  • ViewModel 不需要知道 repo 給的資料是 Cache 還是 Server 來的
  • ViewModel 不需要知道哪些數據是 View 目前需要用到的(比方說 Profile 在這個介面可能就用不到,但是在其他 fragment,或是在其他 screen-size or Orientation 所對應的 layout 中或許就有)
  • ViewModel 中提供的 init data fetch function,在任何有需要的地方都可以 invoke (像是 View 的 onResume 或是 App 有設計了 pull-to-refresh 或是 refresh button 的時候都可以呼叫)
  • View 中元件的更新,只跟「數據更新」有關,它不需要知道他更新的原因是因為有 cache,還是 Server 回傳了新資料(反正我這個元件就是負責呈現這個數據,數據更新了我就要更新)

引入了 Data binding 與 LiveData 後,整體開發/維護上帶來了以下好處:

  1. 不太需要擔心非同步造成的 crash 問題(只要明確的界定每個元件 Observer 時的 LifeCycle scope 在哪)
  2. 把用戶的「操作行為」與「介面更新」的邏輯切開,讓 View 與 ViewModel 中的每一個 function 的作用都更明確且單一
  3. Data binding 的確精簡了很多不必要的 coding (基本上只要把 xml 與 ViewModel 的關係一次綁定就好)

好還要更好

在明確的導入 MVVM 與找到合適的 codebase 架構,以及大範圍的使用 Data binding 與 LiveData 後,整個 App 的開發與維護上其實就順利很多。

因為整個 codebase 可讀性高,稍微參考一下 App 架構與 codebase 很快就能加入開發,並且在新增功能與修復 bug 的時候,做的改動也能夠很快的拆分整個改動所需要新增/調整的元件應該如何放置。

不過當然也遇到了一些問題以及未來可以再更精進的部分

ViewModel 的 scope 大小

到底哪些 View 可以合併共用同一個 ViewModel ? 還是要最精簡化每一個 View 使用自己的 ViewModel,這個在開發過程中其實一直在反覆討論。

某種程度來說兩個方向都沒有錯,但是總是要在耦合度與 codebase 複雜度之間取得一個相對較好的平衡。

View 之間的切換與參數傳遞

這部分不管是 Activity 之間的切換,或是頁面中 fragment 之間的切換, call stack 的維護,還有每次轉換頁面時的參數傳遞。 這部分的確在寫 code 的時候花了很多時間,以及在產品功能有調整的時候,造成蠻大的 loading 在調整。

這部分其實當初沒有時間導入 Navigation 有點可惜,這陣子開始研究 Navigation 後,其實有很多問題可以透過這個機制去改善的,或許之後有機會可以再分享這部分的研究心得。(某種程度有點往 VIPER 的方向演化,把 routing 與 interaction 的元件單獨抽出來)

後記

這個專案算是我參與過算是範圍不大但是變化性很高,但是由於合理的設計與架構規劃,讓我們 App 開發團隊在整個開發,維護的過程中其實少走了很多冤枉路,而且還在過程中盡可能的可以抽出時間來進行多次的 refactor,讓每一次改版都能讓架構更一致,系統更簡潔但是更穩固。