一點點入坑JetPack(終章):實戰MVVM

前言

此次的實戰篇,是這個系列的最後一篇。本文綜合前幾篇的內容,以僞代碼爲主,幫你們理解Google所推崇的MVVM。java

一點點入坑JetPack:ViewModel篇數據庫

一點點入坑JetPack:Lifecycle篇編程

一點點入坑JetPack:LiveData篇json

一點點入坑JetPack:實戰前戲NetworkBoundResource篇緩存

一點點入坑JetPack(終章):實戰MVVM網絡

相信有耐心看到這的小夥伴,徹底足以經過僞代碼,感覺出來如下代碼的設計思路。Go~app

正文

1、平常業務

上代碼以前,咱們思考一個小問題。咱們平時的業務,很重的一個部分是從一個地方獲取數據,而後在UI上展現出來。所以,本節實戰部分的背景:從網絡獲取一批數據,若是網絡請求成功,便更新到RecycleView上;若是網絡請求不成功,加載本地已有緩存,而後更新到RecycleView上。ide

是否是很簡單的需求,不少小夥伴可能順手就能寫出來:函數式編程

// 網絡請求
loadNetwork(參數, Callback(){
    // 請求成功,更新UI
	success(data){
		recyclerview.setData
	}
	// 請求失敗,讀取緩存
	error(){
		loadDB(參數,Callback(){
		    // 緩存讀取成功,更新UI
			success(data){
				recyclerview.setData
			}
		})
	}
})
複製代碼

很是直觀且易閱讀。咱們在深刻想一下,若是其餘頁面也有這樣的需求,是否是也要寫一份這個內容?函數

這裏確定有小夥伴會指出,應該進行封裝!沒錯,還記得上一篇文章提到的NetworkBoundResource嗎?接下來,就讓咱們經過NetworkBoundResource,使用MVVM的思想去封裝這個業務。

2、走進MVVM

2.一、走進MVVM流程圖

針對MVVM官方提供的一張比較清晰的流程圖:

2.二、走進MVVM代碼

按照官方的推薦,咱們須要一個Repository做爲整個數據層的管理者。

例如,咱們設計一個加載歌曲信息,而後更新到RecycleView上的需求。這個Repository我們就叫,MusicRepository,表示音樂相關的數據獲取交由這個類去管理。

那麼這個Repository是什麼樣子的呢?

一、Repository

MusicRepository

// 這裏的三個參數,分別是:線程池,緩存模塊,網絡模塊
class MusicRepository(
    val appExecutors: AppExecutors,
    val musicDao: MusicDao, // 後文會展開這個類
    val service: MusicApiService // 後文會展開這個類(具體的請求模塊)
) {

    companion object {
        val inst: MusicRepository by lazy {
	        // 這裏傳入的內容,固然是業務方本身去實現,好比這前業務已經存在的DB/內存緩存模塊;封裝好的網絡請求模塊,好比OkHttp/Retrofit等等
            MusicRepository(xxx,xxx,xxx)
        }
    }
   
    // Parameter會在後續中展開
    fun querySongs(parameter : Parameter): LiveData<Resource<MusicResp>> {
        return object :
            NetworkBoundResource<MusicResp, MusicResp>(
                appExecutors
            ) {

            override fun saveCallResult(item: MusicResp) {
                // 網絡請求成功,先存入緩存模塊
                musicDao.saveDB(item)
            }

            override fun shouldFetch(data: MusicResp?): Boolean {
                return 本身的是否請求網絡策略
            }

            override fun loadFromDb(): LiveData<MusicResp> {
                return musicDao.getCacheMusicResp(parameter.categoryId)
            }

            override fun createCall(): LiveData<ApiResponse<MusicResp>> {
	            // 調用網絡模塊的請求實現
                return service.querySongs(parameter)
            }
        }.asLiveData()
    }
}
複製代碼

接下來我們挨個展開上述代碼中用到的類,MusicDao一個負責咱們的Cache的實現類:

MusicDao

object MusicDao {
    private val musicStoreSongs: MutableMap<Long, MusicResp> by lazy {
        mutableMapOf<Long, MusicResp>()
    } 

    fun updateSongsCache(categoryId: Long, data: MusicResp) {
        musicStoreSongs[categoryId] = data
    }

    fun querySongsCache(categoryId: Long): LiveData<MusicResp> {
        val cacheSongLiveData = MutableLiveData<MusicResp>()
        cacheSongLiveData.value = musicStoreSongs[categoryId]
        return cacheSongLiveData
    }
}
複製代碼

這裏僅僅是實現了一套內存緩存。基於此咱們還能夠實現本身的數據庫緩存,或者內存+數據庫的二級緩存。而這一切的實現並不會對外邊的邏輯產生影響,作到了實現的隔離。

接下來,我們來看看網絡請求的實現類:MusicApiService

這裏涉及了協程的內容,建議沒有相關基礎的小夥伴,能夠看一看我以前寫過的文章。

老是在聊線程Thread,試試協程吧!

MusicApiService

object MusicApiService {
    
    override fun querySongs(parameter : Parameter): LiveData<ApiResponse<MusicResp>> {
        val liveData = MutableLiveData<ApiResponse<MusicResp>>()
        CoroutineScope(FastMain).launch {
            val resp = resp = withContext(BuzzApiPool) {
            // 這裏對應的是業務方本身的網絡實現封裝
    		val np = NetWorkManager.getInstance().networkProvider
   		    val builder = Uri.parse("服務端的請求接口")
       			 .buildUpon()
       		builder.appendQueryParameter("category_id", parameter.categoryId)
    		try {
    		    // 本身封裝的get請求
        		val json = np.networkClient.get(builder.toString())
        		// 這裏封裝的是Gson把String轉成JavaBean的方法
       			val data: MusicResp = fromServerResp(json)
        		data
    		} catch (e: Exception) {
        		MusicResp(e)
    		}
    		if (resp.isSuccess)) {
                liveData.postValue(ApiSuccessResponse(resp))
            } else {
                liveData.postValue(
                    ApiErrorResponse(resp.exception ?: RuntimeException("unknown_error"))
                )
            }
	    }
        return liveData
    }
}
複製代碼

有了Repository以後,咱們則須要考慮一下ViewModel了。就叫MusicViewModel

二、ViewModel

class MusicViewModel :ViewModel(){
    // Parameter 僞碼
	var parameter = MutableLiveData<Parameter>()
	val data : LiveData<Resource<MusicResp>> = Transformations.switchMap(parameter) { parameter->
        MusicRepository.inst.querySongs(parameter)
    }
}
複製代碼

三、Activity/Fragment

ViewModel這樣就夠了,接下來就是咱們的UI,這裏就叫MusicActivity吧。

class MusicActivity : AppCompatActivity(){
    private lateinit var musicViewModel: MusicViewModel
    
	override fun onCreate(savedInstanceState: Bundle?) {
		setContentView(R.layout.xxx)
		musicViewModel = ViewModelProviders.of(this).get(MusicViewModel::class.java)
		
		musicViewModel.data.observe(this, Observer { musicResp->
			// 這裏監聽的數據就是MusicRepository返回的MusicResp
			adapter.setData(musicResp)
		}
		// 經過LiveData通知MusicRepository進行網絡請求
		musicViewModel.parameter.value=Parameter(categoryId = xx) //本次請求的參數
	}
}
複製代碼

到這裏,咱們最基本的使用,就完成了。

對於UI層來講:

  • 它只須要在本身須要請求數據的時候經過MusicViewModel給「parameter」這個LiveData賦一個真正的請求參數就能夠了。
  • Transformations.switchMap(參數)會收到變換而後執行MusicRepository.inst.querySongs(請求參數),以後的全部邏輯所有交由MusicRepository去處理。
  • 至於怎麼加載網絡,怎麼處理緩存,都是有各個獨立的模塊實現的。
  • 此外UI層在監聽musicViewModel.data的結果,更新UI便可。

這樣你會發現,對於Activity/Fragment來講,它就只是View層了,一點邏輯操做都沒有。

固然這是理想狀態,畢竟PM擁有無窮的想象力,什麼樣的需求都會存在。

2.三、存在問題

我猜理解清楚這套設計的小夥伴,必定會之處問題所在。那就是:

一、性能問題

adapter.setData(musicResp),這就意味着,每次數據回調回來RecycleView都會更新,這樣就產生了不少無用的刷新。 並且這裏是監聽這個數據對象,若是想進行局部刷新,那麼Activity/Fragment中勢必要作不少額外的邏輯操做...

沒錯!這是一個嚴重的問題,但實際上Google早在好久以前就提供了一個類DiffUtil,這個類能夠說完美的幫咱們在這套設計裏,搞定了RecycleView空刷的性能消耗。

若是有必要,下篇文章能夠聊一聊DiffUtil和Immutable、Mutable的理念

二、額外的業務邏輯

畢竟有些時候,咱們沒辦法這麼直來直去的加載數據。更多的時候,咱們須要在業務回來時進行一系列的額外代碼:好比數據的變換邏輯的判斷...

  • 數據變換:這類操做,可使用函數式編程的思想,很方便的在ViewModel中完成並經過LiveData通知給observe方。

  • 邏輯的判斷:這部份內容,並不屬於MVVM(數據驅動)的部分。因此至於它還須要仁者見仁智者見智的封裝...

想了好久,仍是以爲在此就停下實戰篇的內容。由於我覺得這已經夠了,若是能消化這整個系列的內容,我相信該怎麼使用JetPack,小夥伴們心中已經有了本身的想法~

固然,小夥伴們若是有什麼更騷的操做,歡迎留言交流呦~

尾聲

JetPack系列的文章,到此便告一段落了。不知道一路追過來的朋友們是否有收穫。

下一個長篇系列會是什麼內容,暫時尚未想好。你們有啥感興趣的,能夠留言給點建議~

我是一個應屆生,最近和朋友們維護了一個公衆號,內容是咱們在從應屆生過渡到開發這一路所踩過的坑,以及咱們一步步學習的記錄,若是感興趣的朋友能夠關注一下,一同加油~

我的公衆號:鹹魚正翻身
相關文章
相關標籤/搜索