天天看點

Android MVVM 應用架構建構過程詳解

說到android mvvm,相信大家都會想到google 2015年推出的databinding架構。然而兩者的概念是不一樣的,不能混為一談。mvvm是一種架構模式,而databinding是一個實作資料和ui綁定的架構,是建構mvvm模式的一個工具。

之前看過很多關于android

mvvm的部落格,但大多數提到的都是databinding的基本用法,很少有文章仔細講解在android中是如何通過databinding去建構mvvm的應用架構的。view、viewmodel、model每一層的職責如何?它們之間聯系怎樣、分工如何、代碼應該如何設計?這是我寫這篇文章的初衷。

接下來,我們先來看看什麼是mvvm,然後再一步一步來設計整個mvvm架構。

<a></a>

首先,我們先大緻了解下android開發中常見的模式。

mvc

view:xml布局檔案。

model:實體模型(資料的擷取、存儲、資料狀态變化)。

controllor:對應于activity,處理資料、業務和ui。

mvp

view: 對應于activity和xml,負責view的繪制以及與使用者的互動。

model: 依然是實體模型。

presenter: 負責完成view與model間的互動和業務邏輯。

前面我們說,activity充當了view和controller兩個角色,mvp就能很好地解決這個問題,其核心理念是通過一個抽象的view接口(不是真正的view層)将presenter與真正的view層進行解耦。persenter持有該view接口,對該接口進行操作,而不是直接操作view層。這樣就可以把視圖操作和業務邏輯解耦,進而讓activity成為真正的view層。

但mvp也存在一些弊端:

presenter(以下簡稱p)層與view(以下簡稱v)層是通過接口進行互動的,接口粒度不好控制。粒度太小,就會存在大量接口的情況,使代碼太過碎版化;粒度太大,解耦效果不好。同時對于ui的輸入和資料的變化,需要手動調用v層或者p層相關的接口,相對來說缺乏自動性、監聽性。如果資料的變化能自動響應到ui、ui的輸入能自動更新到資料,那該多好!

mvp是以ui為驅動的模型,更新ui都需要保證能擷取到控件的引用,同時更新ui的時候要考慮目前是否是ui線程,也要考慮activity的生命周期(是否已經銷毀等)。

mvp是以ui和事件為驅動的傳統模型,資料都是被動地通過ui控件做展示,但是由于資料的時變性,我們更希望資料能轉被動為主動,希望資料能更有活性,由資料來驅動ui。

v層與p層還是有一定的耦合度。一旦v層某個ui元素更改,那麼對應的接口就必須得改,資料如何映射到ui上、事件監聽接口這些都需要轉變,牽一發而動全身。如果這一層也能解耦就更好了。

複雜的業務同時也可能會導緻p層太大,代碼臃腫的問題依然不能解決。

mvvm

view: 對應于activity和xml,負責view的繪制以及與使用者互動。

model: 實體模型。

viewmodel: 負責完成view與model間的互動,負責業務邏輯。

mvvm的目标和思想與mvp類似,利用資料綁定(data binding)、依賴屬性(dependency property)、指令(command)、路由事件(routed event)等新特性,打造了一個更加靈活高效的架構。

在正常的開發模式中,資料變化需要更新ui的時候,需要先擷取ui控件的引用,然後再更新ui。擷取使用者的輸入和操作也需要通過ui控件的引用。在mvvm中,這些都是通過資料驅動來自動完成的,資料變化後會自動更新ui,ui的改變也能自動回報到資料層,資料成為主導因素。這樣mvvm層在業務邏輯進行中隻要關心資料,不需要直接和ui打交道,在業務處理過程中簡單友善很多。

mvvm模式中,資料是獨立于ui的。

資料和業務邏輯處于一個獨立的viewmodel中,viewmodel隻需要關注資料和業務邏輯,不需要和ui或者控件打交道。ui想怎麼處理資料都由ui自己決定,viewmodel不涉及任何和ui相關的事,也不持有ui控件的引用。即便是控件改變了(比如:textview換成edittext),viewmodel也幾乎不需要更改任何代碼。它非常完美的解耦了view層和viewmodel,解決了上面我們所說的mvp的痛點。

更新ui

在mvvm中,資料發生變化後,我們在工作線程直接修改(在資料是線程安全的情況下)viewmodel的資料即可,不用再考慮要切到主線程更新ui了,這些事情相關架構都幫我們做了。

團隊協作

mvvm的分工是非常明顯的,由于view和viewmodel之間是松散耦合的:一個是處理業務和資料、一個是專門的ui處理。是以,完全由兩個人分工來做,一個做ui(xml和activity)一個寫viewmodel,效率更高。

可複用性

一個viewmodel可以複用到多個view中。同樣的一份資料,可以提供給不同的ui去做展示。對于版本疊代中頻繁的ui改動,更新或新增一套view即可。如果想在ui上做a/b testing,那mvvm是你不二選擇。

單元測試

有些同學一看到單元測試,可能腦袋都大。是啊,寫成一團漿糊的代碼怎麼可能做單元測試?如果你們以代碼太爛無法寫單元測試而逃避,那可真是不好的消息了。這時候,你需要mvvm來拯救。

我們前面說過了,viewmodel層做的事是資料處理和業務邏輯,view層中關注的是ui,兩者完全沒有依賴。不管是ui的單元測試還是業務邏輯的單元測試,都是低耦合的。在mvvm中資料是直接綁定到ui控件上的(部分資料是可以直接反映出ui上的内容),那麼我們就可以直接通過修改綁定的資料源來間接做一些android

ui上的測試。

通過上面的簡述以及模式的對比,我們可以發現mvvm的優勢還是非常明顯的。雖然目前android開發中可能真正在使用mvvm的很少,但是值得我們去做一些探讨和調研。

如何分工

建構mvvm架構首先要具體了解各個子產品的分工。接下來我們來講解view、viewmodel、model它們各自的職責所在。

view

viewmodel

viewmodel層做的事情剛好和view層相反,viewmodel隻做和業務邏輯和業務資料相關的事,不做任何和ui相關的事情,viewmodel

層不會持有任何控件的引用,更不會在viewmodel中通過ui控件的引用去做更新ui的事情。viewmodel就是專注于業務的邏輯處理,做的事情也都隻是對資料的操作(這些資料綁定在相應的控件上會自動去更改ui)。同時databinding架構已經支援雙向綁定,讓我們可以通過雙向綁定擷取view層回報給viewmodel層的資料,并對這些資料上進行操作。關于對ui控件事件的處理,我們也希望能把這些事件處理綁定到控件上,并把這些事件的處理統一化,為此我們通過bindingadapter對一些常用的事件做了封裝,把一個個事件封裝成一個個command,對于每個事件我們用一個replycommand去處理就行了,replycommand會把你可能需要的資料帶給你,這使得我們在viewmodel層處理事件的時候隻需要關心處理資料就行了,具體見mvvm

light toolkit 使用指南的 command 部分。再強調一遍:viewmodel 不做和ui相關的事。

model

model層最大的特點是被賦予了資料擷取的職責,與我們平常model層隻定義實體對象的行為截然不同。執行個體中,資料的擷取、存儲、資料狀态變化都是model層的任務。model包括實體模型(bean)、retrofit的service

,擷取網絡資料接口,本地存儲(增删改查)接口,資料變化監聽等。model提供資料擷取接口供viewmodel調用,經資料轉換和操作并最終映射綁定到view層某個ui元素的屬性上。

關于協作,我們先來看下面的一張圖:

Android MVVM 應用架構建構過程詳解

上圖反映了mvvm架構中各個子產品的聯系和資料流的走向,我們從每個子產品一一拆分來看。那麼我們重點就是下面的三個協作。

viewmodel與view的協作。

viewmodel與model的協作。

viewmodel與viewmodel的協作。

viewmodel與view的協作

Android MVVM 應用架構建構過程詳解

圖2中viewmodel和view是通過綁定的方式連接配接在一起的,綁定分成兩種:一種是資料綁定,一種是指令綁定。資料的綁定databinding已經提供好了,簡單地定義一些observablefield就能把資料和控件綁定在一起了(如textview的text屬性),但是databinding架構提供的不夠全面,比如說如何讓一個url綁定到一個imageview,讓這個imageview能自動去加載url指定的圖檔,如何把資料源和布局模闆綁定到一個listview,讓listview可以不需要去寫adapter和viewholder相關的東西?這些就需要我們做一些工作和簡單的封裝。mvvm

light toolkit 已經幫我們做了一部分的工作,詳情可以檢視mvvm light toolkit

使用指南。關于事件綁定也是一樣,mvvm light toolkit

做了簡單的封裝,對于每個事件我們用一個replycommand去處理就行了,replycommand會把可能需要的資料帶給你,這樣我們處理事件的時候也隻關心處理資料就行了。

由圖1中viewmodel的子產品中我們可以看出viewmodel類下面一般包含下面5個部分:

context (上下文)

model (資料源 java bean)

data field (資料綁定)

command (指令綁定)

child viewmodel (子viewmodel)

我們先來看下示例代碼,然後再一一講解5個部分是幹嘛用的:

context是幹嘛用的呢,為什麼每個viewmodel都最好需要持了一個context的引用呢?viewmodel不處理和ui相關的事也不操作控件,更不更新ui,那為什麼要有context呢?原因主要有以下兩點:

通過圖1中,然後得到一個observable,其實這就是網絡請求部分。其實這就是網絡請求部分,做網絡請求我們必須把retrofit

service傳回的observable綁定到context的生命周期上,防止在請求回來時activity已經銷毀等異常,其實這個context的目的就是把網絡請求綁定到目前頁面的生命周期中。

在圖1中,我們可以看到兩個viewmodel之間的聯系是通過messenger來做,這個messenger是需要用到context,這個我們後續會講解。

當然,除此以外,調用工具類、幫助類有時候需要context做為參數等也是原因之一。

model (資料源)

model是什麼呢?其實就是資料源,可以簡單了解是我們用json轉過來的bean。viewmodel要把資料映射到ui中可能需要大量對model的資料拷貝和操作,拿model的字段去生成對應的observablefield然後綁定到ui(我們不會直接拿model的資料去做綁定展示),這裡是有必要在一個viewmodel保留原始的model引用,這對于我們是非常有用的,因為可能使用者的某些操作和輸入需要我們去改變資料源,可能我們需要把一個bean在清單頁點選後傳給詳情頁,可能我們需要把這個model當做表單送出到伺服器。這些都需要我們的viewmodel持有相應的model(資料源)。

data field(資料綁定)

data field就是需要綁定到控件上的observablefield字段,這是viewmodel的必需品,這個沒有什麼好說。但是這邊有一個建議:

這些字段是可以稍微做一下分類和包裹的。比如說可能一些字段是綁定到控件的一些style屬性上(如長度、顔色、大小),對于這類針對view

style的的字段可以聲明一個viewstyle類包裹起來,這樣整個代碼邏輯會更清晰一些,不然viewmodel裡面可能字段泛濫,不易管理和閱讀性較差。而對于其他一些字段,比如說title、imageurl、name這些屬于資料源類型的字段,這些字段也叫資料字段,是和業務資料和邏輯息息相關的,這些字段可以放在一塊。

command(指令綁定)

command(指令綁定)簡言之就是對事件的處理(下拉重新整理、加載更多、點選、滑動等事件處理)。我們之前處理事件是拿到ui控件的引用,然後設定listener,這些listener其實就是command。但是考慮到在一個viewmodel寫各種listener并不美觀,可能實作一個listener就需要實作多個方法,但是我們可能隻想要其中一個有用的方法實作就好了。更重要一點是實作一個listener可能需要寫一些ui邏輯才能最終擷取我們想要的。簡單舉個例子,比如你想要監聽listview滑到最底部然後觸發加載更多的事件,這時候就要在viewmodel裡面寫一個onscrolllistener,然後在裡面的onscroll方法中做計算,計算什麼時候listview滑動底部了。其實viewmodel的工作并不想去處理這些事件,它專注做的應該是業務邏輯和資料處理,如果有一個東西不需要你自己去計算是否滑到底部,而是在滑動底部自動觸發一個command,同時把目前清單的總共的item數量傳回給你,友善你通過

page=itemcount/limit+1去計算出應該請求伺服器哪一頁的資料那該多好啊。mvvm light toolkit

幫你實作了這一點:

接着在xml布局檔案中通過bind:onloadmorecommand綁定上去就行了。

具體想了解更多請檢視 mvvm light toolkit

使用指南,裡面有比較詳細地講解command的使用。當然command并不是必須的,你完全可以依照自己的習慣和喜好在viewmodel寫listener,不過使用command可以使viewmodel更簡潔易讀。你也可以自己定義更多的、其他功能的command,那麼viewmodel的事件處理都是托管replycommand來處理,這樣的代碼看起來會比較美觀和清晰。command隻是對ui事件的一層隔離ui層的封裝,在事件觸發時把viewmodel層可能需要的資料傳給viewmodel層,對事件的處理做了統一化,是否使用的話,還是看你個人喜好了。

child viewmodel(子viewmodel)

子viewmodel的概念就是在viewmodel裡面嵌套其他的viewmodel,這種場景還是很常見的。比如說你一個activity裡面有兩個fragment,viewmodel是以業務劃分的,兩個fragment做的業務不一樣,自然是由兩個viewmodel來處理,這時候activity對應的viewmodel裡面可能包含了兩個fragment各自的viewmodel,這就是嵌套的子viewmodel。還有另外一種就是對于adapterview,如listview

它們的每個item其實就對應于一個viewmodel,然後在目前的viewmodel通過observablelist持有引用(如上述代碼),這也是很常見的嵌套的子viewmodel。我們其實還建議,如果一個頁面業務非常複雜,不要把所有邏輯都寫在一個viewmodel,可以把頁面做業務劃分,把不同的業務放到不同的viewmodel,然後整合到一個總的viewmodel,這樣做起來可以使我們的代碼業務清晰、簡短意赅,也友善後人的維護。

總的來說,viewmodel和view之前僅僅隻有綁定的關系,view層需要的屬性和事件處理都是在xml裡面綁定好了,viewmodel層不會去操作ui,隻是根據業務要求處理資料,這些資料自動映射到view層控件的屬性上。

關于viewmodel類中包含哪些子產品和字段,這個需要開發者自己去衡量,我們建議viewmodel不要引入太多的成員變量,成員變量最好隻有上面的提到的5種(context、model……),能不引入其他類型的變量就盡量不要引進來,太多的成員變量對于整個代碼結構破壞很大,後面維護的人要時刻關心成員變量什麼時候被初始化、什麼時候被清掉、什麼時候被指派或者改變,一個細節不小心可能就出現潛在的bug。太多不清晰定義的成員變量又沒有注釋的代碼是很難維護的。

另外,我們會把ui控件的屬性和事件都通過xml(如bind:text=@{…})綁定。如果一個業務邏輯要彈一個dialog,但是你又不想在viewmodel裡面做彈窗的事(viewmodel不希望做ui相關的事)或者說改變actionbar上面的圖示的顔色,改變actionbar按鈕是否可點選,這些都不是寫在xml裡面(都是用java代碼初始化的),如何對這些控件的屬性做綁定呢?我們先來看下代碼:

簡單地說你可以對任意的observablefield做監聽,然後根據資料的變化做相應ui的改變,業務層viewmodel隻要根據業務處理資料就行,以資料來驅動ui。

viewmodel與model的協作

從圖1中,viewmodel通過傳參數到model層擷取網絡資料(資料庫同理),然後把model的部分資料映射到viewmodel的一些字段(observablefield),并在viewmodel保留這個model的引用,我們來看下這一塊的大緻代碼(代碼涉及簡單的rxjava,如看不懂可以查閱入門一下):

注1:我們推薦mvvm和rxjava一塊兒使用,雖然兩者皆有觀察者模式的概念,但是rxjava不使用在針對view的監聽,更多是業務資料流的轉換和處理。databinding架構其實是專用于view-viewmodel的動态綁定的,它使得我們的viewmodel隻需要關注資料,而rxjava提供的強大資料流轉換函數剛好可以用來處理viewmodel中的種種資料,得到很好的用武之地,同時加上lambda表達式結合的鍊式程式設計,使viewmodel的代碼非常簡潔同時易讀易懂。

注2:因為本文樣例model層隻涉及到網絡資料的擷取,并沒有資料庫、存儲、資料狀态變化等其他業務,是以本文涉及的源碼并沒有單獨把model層抽出來,我們是建議把model層單獨抽出來放一個類中,然後以面向接口程式設計方式提供外界擷取和存儲資料的接口。

viewmodel與viewmodel的協作

在圖1中我們看到兩個viewmodel之間用一條虛線連接配接着,中間寫着messenger。messenger可以了解是一個全局消息通道,引入messenger最主要的目的是實作viewmodel和viewmodel的通信,雖然也可以用于view和viewmodel的通信,但并不推薦。viewmodel主要是用來處理業務和資料的,每個viewmodel都有相應的業務職責,但是在業務複雜的情況下,可能存在交叉業務,這時候就需要viewmodel和viewmodel交換資料和通信,這時候一個全局的消息通道就很重要的。

關于messenger的詳細使用方法可以參照 mvvm light toolkit 使用指南的 messenger

部分。這裡給出一個簡單的例子僅供參考:場景是這樣的,你的mainactivity對應一個mainviewmodel,mainactivity

裡面除了自己的内容還包含一個fragment,這個fragment

的業務處理對應于一個fragmentviewmodel,fragmentviewmodel請求伺服器并擷取資料。剛好這個資料mainviewmodel也需要用到,我們不可能在mainviewmodel重新請求資料,這樣不太合理,這時候就需要把資料傳給mainviewmodel,那應該怎麼傳呢,如果彼此沒有引用或者回調?那麼隻能通過全局的消息通道messenger。

fragmentviewmodel擷取消息後通知mainviewmodel并把資料傳給它:

mainviewmodel接收消息并處理:

在mainactivity ondestroy取消注冊就行了(不然導緻記憶體洩露):

上面的例子隻是簡單地說明,messenger可以用在很多場景,通知、廣播都可以,不一定要傳資料,在一定條件下也可以用在view層和viewmodel上的通信和廣播,運用範圍特别廣,需要開發者結合實際的業務中去做更深層次的挖掘。

本文主要講解了一些個人開發過程中總結的android

mvvm建構思想,更多是理論上各個子產品如何分工、代碼如何設計。雖然現在業界使用android

mvvm模式開發還比較少,但是随着databinding 1.0的釋出,相信在android mvvm

這一領域會更多的人來嘗試。剛好我最近用mvvm開發了一段時間,有點心得,寫出來僅供參考。

本文和源碼都沒有涉及到單元測試,如果需要寫單元測試,可以結合google開源的mvp架構添加contract類實作面向接口程式設計,可以幫助你更好地編寫單測。同時mvp和mvvm并沒孰好孰壞,适合業務、适合自己的才是最有價值的,建議結合google開源的mvp架構和本文介紹的mvvm相關的知識去探索适合自己業務發展的架構。

mvvm light toolkit隻是一個工具庫,主要目的是更快捷友善地建構android

mvvm應用程式,在裡面添加了一些控件額外屬性和做了一些事件的封裝,同時引進了全局消息通道messenger,個人覺得用起來會比較友善,你也可以嘗試一下。當然這個庫還有不少地方需要完善和優化,後續也會持續做更新和優化,如果不能達到你的業務需求時,可以clone下來自己做一些相關的擴充。如果想更深入了解mvvm

light toolkit,請看我這篇博文 《mvvm light toolkit 使用指南》。

項目的源碼位址 https://github.com/kelin-hong/mvvmlight 。其中:

library是mvvm light toolkit的源碼,源碼很簡單,感興趣的同學可以看看,沒什麼技術難度,可以根據自己的需求,添加更多的控件屬性和事件綁定。

sample是一個實作知乎日報首頁樣式的demo,本文的代碼示例均出自這個demo。代碼包含了一大部分mvvm light

toolkit的使用場景(data、command、messenger均有涉及),同時sample嚴格按照本博文闡述的mvvm設計思想開發,對了解本文會有比較大的幫助。

本文和源碼涉及rxjava+retrofit+lambda如有不懂或沒接觸過,花點時間入門一下,用到的都是比較簡單的東西。

來源:51cto