使用Kotlin構建更適合Android的MVVM應用程序

簡書地址: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框架程序員

MVC、MVP、MVVM

咱們先大體瞭解下Android開發的建立模式github

MVC

Model:實體模型、數據的獲取、存儲等等web

View:Activity、fragment、view、adapter、xml等等算法

Controller:爲View層處理數據,業務等等數據庫

從這個結構來看,Android自己仍是符合MVC架構的。不過因爲做爲純View的xml功能太弱,以及controller能提供給開發者的做用較小,還不如在Activity頁面直接進行處理,但這麼作卻形成了代碼大爆炸。一個頁面邏輯複雜的頁面動輒上千行,註釋沒寫好的話還十分很差維護,並且難以進行單元測試,因此這更像是一個Model-View的架構,不適用於打造穩定的Android項目。

MVP

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的同行都對此怨聲載道。

MVVM

Model:實體模型、數據的獲取、存儲等等

View:Activity、fragment、view、adapter、xml等等

ViewModel:負責完成View與Model間的交互和業務邏輯,基於DataBinding改變UI

MVVM的目標和思想與MVP相似,但它沒有MVP那使人厭煩的各類回調,利用DataBinding就能夠更新UI和狀態,達到理想的效果。

數據驅動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,並遵循統一的命名規範和調用準則,保證開發時的一致性。

如下是咱們現今的架構:

MVVM

建立文章詳情界面

接下來我將展現一下M-V-VM三層之間如何協做,以文章詳情頁面爲例

V—VM

UI由ArtcileDetailActivity.kt及article_detail_activity.xml組成。

要驅動UI,咱們的數據模型須要持有幾個元素:

  • Article Id:文章詳情的id,用於加載文章詳情
  • title:文章的標題
  • content:文章的內容
  • state:加載狀態,用一個State類來封裝

咱們將建立一個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頁面以後

  1. init()方法->先進行數據的初始化,將viewModel和xml文件進行綁定
  2. loadData()方法->調用viewModel的方法

進入ArticleDetailViewModel

  1. 調用Model層獲取詳情方法獲取數據源
  2. 根據須要使用RxJava操做符對數據進行轉換,經過DataBinding更新UI
  3. 返回可觀測的Single對象給View

回到ArticleDetailActivity頁面

  1. 綁定生命週期,避免內存泄漏
  2. 對返回的可觀測對象進行訂閱
  3. 處理成功和失敗的狀況

至此,V-VM之間如何協做就清楚了。

M—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…中找到

一些建議

建議一:在Activity或Fragment裏處理點擊事件

使用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做爲參數,詳細可參考文章

DataBinding結合AspectJ防止屢次點擊

這是這樣處理點擊事件的緣由之一,另外一個好處是方便綁定生命週期,和進行回調處理(好比一些須要用到activity context的dialog和toast的時候,均可以寫在doOnSubscribe和doAfterTerminate操做符裏),避免了ViewModel層持有context。

建議二:多寫單元測試

單元測試能保證數據和邏輯的正確性,並且語法相對簡單,很容易學習。並且運行一次單元測試的時間簡直毫秒殺運行一次app的時間。

我認爲程序員和普通碼農直接的區別之一即是是否進行單元測試。

並且因爲ViewModel層是純Kotlin/Java代碼,感受就如之前使用Eclipse寫簡單的控制檯程序。

固然單元測試的做用不只限於寫測試代碼,我通常都會在裏面玩玩RxJava的操做符,進行一些算法的練習,驗證數據的輸出是否正確等等。

若是你想學習或瞭解單元測試,能夠查看如下文章:

關於安卓單元測試,你須要知道的一切(by 小創)

使用Kotlin和RxJava測試MVP架構的完整示例

關於DataBinding

不少開發者放棄DataBinding緣由就在於出錯了不容易排查錯誤。 只顯示出不少XXBinding未找到。 若是有必定使用經驗的就知道只看最後一條報錯信息就夠了。 這裏介紹一種我常用來排查錯誤的方式: 在Android Studio 的terminal 裏運行

./gradlew clean assembleDebug

或者

./gradlew compileDebugJavaWithJavac

由於DataBinding是編譯生成代碼的,不少錯誤都是xml中表達式寫的有問題致使的,因此運行以上命令容易在terminal中打印出具體錯誤的信息。這些命令對於須要編譯生成代碼的框架排查錯誤十分有用,好比Dagger2。

更多信息請查閱 DataBinding實用指南

規範

想要在使用DataBinding的過程當中不出錯,遵照統一的規範是必定的

  1. ViewModel—View—XML—Model(儘可能) 應該相互對應,以功能模塊開頭

普通頁面

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

結構以下圖所示:

  1. xml佈局文件中的variable統一命名

    ViewModel Presenter(點擊事件) Item(列表項)
    vm presenter item

參考資料

如何構建Android MVVM 應用框架

App開發架構指南(谷歌官方文檔譯文)

相關文章
相關標籤/搜索