<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>

          LiveData beyond the ViewModel

          共 14833字,需瀏覽 30分鐘

           ·

          2021-12-10 09:35

          點(diǎn)擊上方藍(lán)字關(guān)注我,知識會(huì)給你力量


          這個(gè)系列我做了協(xié)程和Flow開發(fā)者的一系列文章的翻譯,旨在了解當(dāng)前協(xié)程、Flow、LiveData這樣設(shè)計(jì)的原因,從設(shè)計(jì)者的角度,發(fā)現(xiàn)他們的問題,以及如何解決這些問題,pls enjoy it。

          多年來,反應(yīng)式架構(gòu)一直是Android的一個(gè)熱門話題。它一直是Android會(huì)議上的一個(gè)永恒主題,通常都是用RxJava的例子來進(jìn)行演示的(見底部的Rx部分)。反應(yīng)式編程是一種關(guān)注數(shù)據(jù)「如何流動(dòng)」以及「如何傳播」的范式,它可以簡化構(gòu)建應(yīng)用程序的代碼,方便顯示來自異步操作的數(shù)據(jù)。

          實(shí)現(xiàn)一些反應(yīng)式概念的一個(gè)工具是LiveData。它是一個(gè)簡單的觀察者,能夠意識到觀察者的生命周期。從你的數(shù)據(jù)源或存儲庫中暴露LiveData是使你的架構(gòu)更具反應(yīng)性的一個(gè)簡單方法,但也有一些潛在的陷阱。

          這篇博文將幫助你避免陷阱,并使用一些模式來幫助你使用LiveData構(gòu)建一個(gè)更加「反應(yīng)式」的架構(gòu)。

          LiveData’s purpose

          在Android中,Activity、Fragment和視圖幾乎可以在任何時(shí)候被銷毀,所以對這些組件之一的任何引用都可能導(dǎo)致泄漏或NullPointerException異常。

          LiveData被設(shè)計(jì)用來實(shí)現(xiàn)觀察者模式,允許視圖控制器(Activity、Fragment等)和UI數(shù)據(jù)的來源(通常是ViewModel)之間進(jìn)行通信。

          通過LiveData,這種通信更加安全:由于它的生命周期意識,數(shù)據(jù)只有在View處于Activity狀態(tài)時(shí)才會(huì)被接收。

          簡而言之,其優(yōu)點(diǎn)是你不需要在View和ViewModel之間手動(dòng)取消訂閱。

          img

          LiveData beyond the ViewModel

          可觀察范式在視圖控制器和ViewModel之間工作得非常好,所以你可以用它來觀察你的應(yīng)用程序的其他組件,并利用生命周期意識的優(yōu)勢。比如說下面這些場景:

          • 觀察SharedPreferences中的變化
          • 觀察Firestore中的一個(gè)文檔或集合
          • 用FirebaseAuth這樣的認(rèn)證SDK觀察當(dāng)前用戶的授權(quán)
          • 觀察Room中的查詢(它支持開箱即用的LiveData)

          這種模式的優(yōu)點(diǎn)是,由于所有的東西都是連在一起的,所以當(dāng)數(shù)據(jù)發(fā)生變化時(shí),用戶界面會(huì)自動(dòng)更新。

          缺點(diǎn)是,LiveData并沒有像Rx那樣提供一個(gè)用于組合數(shù)據(jù)流或管理線程的工具包。

          如果在一個(gè)典型的應(yīng)用程序的每一層中使用LiveData,看起來就像這樣。

          img

          為了在組件之間傳遞數(shù)據(jù),我們需要一種方法來映射和組合數(shù)據(jù)。MediatorLiveData就是LiveData提供的用于組合數(shù)據(jù)的工具,同時(shí)與Transformations類也提供了一些變換工具。

          • Transformations.map
          • Transformations.switchMap

          請注意,當(dāng)你的View被銷毀時(shí),你不需要銷毀這些訂閱,因?yàn)閂iew的lifecycle會(huì)被傳播到下游后繼續(xù)訂閱。

          Patterns

          One-to-one static transformation — map

          img

          在我們上面的例子中,ViewModel只是將數(shù)據(jù)從資源庫轉(zhuǎn)發(fā)到視圖,將其轉(zhuǎn)換為UI模型。每當(dāng)資源庫有新的數(shù)據(jù)時(shí),ViewModel只需對其進(jìn)行映射即可。

          class MainViewModel {
            val viewModelResult = Transformations.map(repository.getDataForUser()) { data ->
               convertDataToMainUIModel(data)
            }
          }

          這種轉(zhuǎn)變是非常簡單的。然而,如果上面的User數(shù)據(jù)是可以改變的,那么你需要使用switchMap。

          One-to-one dynamic transformation — switchMap

          考慮一下這個(gè)例子:你正在觀察一個(gè)暴露了User的用戶管理器,你需要獲取他們的ID,然后才能對存儲庫進(jìn)行觀察。

          img

          你不能在ViewModel的初始化中創(chuàng)建它們,因?yàn)橛脩鬒D不是立即可用的。你可以用switchMap來實(shí)現(xiàn)這一點(diǎn)。

          class MainViewModel {
           // val userId: LiveData<String> = ...

            val repositoryResult = Transformations.switchMap(userManager.userID) { userID ->
               repository.getDataForUser(userID)
            }
          }

          switchMap內(nèi)部使用的也是MediatorLiveData,所以熟悉它很重要,隱藏,當(dāng)你想結(jié)合多個(gè)LiveData的來源時(shí),你需要使用它。

          One-to-many dependency — MediatorLiveData

          MediatorLiveData允許你將一個(gè)或多個(gè)數(shù)據(jù)源添加到一個(gè)LiveData觀察器中。

          val liveData1: LiveData<Int> = ...
          val liveData2: LiveData<Int> = ...

          val result = MediatorLiveData<Int>()

          result.addSource(liveData1) { value ->
              result.setValue(value)
          }
          result.addSource(liveData2) { value ->
              result.setValue(value)
          }

          這個(gè)例子來自官方文檔,當(dāng)任何一個(gè)數(shù)據(jù)來源發(fā)生變化時(shí),都會(huì)更新結(jié)果。請注意,數(shù)據(jù)不是自動(dòng)為你組合的,MediatorLiveData只是負(fù)責(zé)通知的工作。

          為了在我們的示例應(yīng)用程序中實(shí)現(xiàn)轉(zhuǎn)換,我們需要將兩個(gè)不同的LiveDatas合并成一個(gè)。

          img

          使用MediatorLiveData來組合數(shù)據(jù)的方法是在不同的方法中添加來源和設(shè)置值。

          fun blogpostBoilerplateExample(newUser: String): LiveData<UserDataResult> {

              val liveData1 = userOnlineDataSource.getOnlineTime(newUser)
              val liveData2 = userCheckinsDataSource.getCheckins(newUser)

              val result = MediatorLiveData<UserDataResult>()

              result.addSource(liveData1) { value ->
                  result.value = combineLatestData(liveData1, liveData2)
              }
              result.addSource(liveData2) { value ->
                  result.value = combineLatestData(liveData1, liveData2)
              }
              return result
          }

          數(shù)據(jù)的實(shí)際組合是在combineLatestData方法中完成的。

          private fun combineLatestData(
                  onlineTimeResult: LiveData<Long>,
                  checkinsResult: LiveData<CheckinsResult>
          ): UserDataResult {

              val onlineTime = onlineTimeResult.value
              val checkins = checkinsResult.value

              // Don't send a success until we have both results
              if (onlineTime == null || checkins == null) {
                  return UserDataLoading()
              }

              // TODO: Check for errors and return UserDataError if any.
              return UserDataSuccess(timeOnline = onlineTime, checkins = checkins)
          }

          它檢查值是否準(zhǔn)備好或正確,并發(fā)出一個(gè)結(jié)果(加載、錯(cuò)誤或成功)。

          When not to use LiveData

          即使你想嘗試"反應(yīng)式",你也需要在到處添加LiveData之前了解其優(yōu)勢。如果你的應(yīng)用程序的某個(gè)組件與用戶界面沒有任何聯(lián)系,它可能不需要LiveData。

          例如,你應(yīng)用中的一個(gè)用戶管理器會(huì)監(jiān)聽你的認(rèn)證提供者(如Firebase Auth)的變化,并向你的服務(wù)器上傳一個(gè)唯一的令牌。

          img

          令牌上傳者可以觀察用戶管理器,但用誰的生命周期?這個(gè)操作與View完全沒有關(guān)系。此外,如果View被銷毀,用戶令牌可能永遠(yuǎn)不會(huì)被上傳。

          另一個(gè)選擇是使用令牌上傳器的observeForever(),并以某種方式鉤住用戶管理器的生命周期,在完成后刪除訂閱。

          然而,你不需要讓所有的東西都能被觀察到。這個(gè)場景下,你可以讓用戶管理器直接調(diào)用令牌上傳器(或任何對你的架構(gòu)有意義的東西)。

          img
          ?

          如果你的應(yīng)用程序的一部分不影響用戶界面,你可能不需要LiveData。

          ?

          Antipattern: Sharing instances of LiveData

          當(dāng)一個(gè)類將一個(gè)LiveData暴露給其他類時(shí),請仔細(xì)考慮是否要暴露同一個(gè)LiveData實(shí)例或不同的實(shí)例。

          class SharedLiveDataSource(val dataSource: MyDataSource) {

              // Caution: this LiveData is shared across consumers
              private val result = MutableLiveData<Long>()

              fun loadDataForUser(userId: String): LiveData<Long> {
                  result.value = dataSource.getOnlineTime(userId)
                  return result
              }
          }

          如果這個(gè)類在你的應(yīng)用程序中是一個(gè)單例(只有一個(gè)實(shí)例),你就可以總是返回同一個(gè)LiveData,對嗎?不一定:這個(gè)類可能有多個(gè)消費(fèi)者。例如,考慮這個(gè)場景。

          sharedLiveDataSource.loadDataForUser("1").observe(this, Observer {
             // Show result on screen
          }) 

          而第二個(gè)消費(fèi)者也在使用它。

          sharedLiveDataSource.loadDataForUser("2").observe(this, Observer {
             // Show result on screen
          }) 

          第一個(gè)消費(fèi)者將收到屬于用戶 "2 "的數(shù)據(jù)的更新。

          即使你認(rèn)為你只是從一個(gè)消費(fèi)者那里使用這個(gè)類,你也可能因?yàn)槭褂眠@種模式而最終出現(xiàn)錯(cuò)誤。例如,當(dāng)從一個(gè)Activity的一個(gè)實(shí)例導(dǎo)航到另一個(gè)實(shí)例時(shí),新的實(shí)例可能會(huì)暫時(shí)收到來自前一個(gè)實(shí)例的數(shù)據(jù)。請記住,LiveData會(huì)將最新的值分派給新的觀察者。另外,Lollipop中引入了Activity轉(zhuǎn)換,它們帶來了一個(gè)有趣的邊緣情況:兩個(gè)Activity處于活動(dòng)狀態(tài)。這意味著LiveData的唯一消費(fèi)者可能有兩個(gè)實(shí)例,其中一個(gè)可能會(huì)顯示錯(cuò)誤的數(shù)據(jù)。

          解決這個(gè)問題的方法是為每個(gè)消費(fèi)者返回一個(gè)新的LiveData。

          class SharedLiveDataSource(val dataSource: MyDataSource) {
              fun loadDataForUser(userId: String): LiveData<Long> {
                  val result = MutableLiveData<Long>()
                  result.value = dataSource.getOnlineTime(userId)
                  return result
              }
          }

          如果你要在消費(fèi)者之間共享一個(gè)LiveData實(shí)例之前,請仔細(xì)考慮。

          MediatorLiveData smell: adding sources outside initialization

          使用觀察者模式比持有對視圖的引用更安全(通常在MVP架構(gòu)中你會(huì)這樣做)。然而,這并不意味著你可以忘記泄漏的問題!

          考慮一下這個(gè)數(shù)據(jù)源。

          class SlowRandomNumberGenerator {
              private val rnd = Random()

              fun getNumber(): LiveData<Int> {
                  val result = MutableLiveData<Int>()

                  // Send a random number after a while
                  Executors.newSingleThreadExecutor().execute {
                      Thread.sleep(500)
                      result.postValue(rnd.nextInt(1000))
                  }

                  return result
              }
          }

          它只是在500ms后返回一個(gè)帶有隨機(jī)值的新LiveData。這并沒有什么問題。

          在ViewModel中,我們需要公開一個(gè)randomNumber屬性,從生成器中獲取數(shù)字。為此使用MediatorLiveData并不理想,因?yàn)樗竽阍诿看涡枰聰?shù)字時(shí)都要添加源。

          val randomNumber = MediatorLiveData<Int>()

          /**
          * *Don't do this.*
          *
          * Called when the user clicks on a button
          *
          * This function adds a new source to the result but it doesn'
          t remove the previous ones.
          */
          fun onGetNumber() {
             randomNumber.addSource(numberGenerator.getNumber()) {
                 randomNumber.value = it
             }
          }

          如果每次用戶點(diǎn)擊按鈕時(shí),我們都向MediatorLiveData添加一個(gè)源,那么該應(yīng)用就能按預(yù)期工作。然而,我們正在泄露所有以前的LiveDatas,這些LiveDatas不會(huì)再發(fā)送更新,所以這是一種浪費(fèi)。

          你可以存儲一個(gè)對源的引用,然后在添加新的源之前將其刪除。(Spoiler: this is what Transformations.switchMap does! See solution below.)

          我們不要使用MediatorLiveData,而是嘗試(但失敗了)用Transformation.map來解決這個(gè)問題。

          Transformation smell: Transformations outside initialization

          使用前面的例子,這就不可行了。

          var lateinit randomNumber: LiveData<Int>

          /**
           * Called on button click.
           */
          fun onGetNumber() {
             randomNumber = Transformations.map(numberGenerator.getNumber()) {
                 it
             }
          }

          這里有一個(gè)重要的問題需要理解。變換在調(diào)用時(shí)創(chuàng)建一個(gè)新的LiveData(包括map和switchMap)。在這個(gè)例子中,隨機(jī)數(shù)(randomNumber)被暴露在視圖中,但每次用戶點(diǎn)擊按鈕時(shí)它都會(huì)被重新分配。觀察者只在訂閱的時(shí)候接收分配給var的LiveData的更新,這是非常常見的。

          viewmodel.randomNumber.observe(this, Observer { number ->
              numberTv.text = resources.getString(R.string.random_text, number)
          })

          這個(gè)訂閱發(fā)生在onCreate()中,所以如果之后viewmodel.randomNumber LiveData實(shí)例發(fā)生變化,觀察者將不會(huì)被再次調(diào)用。

          換句話說。不要在var中使用Livedata。在初始化的時(shí)候,要將轉(zhuǎn)換的內(nèi)容寫入。

          Solution: wire transformations during initialization

          將暴露的LiveData初始化為一個(gè)transformation。

          private val newNumberEvent = MutableLiveData<Event<Any>>()

          val randomNumber: LiveData<Int> = Transformations.switchMap(newNumberEvent) {
             numberGenerator.getNumber()
          }

          在LiveData中使用一個(gè)事件來指示何時(shí)請求一個(gè)新號碼。

          /**
          * Notifies the event LiveData of a new request for a random number.
          */
          fun onGetNumber() {
             newNumberEvent.value = Event(Unit)
          }

          如果你不熟悉這種模式,請看這篇關(guān)于Activity的文章。

          https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150

          Bonus section

          Tidying up with Kotlin

          上面的MediatorLiveData例子顯示了一些代碼的重復(fù),所以我們可以利用Kotlin的擴(kuò)展函數(shù)。

          /**
          * Sets the value to the result of a function that is called when both `LiveData`s have data
          * or when they receive updates after that.
          */
          fun <T, A, B> LiveData<A>.combineAndCompute(other: LiveData<B>, onChange: (A, B) -> T): MediatorLiveData<T> {

             var source1emitted = false
             var source2emitted = false

             val result = MediatorLiveData<T>()

             val mergeF = {
                 val source1Value = this.value
                 val source2Value = other.value

                 if (source1emitted && source2emitted) {
                     result.value = onChange.invoke(source1Value!!, source2Value!! )
                 }
             }

             result.addSource(this) { source1emitted = true; mergeF.invoke() }
             result.addSource(other) { source2emitted = true; mergeF.invoke() }

             return result
          }

          存儲庫現(xiàn)在看起來干凈多了。

          fun getDataForUser(newUser: String?): LiveData<UserDataResult> {
             if (newUser == null) {
                 return MutableLiveData<UserDataResult>().apply { value = null }
             }

             return userOnlineDataSource.getOnlineTime(newUser)
                     .combineAndCompute(userCheckinsDataSource.getCheckins(newUser)) { a, b ->
                 UserDataSuccess(a, b)
             }
          }

          LiveData and RxJava

          最后,讓我們來討論一個(gè)顯而易見而又沒人愿意討論的問題。LiveData被設(shè)計(jì)為允許視圖觀察ViewModel。一定要把它用在這上面! 即使你已經(jīng)使用了Rx,你也可以用LiveDataReactiveStreams進(jìn)行通信。

          如果你想在表現(xiàn)層之外使用LiveData,你可能會(huì)發(fā)現(xiàn)MediatorLiveData并沒有像RxJava那樣提供一個(gè)工具包來組合和操作數(shù)據(jù)流。然而,Rx有一個(gè)陡峭的學(xué)習(xí)曲線。LiveData轉(zhuǎn)換(和Kotlin魔法)的組合可能足以滿足你的情況,但如果你(和你的團(tuán)隊(duì))已經(jīng)投資學(xué)習(xí)RxJava,你可能不需要LiveData。

          如果你使用auto-dispose,那么為此使用LiveData將是多余的。

          原文鏈接:https://medium.com/androiddevelopers/livedata-beyond-the-viewmodel-reactive-patterns-using-transformations-and-mediatorlivedata-fda520ba00b7

          向大家推薦下我的網(wǎng)站 https://xuyisheng.top/ 專注 Android-Kotlin-Flutter 歡迎大家訪問

          向大家推薦下我的網(wǎng)站 https://xuyisheng.top/  點(diǎn)擊原文一鍵直達(dá)

          專注 Android-Kotlin-Flutter 歡迎大家訪問



          往期推薦


          本文原創(chuàng)公眾號:群英傳,授權(quán)轉(zhuǎn)載請聯(lián)系微信(Tomcat_xu),授權(quán)后,請?jiān)谠瓌?chuàng)發(fā)表24小時(shí)后轉(zhuǎn)載。
          < END >
          作者:徐宜生

          更文不易,點(diǎn)個(gè)“三連”支持一下??


          瀏覽 37
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(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>
                  青青艹网站在线观看 | 日韩黄色电影网址网站 | 老司机免费视频 | 日韩中文无 | 欧美在线免费播放不卡欧美 |