­

MVVM、RxJava、Retrofit三劍合璧,事半功倍

  • 2019 年 10 月 28 日
  • 筆記

前言

MVVM和MVP是當前比較流行的兩種應用開發架構,兩者各有優缺點。目前公司所有Android項目用的都是MVP架構,所以這篇文章我們可以一起來深入了解一下。當然光講MVVM顯得有些空洞,因此本文還會附帶上了解經常和它搭配使用的RxJava和Retrofit。

說明:MVVM是一種基於數據驅動的架構思想,在很多平台的開發中都有應用,比如ReactNative、小程式等,RxJava是一套基於操作流的非同步框架,同樣對很多平台都有支援。本文僅討論在Android平台應用開發中的使用,其它平台也相似但可能存在些許差別。

簡介

在搭建MVVM項目之前我們先來簡單了解下MVVM模式,以及和MVP的區別。

MVVM的全稱就是Model、View、ViewModel。View:視圖,對應Activity和xml文件,純粹的ui展示層,不涉及任何的業務流程;Model:數據模型,我們就可以理解成數據提供方;ViewModel:最重要的一層結構,從名字就可以看出,它是連接View和Model的橋樑,它會把數據更新到ui上,也會接受來自ui的交互事件,並處理相應的業務。

說起更新ui的操作,和以前不同的是,它不會獲取到控制項之後調用控制項的方法實現更新ui,而是依賴DataBinding來實現雙向綁定。Databinding 是一種框架,MVVM是一種模式,兩者的概念是不一樣的。DataBinding實現了控制項和數據的綁定關係,是實現MVVM模式的工具,這套機制可以實現ui控制項和數據之間的動態監聽和動態更新。值得慶幸的是,DataBinding框架Google已經幫我們做好了。在Android中我們可以通過一個簡單的配置集成DataBinding,使我們構建Android MVVM 應用程式成為可能。

MVVM和MVP相比不能說哪個好哪個不好,只能說各有特點和優劣,下面簡單列舉一些MVVM的優勢。

數據驅動

為什麼這點放在第一條,因為這是MVVM最大的特點。在傳統開發模式中,必須先處理業務數據,然後根據數據變化,去獲取ui控制項的引用再調用控制項方法更新ui,同時通過監聽ui控制項來獲取用戶輸入。而在MVVM中,數據和業務邏輯處於一個獨立的ViewModel中,ViewModel只要關注數據和業務邏輯,不需要和ui控制項打交道。由數據自動去驅動ui控制項自動更新,ui控制項的改變又會自動回饋到數據,數據成為主導因素。這樣就使得在業務邏輯處理只需要關心數據,方便而且簡單很多。

低耦合度

MVVM模式中,數據是獨立於ui的,ViewModel只負責處理和提供數據,ui想怎麼展示數據都由控制項自己決定,ViewModel 不涉及任何和ui控制項相關的事也不持有ui控制項的引用。即使控制項改變(TextView 換成 EditText),ViewModel也幾乎不需要更改任何程式碼,專註自己的數據處理就可以了,這一點和以前完全不同。如果是MVP遇到ui更改,就可能需要改變獲取控制項的方式,改變更新ui的介面,改變從ui上獲取輸入的程式碼,可能還需要更改訪問控制項對象的屬性程式碼等等,會非常麻煩。因此,MVVM還極大地降低了兩者的耦合度。

更新 UI

在MVVM中,我們可以在工作執行緒中直接修改ViewModel的數據(只要數據是執行緒安全的),剩下的數據綁定框架幫你搞定,很多事情都不需要你去關心。這一點太難得了,我們不需要再寫Handler輔助更新ui。

團隊協作

MVVM的分工是非常明顯的,由於View和ViewModel之間是鬆散耦合的。一個是處理業務和數據,一個是專門的ui展示,完全可以由兩個人分工來做。一個轉做ui(xml 和 Activity)一個轉寫ViewModel業務,效率更高(儘管實際開發我們不會這麼分工,但是MVVM確實可以做到這一點)。

可復用性

一個ViewModel復用到多個View中,同樣的一份數據,用不同的UI去做展示,對於頻繁的版本迭代ui改動,只要更換View層就行。

實戰Demo

在學習一個新東西時不能只看理論,既枯燥也容易忘,所以我們可以寫一個小demo演練一下。

我們一起來寫一個簡單的demo,類似社交軟體。包含四個頁面:用戶資訊、好友列表、聊天記錄、粉絲列表。

之所以設計這四個頁面,是為了練習數據綁定的寫法:

(1)用戶資訊:這個頁面很簡單,主要是演示常規頁面數據綁定的寫法

(2)好友列表:主要演示RecyclerView列表頁面數據綁定的寫法

(3)聊天頁面:主要演示多類型RecyclerView列表Adapter的寫法

(4)粉絲列表:從網路獲取數據,主要演示RxJava+Retrofit的基本使用方法

構建

廢話講了這麼多,下面開始正式搭建MVVM應用。我們先從最簡單的頁面—「用戶資訊」開始。

Gradle

UserInfoActivity

這裡和傳統寫法不太一樣,不是直接調用Activity的setContentView方法,而是調用DataBindingUtil.setContentView。這個方法返回一個ActivityUserInfoBinding對象,第一次接觸DataBinding肯定很奇怪,這個類是哪來的呢?不要慌,這個類是自動編譯生成的,根據layout.xml文件的名字,把下橫線去掉,然後首字母大寫,最後在加上一個Binding。比如我們傳的layout名字叫:activity_user_info,根據規則生成的類名就叫ActivityUserInfoBinding。規則有點複雜,但是就是這麼規定的。

Activity_user_info.xml

看完activity程式碼自然要看看下layout的程式碼。這段程式碼有點長,我們先不要看具體的程式碼,先看結構。

和我們通常的xml布局不一樣,最外層是一個layout節點,然後包含兩個子節點,第一個是data,第二個是LinearLayout。

這個結構是固定的,LinearLayout標籤是我們正常的布局節點,我們可以像以前那樣編寫,此處就不細說了。Data標籤是數據綁定的節點,用於指定該布局文件中需要用到的數據(ViewModel),Type是ViewModel類的全路徑,Name是給ViewModel取的變數名,後面布局中就是用這個name訪問ViewModel的數據。

在這裡例子中,我們頁面中展示的是用戶資訊,對應的ViewModel就是UserInfoViewModel,我們給它取個別名叫UserInfoViewModel,下面在訪問用戶名等就用UserInfoViewModel.name。

UserInfoViewModel

上文終於說到了ViewModel,我們一直說ViewModel是MVVM的核心,下面就來看看ViewModel的程式碼是怎麼寫的。

ViewModel是MVVM里最複雜的一層。首先是類名,有一個泛型<UserInfoActivity>,構造函數傳入實例,這個不是必須的,是我自己在父類定義的。因為我們有可能在ViewModel中需要調用Activity的方法,比如跳轉頁面,所以需要有一個入口獲取Activity實例。

再看成員變數。這些成員變數都是ObservableField類型或者ObservableInt類型,泛型包著的才是layout需要的數據, 為什麼要用Observable包一下呢,因為需要自動更新ui。我們在layout文件中把ViewModel和控制項綁定到了一起,當ViewModel的數據發生改變(比如:name.set("朱小明");)DataBinding就會自動把新的數據更新到ui上。

這麼看ViewModel也不複雜,還有一些在這個簡單的例子沒有體現,比如點擊事件,MVVM的點擊事件是在layout.xml中定義:

看onClick屬性,寫法很奇怪,@{userInfoViewModel::onNameClick},中間是雙冒號,這也是固定寫法,onNameClick是ViewModel中的方法名:

另外在這裡LoadUserInfo方法寫的是假數據,真實項目中這裡肯定是請求網路數據,這在最後粉絲列表頁面中也會講到。

這樣,一個最最簡單的頁面就完成了。

RecyclerView

完成了最簡單的頁面,我們來看看MVVM的列表頁面怎麼實現。首先是layout.xml。

看完第一個例子,再看這個布局就很簡單了,大結構都一樣,只不過這裡的主布局是RecyclerView,綁定的ViewModel是FriendListViewModel,再看下Activity里是怎麼設置RecyclerView的。

FriendListActivity

我們驚訝地發現FriendListBinding居然可以直接訪問RecyclerView對象。其實和前面的setViewModel方法一樣,這也是根據xml中控制項名自動編譯生成的。拿到了RecyclerView實例就可以設置LayoutManager和Decoration,這都沒什麼說的。

(PS:這裡友情提示一下,這些編譯生成的程式碼是在你Coding過程中動態產生的,所以有時候你會遇到你程式碼寫完了,但是它沒有編譯出來的情況,不要驚慌,手動Build一下就好。)

緊接著就是設置Adapter,這裡值得說的是:我在這裡用的是CommonAdapter,那麼它是怎麼實現的呢?

CommonAdapter

用MVVM的方式寫Adapter和以前的寫法是不一樣的,看下完整程式碼:

程式碼有點多,如果僅僅是寫一個普通的Adapter,其實並不需要這麼複雜,我是想讓它通用一點所以加了泛型。仔細看程式碼,和傳統的Adapter在大體結構上是一致的,不同的主要是兩點:

(1)載入布局 onCreateViewHolder()

(2)綁定數據 onBindViewHolder()

載入布局時,我們要用DataBindingUtil.inflate()方法,該方法返回一個ViewDataBinding對象,然後把傳遞給ViewHolder,傳統的寫法ViewHolder持有的是一個View,而這裡是一個ViewDataBinding。

綁定數據時,不同於以前直接給控制項賦值的方式,而是調用了ViewDataBinding的setVariable(mVariableId, itemInfo)和executePendingBindings()方法,這種方式和前面的例子是一樣的,都是把View和ViewModel綁定在一起,只不過這種寫法比較手工。

mVariableId是什麼呢?它其實是xml中申明的ViewModel的ID。比如我們在xml中申明了一個ViewModel,Name叫FriendListViewModel,就會自動在BR類中編譯出一個ID,叫BR.friendViewModel,Activity中在New CommonAdapter時,就是傳遞的這個值。

FriendListViewModel

又到了ViewModel,我們看下列表的ViewModel是怎麼寫的:

這個ViewModel只有一個成員變數,仍然是ObservableField類型,泛型是List<FriendViewModel>,這個FriendViewModel又是什麼呢?它也是一個ViewModel,它是RecyclerView每一個item的ViewModel,item在載入時也和普通布局一樣,也是通過綁定一個ViewModel來載入數據的。

看到這就明白了吧,這就是MVVM的風格,不管是普通頁面、列表頁面、還是列表元素,Layout.xml只管展示,但是Layout.xml必須綁定一個ViewModel,數據都來自ViewModel,ViewModel處理業務邏輯並通過DataBinding更新數據。

這裡就不貼FriendViewModel的程式碼了,聰明的你一定知道該怎麼寫了。我們一起看下運行效果圖:

多類型RecyclerView

按照傳統的寫法,我們要寫一個多類型的Adapter和單類型的Adapter是基本一樣的,只是為不同類型載入不同布局,然後再為不同類型的布局分別賦值。在MVVM中也是如此,只不過我們要為每一種Type匹配一個Layout.xml,再為每一個Layout.xml匹配一個VariableId用於綁定數據。先看下Adapter的全部程式碼:

這麼多程式碼不用全看,就看和之前單類型Adapter的區別,主要有一下三點:

(1)成員變數多了一個mLayoutMapping

(2)onCreateViewHolder()方法根據Type獲取不同的Layout.xml

(3)onBindViewHolder()方法為不同的Layout.xml匹配不同的VariableId,綁定不同的數據

mLayoutMapping保存Type類型和Layout.xml的對應關係,因為在onCreateViewHolder()要根據Type獲取Layout,為什麼不把Layout的ID放在T類型的Bean裡面呢,因為onCreateViewHolder()的參數里只有Type,沒有Position, 我們沒有辦法獲取到每個位置的Bean。而且多類型Adapter的Type和Layout的對應關係本來就不應該和Bean相關,它就是獨立的一組對應關係,所以用SparseIntArray保存起來。

onBindViewHolder()方法中,會為每一個Layout.xml綁定一個VariableId(就是ViewModel的id),而這個VariableId是從每個T類型Bean里取出來的,這裡為什麼放在Bean里,因為這裡有Position參數,我們可以獲取到每個位置的Bean。當然這個VariableId是通過一個方法獲取的,這個方法定義在父類,子類重寫並返回對應的VariableId。

這裡每一條消息的ViewModel是MessageViewModel,它繼承自MultiTypeListItemViewModel,看下程式碼:

MultiTypeListItemViewModel.java

MessageViewModel.java

父類里有一個mType成員變數,還有一個VariableId()抽象方法,在構建子類對象時給mType賦值,並在子類中重寫VariableId()方法,返回每種Type對應的VariableId。

但是我這裡為什麼我只返回固定的VariableId呢?因為我的聊天介面左右Type的Layout所對應的ViewModel 是一樣的,如果在你的項目中是不一樣的,那你就需要返回每種Type對應的VariableId。

這樣我們就完成了多類型的列表頁面:

RxJava+Retrofit網路請求

前面講的三個頁面都是單機遊戲,數據都是本地假數據,最後來看下如果用RxJava+Retrofit進行網路請求,從伺服器請求粉絲列表。

當然這個粉絲不是真的啦,只是我自己搭建的一個本地伺服器,返回一段固定的Json,能起到演示效果就好。

http://99.48.58.51:8080/springMvcDemo/testController/testFansList.do

PS:這裡用到的頭像取自多位Android部落格大神的主頁,向大神致敬!

(PS:RxJava和Retrofit的使用細節不在這篇文章里講,如果想了解詳情可以參考其它部落格https://www.jianshu.com/u/26ef80e64974)

下面開始正式寫程式碼。

Gradle

Service

Retrofit的每一個請求都需要先定義一個Service,這個Service是一個Interface,裡面定義每一個請求的方法。

然後調用RetrofitManager的Create方法創建Service實例。

至於它是怎麼創建實例的先不用管,反正我們拿到了這個Service實例就可以調用它的方法了。

Service的RequestFansList()方法返回的是Observable對象,所以Map函數把它轉換成我們需要的結果數據List<FansBean>。

Compose指定請求網路和結果回調的執行緒。

PreAction主要是為了在請求之前做一些前置操作,比如ShowLoadingView啥的。

Subscribe綁定了訂閱者,下面就來看下這個訂閱這做了哪些事情。

onCompleted()表示事件序列的結束,所以我們需要finishLoadingView。

onError()表示事件序列過程中發生異常,它和onCompleted()是互斥的,只會走其一,我們需要在這個回調中做相應的異常處理,並finishLoadingView。

onNext()就是事件的正常返回了,我們得到請求結果後,轉換成ViewModel的數據,就可以更新ui了。

(PS:這裡必須再次說明下,網路請求這塊我省略了很多零碎的東西,因為這篇文章主講MVVM,不是主講Retrofit,一些關於Retrofit使用、配置的細節沒有一一列出,如果有不清楚的地方還請查閱Retrofit相關文檔。)

@BindingAdapter

文章的最後,必須要說一下

@BindingAdapter,看了前面的程式碼,大家心中可能會有以下一些疑惑:

ImageView

為什麼ImageView設置三個Img參數就能載入圖片?

RecyclerView

為什麼RecyclerView這樣設置Data就可以載入出數據?

答案就是:其實這背後都是@BindingAdapter這個註解在工作。

我們隨便寫一個類,真的是隨便寫,因為類名不重要。然後寫一個靜態方法用於載入圖片。 這個方法上加上一個註解@BindingAdapter({"img:imgurl", "img:placeholder", "img:error"}),註解裡面有三個參數,分別表示圖片url,占點陣圖,錯誤圖。

方法的參數是需要載入數據的控制項,和註解里申明的三個參數,然後在方法內部我們可以用自己喜歡的方式載入圖片,我用的是Glide,你也可以用Picasso。這樣我們就定義好了一個@BindingAdapter。

在Xml中使用時,我們首先要申明命名空間

這個命名空間Img就是在註解里定義的Img,必須保持統一。否則報錯。

然後就用這個命名空間給控制項賦值。

還有一點很重要的是,註解里申明了幾個參數,就必須傳幾個參數否則會報錯。

ImageView的說完了,RecyclerView自然就簡單了。

第一個BindingAdapter是用於單類型RecyclerView,第二個例子就是用的這個。

第二個BindingAdapter是用於多類型RecyclerView,第三個例子用的就是這個。

另外還有一點需要提下,如果一個控制項的某個屬性沒有Set方法,也是需要用這種方式寫的。如果是自定義View的屬性,也是可以用這種方式寫的。

總結

好吧,以上就是本次分享的全部。啰啰嗦嗦講了這麼多,思緒也許整理得不是很好,但是MVVM+RxJava+Retrofit的使用確實也比較複雜,這篇文章算是一個入門。如有錯誤或不當之處歡迎留言指出。

任家亮

享米Android開發工程師,高冷的葬愛家族大公爵。摩羯男一枚,最大的愛好是王者榮耀,看科探片。