簡書地址:www.jianshu.com/p/77e42aebd…php
說到MVVM,你們都會想起前端的MVVM框架,相較於前端MVVM的火熱,它在移動開發領域就不那麼熱門了。Google在2015年才推出DataBinding框架,起步較晚,並且2015年是MVP模式爆發的一年,2016年是各類熱修復、插件化爆發的一年,它沒遇上好時機。html
PS:DataBinding和MVVM兩者並不相同。MVVM是一種架構模式,而DataBinding是Android中實現數據與UI綁定的框架,是構建MVVM架構的一個工具,做用相似於加強版的ButterKnife。前端
自16年接觸DataBinding以來,苦於這方面的知識較少,可是Databinding在使用過程當中又十分便捷,因此一直以來都在不停探索怎樣才能構建出合適的MVVM架構程序,在通過幾回的項目重構以後,終於在近期結合Kotlin語言探索出了更適合Android的MVVM架構。android
小專欄 :使用Kotlin構建Android MVVM應用程序 Github示例:github.com/ditclear/Pa…git
咱們先來看看什麼是MVVM,而後再一步一步來闡述整個的MVVM框架程序員
咱們先大體瞭解下Android開發的建立模式github
Model:實體模型、數據的獲取、存儲等等web
View:Activity、fragment、view、adapter、xml等等算法
Controller:爲View層處理數據,業務等等數據庫
從這個結構來看,Android自己仍是符合MVC架構的。不過因爲做爲純View的xml功能太弱,以及controller能提供給開發者的做用較小,還不如在Activity頁面直接進行處理,但這麼作卻形成了代碼大爆炸。一個頁面邏輯複雜的頁面動輒上千行,註釋沒寫好的話還十分很差維護,並且難以進行單元測試,因此這更像是一個Model-View的架構,不適用於打造穩定的Android項目。
Model:實體模型、數據的獲取、存儲等等
View:Activity、fragment、view、adapter、xml等等
Presenter:負責完成View與Model間的交互和業務邏輯,以回調返回結果。
前面說,Activity充當了View和Controller的做用, 形成了代碼爆炸。而MVP架構很好的處理了這個問題。其核心理念是經過一個抽象的View接口(不是真正的View層)將Presenter與真正的View層進行解耦。Persenter持有該View接口,對該接口進行操做,而不是直接操做View層。這樣就能夠把視圖操做和業務邏輯解耦,從而讓Activity成爲真正的View層。
這也是現今比較流行的架構,但是弊端也是有的。若是業務複雜了,也可能致使P層太臃腫,並且V和P層有必定耦合度,若是UI有什麼地方須要更改,那麼P層不僅改一個地方那麼簡單,還須要改View的接口及其實現,牽一髮動全身,運用MVP的同行都對此怨聲載道。
Model:實體模型、數據的獲取、存儲等等
View:Activity、fragment、view、adapter、xml等等
ViewModel:負責完成View與Model間的交互和業務邏輯,基於DataBinding改變UI
MVVM的目標和思想與MVP相似,但它沒有MVP那使人厭煩的各類回調,利用DataBinding就能夠更新UI和狀態,達到理想的效果。
在使用MVC或MVP開發時,咱們若是要更新UI,首先須要找到這個view的引用,而後賦予值,才能進行更新。在MVVM中,這就不須要了。MVVM是經過數據驅動UI的,這些都是自動完成。數據的重要性在MVVM架構中獲得提升,成爲主導因素。在這種架構模式中,開發者重點關注的是怎樣處理數據,保證數據的正確性。
常見的錯誤就是把全部代碼都寫在Activity或者Fragment中。任何跟UI和系統交互無關的事情都不該該放在這些類當中。儘量讓它們保持簡單輕量能夠避免不少生命週期方面的問題。MVVM架構模式下,數據和業務邏輯都處於ViewModel中,ViewModel只關心數據和業務,不須要直接和UI打交道,而Model只須要提供ViewModel的數據源,View則關心如何顯示數據和處理與用戶的交互。
經過以上簡述和與MVC、MVP的對比,咱們能夠發現MVVM仍是頗有優點的,而若是再搭配Kotlin語言的話,能夠說是如虎添翼了。
其實結構已經很清晰了,咱們只須要作M-V-VM層各層應該作的事情,作到關注點分離。
M層 的關注點是怎麼提供數據給ViewModel
ViewModel層 關注點是怎麼處理數據(包括使用DataBinding綁定數據,以及控制loading、empty狀態)
View層的關注點是顯示數據,接收用戶的操做,調用ViewModel中的方法
爲了打造更適合Android的MVVM架構,使用到的技術有AOP、Dagger二、RxJava、Retrofit、Room和Kotlin,並遵循統一的命名規範和調用準則,保證開發時的一致性。
如下是咱們現今的架構:
接下來我將展現一下M-V-VM三層之間如何協做,以文章詳情頁面爲例
UI由ArtcileDetailActivity.kt及article_detail_activity.xml組成。
要驅動UI,咱們的數據模型須要持有幾個元素:
咱們將建立一個ArticleDetailViewModel.kt來保存。
一個ViewModel爲特定的UI組件提供數據,好比fragment 或者 activity,並負責和數據處理的業務邏輯部分通訊,好比調用其它組件加載數據或者轉發用戶的修改。ViewModel並不知道View的存在,也不會受configuration change影響。
如今咱們有了三個文件。
article_detail_activity.xml: 定義頁面的UI
ArticleDetailViewModel.kt: 爲UI準備數據的類
ArtcileDetailActivity.kt: 顯示ViewModel中的數據與響應用戶交互的控制器
下面開始實現(爲了簡單,只顯示了主要部分):
<?xml version="1.0" encoding="utf-8"?> <layout > <data> <import type="android.view.View"/> <variable name="vm" type="io.ditclear.app.viewmodel.ArticleDetailViewModel"/> </data> <android.support.design.widget.CoordinatorLayout> <android.support.design.widget.AppBarLayout> <android.support.design.widget.CollapsingToolbarLayout> <android.support.v7.widget.Toolbar app:title="@{vm.title}"/> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> <android.support.v4.widget.NestedScrollView> <LinearLayout> <ProgressBar android:visibility="@{vm.loading?View.VISIBLE:View.GONE}"/> <WebView android:id="@+id/web_view" app:markdown="@{vm.content}" android:visibility="@{vm.loading?View.GONE:View.VISIBLE}"/> </LinearLayout> </android.support.v4.widget.NestedScrollView> </android.support.design.widget.CoordinatorLayout> </layout> 複製代碼
/** * 頁面描述:ArticleDetailViewModel * @param repo 數據源Model(MVVM 中的M),負責提供ViewModel中須要處理的數據 * Created by ditclear on 2017/11/17. */ class ArticleDetailViewModel @Inject constructor(val repo: ArticleRepository) { //////////////////data////////////// lateinit var articleId:Int val loading=ObservableBoolean(false) val content = ObservableField<String>() val title = ObservableField<String>() //////////////////binding////////////// fun loadArticle():Single<Article> = repo.getArticleDetail(articleId) .async() .doOnSuccess { t: Article? -> t?.let { title.set(it.title) content.set(it.content) } } .doOnSubscribe { startLoad()} .doAfterTerminate { stopLoad() } fun startLoad()=loading.set(true) fun stopLoad()=loading.set(false) } 複製代碼
/** * 頁面描述:ArticleDetailActivity,處理和用戶的交互(點擊事件),以及處理 * viewModel層回調的數據,附加一些顯示Loading,空狀態和綁定生命週期等等的操做 * Created by ditclear on 2017/11/17. */ class ArticleDetailActivity : BaseActivity<ArticleDetailActivityBinding>() { override fun getLayoutId(): Int = R.layout.article_detail_activity @Inject lateinit var viewModel: ArticleDetailViewModel //init override fun initView() { //統一都是KEY_DATA,別本身瞎命名 val articleID: Int? = intent?.extras?.getInt(Constants.KEY_DATA) if (articleID == null) { toast("文章不存在", ToastType.WARNING) finish() } getComponent().inject(this) mBinding.vm = viewModel.apply { this.articleID = articleID } } //加載數據 override fun loadData() { viewModel.loadData() .compose(bindToLifecycle()) // .doOnSubcribe{ showLoadingDialog() } // .doAfterTerminate{ hideLoadingDialog() } .subscribe({},{ dispatchFailure(it) }) } } 複製代碼
他們是如何工做的呢?
在進入到ArticleDetailActivity
頁面以後
進入ArticleDetailViewModel
回到ArticleDetailActivity
頁面
至此,V-VM之間如何協做就清楚了。
如今咱們把View和ViewModel聯繫了起來,可是ViewModel該如何獲取數據呢?
咱們使用Retrofit來從後端獲取網絡數據。
interface ArticleService{ //文章詳情 @GET("article_detail.php") fun getArticleDetail(@Query("id") id: Int): Single<Article> } 複製代碼
使用Room數據庫來進行持久化
@Dao interface ArticleDao{ @Query("SELECT * FROM Articles WHERE articleid= :id") fun getArticleById(id:Int):Single<Article> @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertArticle(article :Article) } 複製代碼
而後使用ArticleRepository.kt對網絡和本地操做進行一層封裝
/** * 頁面描述:ArticleRepository * 提供數據給ViewModel層 , 處理網絡數據和本地緩存之間的關係 * Created by ditclear on 2017/11/17. */ class ArticleRepository @Inject constructor (private val remote: ArticleService, private val local: ArticleDao) { /* 文章詳情 * 先查看本地是否有緩存,若是沒有那麼再去請求網絡,成功後更新本地緩存 */ fun getArticle(articleId: Int): Single<Article> = local.getArticleById(articleId).onErrorResumeNext { if (it is EmptyResultSetException) { remote.getArticleDetail(articleId).doOnSuccess { t -> t?.let { local.insertArticle(it) } } } else throw it } } 複製代碼
先查看本地是否有緩存,若是沒有那麼再去請求網絡,成功後更新本地緩存。
封裝成Repository的緣由是ViewModel不須要知道它的數據具體是從哪來的,這不是ViewModel這一層須要關心的事情。
即便你的項目沒有進行數據緩存,老是從網絡拉取數據,也建議封裝成Repository,這意味着你的網絡層是能夠替換的,意義有點相似於封裝一個ImageLoadUtil。
整體的流程就這麼多,其實弄懂就很簡單了。關鍵點是各層之間職責明確,以及解耦(Dagger2)和使用DataBinding時須要一個統一的規範。
而再細分,優化,也就是進行模塊化、組件化的工做,深刻些的插件化、熱修復等等。不過萬丈高樓平地起,咱們的地基打的嚴實,之後的工做纔會相對容易。
本文的代碼均可以在github.com/ditclear/Pa…中找到
使用Presenter來繼承View.OnClickListener
interface Presenter:View.OnClickListener{ override fun onClick(v: View?) } 複製代碼
而後在BaseActivity/BaseFragment裏實現它
abstract class BaseActivity<VB : ViewDataBinding> : RxAppCompatActivity(),Presenter{ } 複製代碼
這樣當咱們要設置點擊事件時,只須要
class ArticleDetailActivity : BaseActivity<ArticleDetailActivityBinding>() { //... //init override fun initView() { mBinding.let{ it.vm=mViewModel it.presenter=this } } } 複製代碼
在xml中使用時,則統一使用presenter.onClick(view)
方法
<layout> <data> <variable name="presenter" type="com.ditclear.paonet.view.helper.presenter.Presenter"/> </data> <android.support.design.widget.CoordinatorLayout> <android.support.design.widget.FloatingActionButton android:id="@+id/stow_fab" android:onClick="@{(v)->presenter.onClick(v)}" /> </android.support.design.widget.CoordinatorLayout> </layout> 複製代碼
真正處理則放在相應的Activity/Fragment裏
class ArticleDetailActivity : BaseActivity<ArticleDetailActivityBinding>() { //... @SingleClick override fun onClick(v: View?) { when (v?.id) { R.id.stow_fab -> stow() //more .. R.id.other_action -> other() } } //其它 private fun stow() { } //收藏 private fun stow() { viewModel.stow().compose(bindToLifecycle()) .subscribe({ toastSuccess(it?.message?:"收藏成功") } , { toastFailure(it) } }) } } 複製代碼
@SingleClick是一個註解,做爲AspectJ的切面,來防止屢次點擊,須要將view做爲參數,詳細可參考文章
這是這樣處理點擊事件的緣由之一,另外一個好處是方便綁定生命週期,和進行回調處理(好比一些須要用到activity context的dialog和toast的時候,均可以寫在doOnSubscribe和doAfterTerminate操做符裏),避免了ViewModel層持有context。
單元測試能保證數據和邏輯的正確性,並且語法相對簡單,很容易學習。並且運行一次單元測試的時間簡直毫秒殺運行一次app的時間。
我認爲程序員和普通碼農直接的區別之一即是是否進行單元測試。
並且因爲ViewModel層是純Kotlin/Java代碼,感受就如之前使用Eclipse寫簡單的控制檯程序。
固然單元測試的做用不只限於寫測試代碼,我通常都會在裏面玩玩RxJava的操做符,進行一些算法的練習,驗證數據的輸出是否正確等等。
若是你想學習或瞭解單元測試,能夠查看如下文章:
不少開發者放棄DataBinding緣由就在於出錯了不容易排查錯誤。 只顯示出不少XXBinding未找到。 若是有必定使用經驗的就知道只看最後一條報錯信息就夠了。 這裏介紹一種我常用來排查錯誤的方式: 在Android Studio 的terminal 裏運行
./gradlew clean assembleDebug
或者
./gradlew compileDebugJavaWithJavac
由於DataBinding是編譯生成代碼的,不少錯誤都是xml中表達式寫的有問題致使的,因此運行以上命令容易在terminal中打印出具體錯誤的信息。這些命令對於須要編譯生成代碼的框架排查錯誤十分有用,好比Dagger2。
更多信息請查閱 DataBinding實用指南
想要在使用DataBinding的過程當中不出錯,遵照統一的規範是必定的
普通頁面
ViewModel | View | XML |
---|---|---|
ArticleDetailViewModel.kt | ArticleDetailActivity.kt | article_detail_activity.xml |
列表頁面 :請參考文章 告別反覆、冗餘的自定義Adapter
ViewModel | View | XML |
---|---|---|
ArticleListViewModel.kt | ArticleListActivity.kt | article_list_activity.xml |
Item ViewModel | Item XML | |
ArticleItemViewModel.kt | article_list_item.xml |
Model 層命名
Remote | Local | Repository |
---|---|---|
ArticleService.kt | ArticleDao.kt | ArticleRepository.kt |
結構以下圖所示:
xml佈局文件中的variable統一命名
ViewModel | Presenter(點擊事件) | Item(列表項) |
---|---|---|
vm | presenter | item |