簡書地址: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 |