<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Google 推薦在 MVVM 架構(gòu)中使用 Kotlin Flow

          共 9494字,需瀏覽 19分鐘

           ·

          2020-08-22 18:38

          作者:HiDhl

          鏈接:https://juejin.im/post/6854573211930066951


          在之前分享過(guò)一篇 Jetpack 綜合實(shí)戰(zhàn)應(yīng)用 [神奇寶貝(PokemonGo) 眼前一亮的 Jetpack + MVVM 極簡(jiǎn)實(shí)戰(zhàn)](https://juejin.im/post/6850037271253483534?utm_source=gold_browser_extension)?,這個(gè)項(xiàng)目主要包了以下功能:

          1. 自定義 RemoteMediator 實(shí)現(xiàn) network + db 的混合使用 ( RemoteMediator 是 Paging3 當(dāng)中重要成員 )
          2. 使用 Data Mapper 分離數(shù)據(jù)源 和 UI
          3. Kotlin Flow 結(jié)合 Retrofit2 + Room 的混合使用
          4. Kotlin Flow 與 LiveData 的使用
          5. 使用 Coil 加載圖片
          6. 使用 ViewModel、LiveData、DataBinding 協(xié)同工作
          7. 使用 Motionlayout 做動(dòng)畫(huà)
          8. App Startup 與 Hilt 的使用
          9. 增加 Fragment 1.2.0 上重要的更新:通過(guò) Fragment 的構(gòu)造函數(shù)傳遞參數(shù),以及 FragmentFactory 和 FragmentContainerView 的使用

          我近期也在開(kāi)發(fā)另外一個(gè) Jetpack + MVVM 實(shí)戰(zhàn)應(yīng)用,和神奇寶貝(PokemonGo) 有很多不同之處,神奇寶貝(PokemonGo) 主要偏向于 Paging3 的分頁(yè)處理,以及 Flow 在 MVVM 中的實(shí)戰(zhàn)。

          而今天這篇文章主要來(lái)分析一下 神奇寶貝(PokemonGo) 項(xiàng)目,主要包含以下幾個(gè)方面的內(nèi)容:

          • 在 Repositories 或者 DataSource 中直接使用 LiveData 這種做法對(duì)嗎?

          • Kotlin Flow 是什么?

          • Kotlin Flow 解決了什么問(wèn)題?

          • Kotlin Flow 如何在 MVVM 中使用?

          • Kotlin Flow 如何與 Retrofit2 + Room 混合使用?


          Google 推薦在 MVVM 中

          使用 Kotlin Flow

          Google 推薦在 MVVM 中使用 Kotlin Flow我相信如今幾乎所有的 Android 開(kāi)發(fā)者至少都聽(tīng)過(guò) MVVM 架構(gòu),在 Google Android 團(tuán)隊(duì)宣布了 Jetpack 的視圖模型之后,它已經(jīng)成為了現(xiàn)代 Android 開(kāi)發(fā)模式最流行的架構(gòu)之一,如下圖所示:

          在官宣 Jetpack 的視圖模型之后,同時(shí) Google 在 [Jetpack?Guide](https://developer.android.com/jetpack/guide#fetch-data)?文章中的示例,也在 Repositories 或者 DataSource 中使用 LiveData,以至于在很多開(kāi)源的 MVVM 項(xiàng)目中也是直接使用 LiveData,但是在 Repositories 或者 DataSource 中直接使用 LiveData 這種做法對(duì)嗎?這是我一直以來(lái)的一個(gè)疑問(wèn)?

          直到我打開(kāi)[?Android 架構(gòu)組件?](https://developer.android.com/topic/libraries/architecture/index.html)頁(yè)面,看了在頁(yè)面上增加了最新的文章,這幾篇文章大概的內(nèi)容是說(shuō)如何在 MVVM 中使用 Flow 以及如何與 LiveData 一起使用,當(dāng)我看完并通過(guò)實(shí)踐之后大概明白了,LiveData 是一個(gè)生命周期感知組件,它并不屬于 Repositories 或者 DataSource 層,下文會(huì)有詳細(xì)的分析。

          在 Google 發(fā)布的 Jetpack 的最新成員 Paging3,在其內(nèi)部的源碼實(shí)現(xiàn)也是使用的 Flow,關(guān)于 Paging3 的使用可以參考以下鏈接:

          • Jetpack 成員 Paging3 實(shí)踐以及源碼分析(一)(https://juejin.im/post/6844904193468137486)

          • Jetpack 新成員 Paging3 網(wǎng)絡(luò)實(shí)踐及原理分析(二)(https://juejin.im/post/6844904196207345672)

          • 自定義 RemoteMediator 實(shí)現(xiàn) network + db 的混合使用(https://github.com/hi-dhl/PokemonGo)

          不僅僅是 Jetpack 成員支持 Flow,在 Google 提供的 Demo 里面也都在使用 Flow,也有很多開(kāi)源的 MVVM 項(xiàng)目也在逐漸切換到 Flow,為什么 Google 會(huì)推薦使用它呢,使用 Flow 能帶來(lái)那些好處呢,為我們解決了什么問(wèn)題

          Kotlin Flow 是什么?

          Kotlin Flow 解決了什么問(wèn)題?

          Flow 庫(kù)是在 Kotlin Coroutines 1.3.2 發(fā)布之后新增的庫(kù),也叫做異步流,類(lèi)似 RxJava 的?Observable?、?Flowable?等等,所以很多人都用 Flow 與 RxJava 做對(duì)比。

          Flow 相比于 RxJava 簡(jiǎn)單的太多了,你還記得那些 RxJava 傻傻分不清楚的操作符嗎?Observable、?Flowable?、?Single?、?Completable?、?Maybe?等等。

          那么 Flow 為我們解決了什么問(wèn)題,我主要從以下幾個(gè)方面思考:

          • LiveData 是一個(gè)生命周期感知組件,最好在 View 和 ViewModel 層中使用它,如果在 Repositories 或者 DataSource 中使用會(huì)有幾個(gè)問(wèn)題

            • 它不支持線程切換,其次不支持背壓,也就是在一段時(shí)間內(nèi)發(fā)送數(shù)據(jù)的速度 >?接受數(shù)據(jù)的速度,LiveData 無(wú)法正確的處理這些請(qǐng)求

            • 使用 LiveData 的最大問(wèn)題是所有數(shù)據(jù)轉(zhuǎn)換都將在主線程上完成

          • RxJava 雖然支持線程切換和背壓,但是 RxJava 那么多傻傻分不清楚的操作符,實(shí)際上在項(xiàng)目中常用的可能只有幾個(gè)例如?Observable?、?Flowable?、?Single?等等,如果我們不去了解背后的原理,造成內(nèi)存泄露是很正常的事,大家可以從 StackOverflow 上查看一下,有很多因?yàn)?RxJava 造成內(nèi)存泄露的例子

          • RxJava 入門(mén)的門(mén)檻很高,學(xué)習(xí)過(guò)的朋友們,我相信能夠體會(huì)到從入門(mén)到放棄是什么感覺(jué)

          • 解決回調(diào)地獄的問(wèn)題

          而相對(duì)于以上的不足,F(xiàn)low 有以下優(yōu)點(diǎn):

          • Flow 支持線程切換、背壓

          • Flow 入門(mén)的門(mén)檻很低,沒(méi)有那么多傻傻分不清楚的操作符

          • 簡(jiǎn)單的數(shù)據(jù)轉(zhuǎn)換與操作符,如 map 等等

          • Flow 是對(duì) Kotlin 協(xié)程的擴(kuò)展,讓我們可以像運(yùn)行同步代碼一樣運(yùn)行異步代碼,使得代碼更加簡(jiǎn)潔,提高了代碼的可讀性

          • 易于做單元測(cè)試


          Kotlin Flow 如何在 MVVM 中使用


          Jetpack 的視圖模型 MVVM 架構(gòu)由 View + DataBinding + ViewModel + Model 組成,如下所示,我相信下面這張圖大家非常熟悉了,

          接下來(lái)我們一起來(lái)探究一下 Kotlin Flow 在 MVVM 當(dāng)中每層是如何實(shí)現(xiàn)的。

          Kotlin Flow 在數(shù)據(jù)源中的使用

          在 [PokemonGo](https://github.com/hi-dhl/PokemonGo) 項(xiàng)目中,進(jìn)入詳情頁(yè),會(huì)檢查本地是否有數(shù)據(jù),如果沒(méi)有會(huì)去請(qǐng)求 [pokeapi] (https://pokeapi.co/)詳情頁(yè)接口,獲得最新的數(shù)據(jù),然后存儲(chǔ)在數(shù)據(jù)庫(kù)中。

          Flow 是協(xié)程的擴(kuò)展,如果要在 Room 和 Retrofit 中使用,Room 和 Retrofit 需要支持協(xié)程才可以,在 Retrofit >= 2.6.0 和 Room >= 2.1 版本都支持協(xié)程,我們來(lái)看一下 Room 和 Retrofit 數(shù)據(jù)源的配置。

          Room:
          PokemonGo/app/src/main/java/com/hi/dhl/pokemon/data/local/PokemonInfoDao.kt

          @Query("SELECT * FROM PokemonInfoEntity where name = :name")
          suspend fun getPokemon(name: String): PokemonInfoEntity?

          或者直接返回?Flow

          @Query("SELECT * FROM PokemonInfoEntity where name = :name")
          fun getPokemon(name: String): Flow

          Retrofit:
          PokemonGo/app/src/main/java/com/hi/dhl/pokemon/data/remote/PokemonService.kt

          @GET("pokemon/{name}")
          suspend fun fetchPokemonInfo(@Path("name") name: String): NetWorkPokemonInfo

          如上所見(jiàn)在方法前增加了用?suspend?進(jìn)行了修飾,只有被?suspend?修飾的方法,才可以在協(xié)程中調(diào)用。

          按照如上配置,在數(shù)據(jù)源的工作就完成了,相比于 RxJava 的?Observable?、?Flowable?、?Single?、?Completable?、?Maybe?使用場(chǎng)景要簡(jiǎn)單太多了,我們來(lái)看一下在 Repositories 中是如何使用的。

          Kotlin Flow 在 Repositories 中的使用

          如果我們想在 Flow 中使用 Retrofit 或者 Room 進(jìn)行網(wǎng)絡(luò)請(qǐng)求或者查詢數(shù)據(jù)庫(kù)的操作,我們需要將使用?suspend?修飾符的操作放到?flow { ... }?中執(zhí)行,最后使用?emit()?方法更新數(shù)據(jù),將數(shù)據(jù)發(fā)送給 ViewModel,代碼如下所示:
          PokemonGo/app/src/main/java/com/hi/dhl/pokemon/data/repository/PokemonRepositoryImpl.kt

          flow {
          val pokemonDao = db.pokemonInfoDao()
          // 查詢數(shù)據(jù)庫(kù)是否存在,如果不存在請(qǐng)求網(wǎng)絡(luò)
          var infoModel = pokemonDao.getPokemon(name)
          if (infoModel == null) {
          // 網(wǎng)絡(luò)請(qǐng)求
          val netWorkPokemonInfo = api.fetchPokemonInfo(name)
          // 將網(wǎng)路請(qǐng)求的數(shù)據(jù),換轉(zhuǎn)成的數(shù)據(jù)庫(kù)的 model,之后插入數(shù)據(jù)庫(kù)
          infoModel = netWorkPokemonInfo.let {
          PokemonInfoEntity(
          name = it.name,
          height = it.height,
          weight = it.weight,
          experience = it.experience
          )
          }
          // 插入更新數(shù)據(jù)庫(kù)
          pokemonDao.insertPokemon(infoModel)
          }
          // 將數(shù)據(jù)源的 model 轉(zhuǎn)換成上層用到的 model,
          // ui 不能直接持有數(shù)據(jù)源,防止數(shù)據(jù)源的變化,影響上層的 ui
          val model = mapper2InfoModel.map(infoModel)
          // 更新數(shù)據(jù),將數(shù)據(jù)發(fā)送給 ViewModel
          emit(model)
          }.flowOn(Dispatchers.IO) // 通過(guò) flowOn 切換到 IO 線程

          將上面的代碼簡(jiǎn)化如下所示:

          flow {
          // 進(jìn)行網(wǎng)絡(luò)或者數(shù)據(jù)庫(kù)操作
          emit(model)
          }.flowOn(Dispatchers.IO) // 通過(guò) flowOn 切換到 IO 線程

          正如你所見(jiàn),將耗時(shí)操作放到?flow { ... }?里面,通過(guò)?flowOn(Dispatchers.IO)?切換到 IO 線程,最后通過(guò)?emit()?方法將數(shù)據(jù)發(fā)送給 ViewModel,接下來(lái)我們來(lái)看一下如何在 ViewModel 中接受 Flow 發(fā)送的數(shù)據(jù)。

          Kotlin Flow 在 ViewModel 中的使用

          在 ViewModel 中使用 Flow 之前在 Jetpack 成員 Paging3 實(shí)踐以及源碼分析(一)?文章也有提到, 這里我們?cè)谏钊敕治鲆幌拢?ViewModel 中接受 Flow 發(fā)送的數(shù)據(jù)有三種方法,根據(jù)實(shí)際情況去調(diào)用。
          PokemonGo/app/src/main/java/com/hi/dhl/pokemon/ui/detail/DetailViewModel.kt

          方法一

          在 LifeCycle 2.2.0 之前使用的方法,使用兩個(gè) LiveData,一個(gè)是可變的,一個(gè)是不可變的,如下所示:

          // 私有的 MutableLiveData 可變的,對(duì)內(nèi)訪問(wèn)
          private val _pokemon = MutableLiveData()

          // 對(duì)外暴露不可變的 LiveData,只能查詢
          val pokemon: LiveData = _pokemon

          viewModelScope.launch {
          polemonRepository.featchPokemonInfo(name)
          .onStart {
          // 在調(diào)用 flow 請(qǐng)求數(shù)據(jù)之前,做一些準(zhǔn)備工作,例如顯示正在加載數(shù)據(jù)的進(jìn)度條
          }
          .catch {
          // 捕獲上游出現(xiàn)的異常
          }
          .onCompletion {
          // 請(qǐng)求完成
          }
          .collectLatest {
          // 將數(shù)據(jù)提供給 Activity 或者 Fragment
          _pokemon.postValue(it)
          }
          }
          • 準(zhǔn)備一私有的 MutableLiveData,只對(duì)內(nèi)訪問(wèn)
          • 對(duì)外暴露不可變的 LiveData

          • 在?viewModelScope.launch?方法中執(zhí)行協(xié)程代碼塊

          • collectLatest?是末端操作符,收集 Flow 在 Repositories 層發(fā)射出來(lái)的數(shù)據(jù),在一段時(shí)間內(nèi)發(fā)送多次數(shù)據(jù),只會(huì)接受最新的一次發(fā)射過(guò)來(lái)的數(shù)據(jù)

          • 調(diào)用?_pokemon.postValue?方法將數(shù)據(jù)提供給 Activity 或者 Fragment

          方法二

          在 LifeCycle 2.2.0 之后,可以用更精簡(jiǎn)的方法來(lái)完成,使用 LiveData 協(xié)程構(gòu)造方法 (coroutine builder),這個(gè)方法也是在 PokemonGo 項(xiàng)目中用到的方法。

          @OptIn(ExperimentalCoroutinesApi::class)
          fun fectchPokemonInfo(name: String) = liveData {
          polemonRepository.featchPokemonInfo(name)
          .onStart { // 在調(diào)用 flow 請(qǐng)求數(shù)據(jù)之前,做一些準(zhǔn)備工作,例如顯示正在加載數(shù)據(jù)的進(jìn)度條 }
          .catch { // 捕獲上游出現(xiàn)的異常 }
          .onCompletion { // 請(qǐng)求完成 }
          .collectLatest {
          // 更新 LiveData 的數(shù)據(jù)
          emit(it)
          }
          }
          • liveData{ ... }?協(xié)程構(gòu)造方法提供了一個(gè)協(xié)程代碼塊,產(chǎn)生的是一個(gè)不可變的 LiveData,emit()?方法則用來(lái)更新 LiveData 的數(shù)據(jù)
          • collectLatest?是末端操作符,收集 Flow 在 Repositories 層發(fā)射出來(lái)的數(shù)據(jù),在一段時(shí)間內(nèi)發(fā)送多次數(shù)據(jù),只會(huì)接受最新的一次發(fā)射過(guò)來(lái)的數(shù)據(jù)

          PS:需要注意的是?flow { ... }?和?liveData{ ... }?內(nèi)部都有一個(gè)?emit()?方法。

          方法三:

          調(diào)用 Flow 的擴(kuò)展方法?asLiveData()?返回一個(gè)不可變的 LiveData,供 Activity 或者 Fragment 調(diào)用。

          @OptIn(ExperimentalCoroutinesApi::class)
          suspend fun fectchPokemonInfo3(name: String) =
          polemonRepository.featchPokemonInfo(name)
          .onStart {
          // 在調(diào)用 flow 請(qǐng)求數(shù)據(jù)之前,做一些準(zhǔn)備工作,例如顯示正在加載數(shù)據(jù)的按鈕
          }
          .catch {
          // 捕獲上游出現(xiàn)的異常
          }
          .onCompletion {
          // 請(qǐng)求完成
          }.asLiveData()

          因?yàn)?polemonRepository.featchPokemonInfo(name)?是一個(gè)用?suspend?修飾的方法,所以在 ViewModel 中調(diào)用也需要使用?suspend?來(lái)修飾。

          為什么說(shuō)調(diào)用?asLiveData()?方法會(huì)返回一個(gè)不可變的 LiveData,我們來(lái)看一下源碼:

          fun  Flow.asLiveData(
          context: CoroutineContext = EmptyCoroutineContext,
          timeoutInMs: Long = DEFAULT_TIMEOUT
          ): LiveData = liveData(context, timeoutInMs) {
          collect {
          emit(it)
          }
          }

          asLiveData()?方法其實(shí)就是對(duì)?方法二?中的?liveData{ ... }?的封裝

          • asLiveData?是 Flow 的擴(kuò)展函數(shù),返回值是一個(gè) LiveData

          • liveData{ ... }?協(xié)程構(gòu)造方法提供了一個(gè)協(xié)程代碼塊,在?liveData{ ... }?中執(zhí)行協(xié)程代碼

          • collect?是末端操作符,收集 Flow 在 Repositories 層發(fā)射出來(lái)的數(shù)據(jù)

          • 最后調(diào)用 LiveData 中的?emit()?方法更新 LiveData 的數(shù)據(jù)

          DataBinding(數(shù)據(jù)綁定)

          在 PokemonGo 項(xiàng)目中使用了 DataBinding 進(jìn)行的數(shù)據(jù)綁定。

          DataBinding(數(shù)據(jù)綁定)實(shí)際上是 XML 布局中的另一個(gè)視圖結(jié)構(gòu)層次,視圖 (XML) 通過(guò)數(shù)據(jù)綁定層不斷地與 ViewModel 交互,如下所示:
          PokemonGo/app/src/main/res/layout/activity_details.xml

          "http://schemas.android.com/apk/res/android"
          xmlns:app="http://schemas.android.com/apk/res-auto"
          xmlns:tools="http://schemas.android.com/tools">


          name="viewModel"
          type="com.hi.dhl.pokemon.ui.detail.DetailViewModel" />


          ......
          android:id="@+id/weight"
          android:text="@{viewModel.pokemon.getWeightString}"/>
          ......

          這是獲取神奇寶貝的詳細(xì)信息,通過(guò) DataBinding 以聲明方式將數(shù)據(jù)(神奇寶貝的體重)綁定到界面上,更多使用參考項(xiàng)目中的代碼。

          如何處理 ViewModel 的三種方式

          如果不使用數(shù)據(jù)綁定,在 Activity 或者 Fragment 中如何處理 ViewModel 的三種方式。
          PokemonGo/app/src/main/java/com/hi/dhl/pokemon/ui/detail/DetailsFragment.kt

          方式一:

          使用兩個(gè) LiveData,一個(gè)是可變的,一個(gè)是不可變的,在 Activity 或者 Fragment 中調(diào)用對(duì)外暴露不可變的 LiveData 即可,如下所示:

          // 方法一
          mViewModel.pokemon.observe(this, Observer {
          // 將數(shù)據(jù)顯示在頁(yè)面上
          })

          方式二:

          使用 LiveData 協(xié)程構(gòu)造方法 (coroutine builder) 提供的協(xié)程代碼塊,產(chǎn)生的是一個(gè)不可變的 LiveData,處理方式?同方法一,在 Activity 或者 Fragment 中調(diào)用這個(gè)不可變的 LiveData 即可,如下所示:

          // 方法二
          mViewModel.fectchPokemonInfo2(mPokemonModel.name).observe(this, Observer {
          // 將數(shù)據(jù)顯示在頁(yè)面上
          })

          方式三:

          調(diào)用 Flow 的擴(kuò)展方法?asLiveData()?返回一個(gè)不可變的 LiveData,在 Activity 或者 Fragment 調(diào)用這個(gè)不可變的 LiveData 即可,如下所示:

          // 方法三
          lifecycleScope.launch {
          mViewModel.apply {
          fectchPokemonInfo3(mPokemonModel.name).observe(this@DetailsFragment, Observer {
          // 將數(shù)據(jù)顯示在頁(yè)面上
          })
          }
          }

          到這里關(guān)于 Kotlin Flow 在 MVVM 當(dāng)中每層的實(shí)踐就分析完了,如果使用過(guò) RxJava 的小伙伴們應(yīng)該會(huì)非常熟悉,對(duì)于沒(méi)有使用過(guò) RxJava 的小伙伴們,入門(mén)的門(mén)檻也是非常低的,強(qiáng)烈建議至少體驗(yàn)一次,體驗(yàn)過(guò)之后,我認(rèn)為你會(huì)跟我一樣愛(ài)上它的。

          神奇寶貝 (PokemonGo) 基于 Jetpack + MVVM + Repository + Data Mapper + Kotlin Flow 的實(shí)戰(zhàn)項(xiàng)目,我也正在為 PokemonGo 項(xiàng)目設(shè)計(jì)更多的場(chǎng)景,也會(huì)加入更多的 Jetpack 成員,可以點(diǎn)擊下方鏈接前往查看。

          PokemonGo GitHub 地址:https://github.com/hi-dhl/PokemonGo


          結(jié)語(yǔ)
          致力于分享一系列 Android 系統(tǒng)源碼、逆向分析、算法、翻譯、Jetpack 源碼相關(guān)的文章,正在努力寫(xiě)出更好的文章,如果這篇文章對(duì)你有幫助給個(gè) star,文章中有什么沒(méi)有寫(xiě)明白的地方,或者有什么更好的建議歡迎留言,歡迎一起來(lái)學(xué)習(xí),在技術(shù)的道路上一起前進(jìn)。
          瀏覽 127
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费看操逼网站 | 超碰免费天天操天天干 | 日本精品一区二区 | 日韩无码一区二区三区 | 久久天堂影院 |