簡書地址:www.jianshu.com/p/8993b2479…php
這是使用Kotlin開發MVVM應用程序的第二部分—Retrofit及RxJavajava
在前一部分中咱們簡單瞭解了MVVM的基本概念和寫法。若是你沒有看過上一篇,請先快速瀏覽一遍,由於本系列是按部就班的。能夠在這裏查看使用Kotlin構建MVVM應用程序—第一部分:入門篇react
若是第一篇是入了門,那這一篇就有點實戰的意思了,更加貼近咱們具體的需求,本文將闡述如何在MVVM中處理網絡數據。android
咱們先加入依賴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
此次咱們的Model層的數據源來自網絡,而且在ViewModel中使用RxJava進行數據的轉換。bash
開始正文
帶着問題看文章是個好習慣。—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
}
}
複製代碼
基本如出一轍,只是改變了數據源而已。
看一下效果:
好的,目的達到了。你能夠在這裏查看變動
爲了更有說服力,我優化了一下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
方法嗎?
別急,其實這只是使用kotlin
給MarkdownView
添加的擴展函數,具體見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? -> })
}
}
複製代碼
這裏一個重點:doOnSubscribe
和doAfterTerminate
doOnSubscribe
是在訂閱開始時會觸發的方法,能夠代替onStart()
而doAfterTerminate
是在Single成功或者失敗以後會觸發的方法,能夠代替onComplete()
咱們再來優化一下loadArticle
方法
這裏咱們定義一個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,錯誤的作法是
將activity或者fragment的Context做爲參數傳進了,而後直接在ViewModel裏使用。❌
由於ViewModel裏不該該有任何上下文Context的引用(除了App的ApplicationContext),而應該儘可能是純Java/kotlin代碼。一是爲了單元測試的便捷性,二是爲了防止內存泄漏。
使用一個回調到Activity或者Fragment中去處理 ❌
這也就是我在第一部分中提過的跑偏了,這不就又變爲MVP了嗎(ps:本身也在這條路上跑了好幾步)
聰明點的作法是再自定義一個@BindAdapter
,經過綁定使用View的context ✔️·
@BindingAdapter(value = "toast")
fun bindToast(v: View,msg:Throwable ?){
msg?.let {
Toast.makeText(v.context,it.message,Toast.LENGTH_SHORT).show()
}
}
複製代碼
這種作法可行,但我我的來講不太喜歡,由於我比較喜歡下一種。
充分利用RxJava ✔️·
其實Rxjava和MVVM的思想上有一致的地方。
其實更像是MVP,由於在Subscriber中成功和失敗。。。等等的回調,既然Subscriber中就有這些回調,那爲何不加以利用?
只須要將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中怎麼處理網絡數據就基本告一段落。
接下來
很少說,可使用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數據庫,敬請期待。