使用Kotlin構建MVVM應用程序—第二部分:Retrofit及RxJava

簡書地址:www.jianshu.com/p/8993b2479…php

目錄

寫在前面

這是使用Kotlin開發MVVM應用程序的第二部分—Retrofit及RxJavajava

在前一部分中咱們簡單瞭解了MVVM的基本概念和寫法。若是你沒有看過上一篇,請先快速瀏覽一遍,由於本系列是按部就班的。能夠在這裏查看使用Kotlin構建MVVM應用程序—第一部分:入門篇react

若是第一篇是入了門,那這一篇就有點實戰的意思了,更加貼近咱們具體的需求,本文將闡述如何在MVVM中處理網絡數據。android

Retrofit及RxJava

咱們先加入依賴git

//rx android
    compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
    compile 'io.reactivex.rxjava2:rxjava:2.1.3'
    //retrofit
    compile 'com.squareup.retrofit2:retrofit:2.3.0'
    compile 'com.squareup.retrofit2:converter-gson:2.3.0'
    compile 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
    compile 'com.google.code.gson:gson:2.8.0'
複製代碼

此次相比上一篇稍微加了點難度,此次加入了網絡請求庫Retrofit和Rxjava。github

Retrofit是如今主流的網絡請求庫,不瞭解的看官網web

RxJava是一個在 Java VM 上使用可觀測的序列來組成異步的、基於事件的程序的庫。不瞭解的固然是推薦經久不衰的給 Android 開發者的 RxJava 詳解數據庫

準備工做作好後,先看看如今的MVVM結構api

MVVM

此次咱們的Model層的數據源來自網絡,而且在ViewModel中使用RxJava進行數據的轉換。bash

開始正文

在MVVM中是怎麼處理網絡數據的?

帶着問題看文章是個好習慣。—ditclear

此次咱們先來看看xml佈局文件

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">

    <data>
        <!--須要的viewModel,經過mBinding.vm=mViewMode注入-->
        <variable
                name="vm"
                type="io.ditclear.app.viewmodel.PaoViewModel"/>
    </data>
        <!--省略-->
        <Button
                 <!--省略-->
                android:onClick="@{()->vm.loadArticle()}"
                android:text="load article"/>
        <TextView
                 <!--省略-->
                android:text="@{vm.articleDetail}"
                tools:text="點擊按鈕,調用ViewModel中的loadArticle方法,經過DataBinding更新UI"/>
</layout>
複製代碼

要作的和上一篇差很少,只是如今多了網絡請求。

看看如今的項目結構:

結構

相比上一篇,多了一個PaoService.kt

interface PaoService{
    //文章詳情
    @GET("article_detail.php")
    fun getArticleDetail(@Query("id") id: Int): Single<Article>
}
複製代碼

爲了簡單起見,就只有一個加載文章詳情的接口getArticleDetail

簡單示例

咱們如今使用PaoService做爲咱們的數據源(Model層),提供數據給咱們的PaoViewModel(ViewModel層)

class PaoViewModel(val remote: PaoService) {
    //////////////////data//////////////
    val articleDetail = ObservableField<String>()
    //////////////////binding//////////////
    fun loadArticle() {
        //爲了簡單起見這裏先寫個默認的id
        remote.getArticleDetail(8773)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({ t: Article? ->
                    articleDetail.set(t?.toString())
                }, { t: Throwable? ->
                    articleDetail.set(t?.message ?: "error")
                })
    }
}
複製代碼

和上一篇對比來看相差也不大,只是如今咱們的數據來自網絡。 再來看看PaoActivity.kt(View層)

class PaoActivity : AppCompatActivity() {

    lateinit var mBinding : PaoActivityBinding
    lateinit var mViewMode : PaoViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding=DataBindingUtil.setContentView(this,R.layout.pao_activity)

        //////model
        val remote=Retrofit.Builder()
                .baseUrl("http://api.jcodecraeer.com/")
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .build().create(PaoService::class.java)

        /////ViewModel
        mViewMode= PaoViewModel(remote)
        ////binding
        mBinding.vm=mViewMode
    }
}
複製代碼

基本如出一轍,只是改變了數據源而已。

看一下效果:

好的,目的達到了。你能夠在這裏查看變動

github.com/ditclear/MV…

優化

爲了更有說服力,我優化了一下UI,並加入loading的效果。

還算有點模樣,那麼如今就到了本篇的重點了,怎麼像這樣處理返回的網絡數據?

再來看看xml佈局文件,因爲篇幅緣由,因此這裏只截取主要部分,詳細請查看pao_activity.xml

<!--省略-->
                <us.feras.mdv.MarkdownView android:id="@+id/web_view" android:layout_width="match_parent" android:layout_height="wrap_content" app:markdown="@{vm.content}" android:visibility="@{vm.loading?View.GONE:View.VISIBLE}"/>
<!--省略-->
複製代碼

這裏使用到了一個第三方庫MarkdownView,使用方法是這樣的

markdownView.loadMarkdown("## Hello Markdown");

並無提供setMarkDown(markdown:String)方法,相信這種狀況很常見,常常須要咱們改動第三方庫達到項目的需求。因此這裏須要自定義一下BindingAdapter,具體見NormalBinds.kt

@BindingAdapter(value = "markdown")
fun bindMarkDown(v: MarkdownView, markdown: String?) {
    markdown?.let {
        v.setMarkdown(it)
    }
}
複製代碼

眼尖的同窗就發現了,這不是有setMarkdown方法嗎?

別急,其實這只是使用kotlinMarkdownView添加的擴展函數,具體見NormalExtens.kt

fun MarkdownView.setMarkdown(markdown : String?){
    loadMarkdown(markdown)
}
複製代碼

再來瞧瞧咱們的PaoActivity.kt,依然撿重點

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
    menu?.let {
        menuInflater.inflate(R.menu.detail_menu,it)
    }
    return super.onCreateOptionsMenu(menu)
}

override fun onOptionsItemSelected(item: MenuItem?): Boolean {
    item?.let {
        when(it.itemId){
            R.id.action_refresh -> mViewMode.loadArticle()
        }
    }
    return super.onOptionsItemSelected(item)
}
複製代碼

啊?這叫重點嗎?不就是操做一下menu菜單嗎?

嗯,不錯,很日常的操做,關鍵在於R.id.action_refresh -> mViewMode.loadArticle()這裏。

因爲許多沒法預料的緣由,不可避免的咱們沒法在xml文件中去綁定數據和事件,須要在Activity/Fragment調用viewmodel裏的方法。爲何要提這一點呢?固然是後面須要用到。

再來瞧瞧咱們的PaoViewModel.kt

class PaoViewModel(val remote: PaoService) {
    //////////////////data//////////////
    val loading=ObservableBoolean(false)//加載
    val content = ObservableField<String>()//內容
    val title = ObservableField<String>()//標題
    //////////////////binding//////////////
    fun loadArticle() {
        //爲了簡單起見這裏先寫個默認的id
        remote.getArticleDetail(8773)
                .subscribeOn(Schedulers.io())
                .delay(1000,TimeUnit.MILLISECONDS)//爲了加載效果更明顯,這裏延遲1秒
                .observeOn(AndroidSchedulers.mainThread())
                .doOnSubscribe { loading.set(true) }//開始請求數據,設置加載爲true
                .doAfterTerminate { loading.set(false) }//請求完成,設置加載爲false
                .subscribe({ t: Article? ->
                    t?.let {
                        title.set(it.title)
                        it.content?.let {
                            val articleContent=Utils.processImgSrc(it)
                            content.set(articleContent)
                        }
                    }
                }, { t: Throwable? ->  })
    }
}
複製代碼

這裏一個重點:doOnSubscribedoAfterTerminate

doOnSubscribe是在訂閱開始時會觸發的方法,能夠代替onStart()

doAfterTerminate是在Single成功或者失敗以後會觸發的方法,能夠代替onComplete()

咱們再來優化一下loadArticle方法

  1. 使用kotlin的擴展將異步操做組合起來

這裏咱們定義一個Rxjava的擴展函數

fun <T> Single<T>.async(withDelay: Long = 0): Single<T> =
        this.subscribeOn(Schedulers.io())
                .delay(withDelay, TimeUnit.MILLISECONDS)
                .observeOn(AndroidSchedulers.mainThread())
複製代碼

因此能夠將其轉換成這樣

remote.getArticleDetail(8773)
                .async(1000)
                .doOnSubscribe { loading.set(true) }
                .doAfterTerminate { loading.set(false) }
                .subscribe(...)
複製代碼

2 . 不依賴於具體實現

loading.set(true) 和 loading.set(false) 如今就能達到咱們想要的效果

可是若是萬一咱們須要使用另外一種加載方式,那麼就須要去改這裏,一個方法還好,若是多個方法都這麼寫,就比較麻煩了。因此最好定義兩個方法startLoad()stopLoad(),表明開始加載和結束加載。

fun loadArticle() {
        //爲了簡單起見這裏先寫個默認的id
        remote.getArticleDetail(8773)
                .async(1000)
                .doOnSubscribe { startLoad()}
                .doAfterTerminate { stopLoad() }
                .subscribe({ t: Article? ->
                    t?.let {
                        title.set(it.title)
                        it.content?.let {
                            val articleContent=Utils.processImgSrc(it)
                            content.set(articleContent)
                        }

                    }

                }, { t: Throwable? ->
                })
    }
//...
fun startLoad()=loading.set(true)
fun stopLoad()=loading.set(false)
複製代碼

嗯好的,如今看着順眼些了,那麼還有一個問題,若是出現error了怎麼處理,好像還沒處理到,假設這裏有一個需求是:當加載失敗的時候,使用Snackbar或者Toast、Diglog提示錯誤信息。

假設是Toast,那咱們須要調用

Toast.makeText(context,"error msg",Toast.LENGTH_SHORT).show()

這裏須要上下文Context,錯誤的作法是

  1. 將activity或者fragment的Context做爲參數傳進了,而後直接在ViewModel裏使用。❌

    由於ViewModel裏不該該有任何上下文Context的引用(除了App的ApplicationContext),而應該儘可能是純Java/kotlin代碼。一是爲了單元測試的便捷性,二是爲了防止內存泄漏。

  2. 使用一個回調到Activity或者Fragment中去處理 ❌

    這也就是我在第一部分中提過的跑偏了,這不就又變爲MVP了嗎(ps:本身也在這條路上跑了好幾步)

  3. 聰明點的作法是再自定義一個@BindAdapter,經過綁定使用View的context ✔️·

    @BindingAdapter(value = "toast")
    fun bindToast(v: View,msg:Throwable ?){
        msg?.let {
            Toast.makeText(v.context,it.message,Toast.LENGTH_SHORT).show()
        }
    }
    複製代碼

    這種作法可行,但我我的來講不太喜歡,由於我比較喜歡下一種。

  4. 充分利用RxJava ✔️·

    其實Rxjava和MVVM的思想上有一致的地方。

    • Observable.create/just/from...等操做符用於提供數據源,能夠認爲是MVVM的M層
    • Observable.map/flatMap/reduce...等操做符用於數據的轉換,將其變爲訂閱者須要的數據,這不正是ViewModel的功能
    • Subscriber...就至關於View層去使用這些數據

    其實更像是MVP,由於在Subscriber中成功和失敗。。。等等的回調,既然Subscriber中就有這些回調,那爲何不加以利用?

    怎麼充分利用RxJava

    只須要將loadArticle方法改形成爲一個Single

    fun loadArticle():Single<Article> =
            remote.getArticleDetail(8773)
                    .async(1000)
                    .doOnSuccess { t: Article? ->
                        t?.let {
                            title.set(it.title)
                            it.content?.let {
                                val articleContent=Utils.processImgSrc(it)
                                content.set(articleContent)
                            }
    
                        }
                    }
                    .doOnSubscribe { startLoad()}
                    .doAfterTerminate { stopLoad() }
    複製代碼

    doOnSuccess操做符中咱們對數據進行了處理,而後在acticity中須要更改一下調用時的寫法。

    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        item?.let {
            when(it.itemId){
                R.id.action_refresh -> mViewMode.loadArticle()
                        .subscribe { _, error -> dispatchError(error) }
                else -> { }
            }
        }
        return super.onOptionsItemSelected(item)
    }
    
    //依舊不依賴於具體實現,能夠是Toast/Dialog/Snackbar等等
    private fun dispatchError(error:Throwable?){
        error?.let { 
            Toast.makeText(this,it.message,Toast.LENGTH_SHORT).show()
        }
    }
    複製代碼

    到此,在MVVM中怎麼處理網絡數據就基本告一段落。

    接下來

    處理內存泄漏問題RxLifecycle

    很少說,可使用CompositeDisposable將全部的訂閱都統一解除,我習慣於使用RxLifecycle,更加方便

    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
            item?.let {
                when(it.itemId){
                    R.id.action_refresh -> mViewMode.loadArticle().compose(bindToLifecycle())
                            .subscribe { _, error -> dispatchError(error) }
                    else -> { }
                }
            }
            return super.onOptionsItemSelected(item)
        }
    複製代碼

    使用compose操做符綁定一下就行了,這也是我更傾向於這樣的寫法的緣由之一。

更新: 18.7.5:建議使用AutoDispose代替RxLifecycle

RxLifecycle 的做者推薦使用AutoDispose,他認爲AutoDispose的設計更爲優秀,爲此寫了一篇Why Not RxLifecycle?

不過對於咱們開發者而言,其實變化不大。

添加AutoDispose依賴

//autodispose
    implementation 'com.uber.autodispose:autodispose:0.8.0'
    implementation 'com.uber.autodispose:autodispose-android-archcomponents:0.8.0'
複製代碼

而後

override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        item?.let {
            when(it.itemId){
                R.id.action_refresh -> mViewModel.loadArticle()
                        .`as`(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(this)))
                        .subscribe({},{dispatchError(it)})
                else -> { }
            }
        }
        return super.onOptionsItemSelected(item)
    }

複製代碼

對於習慣使用RxLifeCycle的開發者建議擴展成RxLifeCycle的形式

override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        item?.let {
            when(it.itemId){
                R.id.action_refresh -> mViewModel.loadArticle()
                        .bindLifeCycle(this)
                        .subscribe({},{dispatchError(it)})
                else -> { }
            }
        }
        return super.onOptionsItemSelected(item)
    }

fun <T> Single<T>.bindLifeCycle(owner: LifecycleOwner): SingleSubscribeProxy<T> {
    return this.`as`(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(owner)))
}
複製代碼

結尾

本項目的github地址:github.com/ditclear/MV…

更多的例子能夠查看:github.com/ditclear/Pa…

這是使用Kotlin構建MVVM項目的第二部分,主要講了怎麼在MVVM中較好的處理從網絡返回的數據和解決內存泄漏問題。

其實回過頭來看會發現,這樣的方式基本告別了回調,寫着都感受好舒服,因而問本身爲何之前沒想到呢?原本就該這樣處理啊!至於緣由的話,多是如今Android項目中使用MVVM的例子太少,這樣的方式在github上不多出現,致使本身沒轉過彎。因此寫本文的目的之一是分享,二是但願android開發者不要盲目追從MVP,而遺忘了MVVM。

第三篇我會在本項目的基礎上進行數據持久化,即加入android架構組件的Room數據庫,敬請期待。

相關文章
相關標籤/搜索