<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-Flow在MVVM中的最佳實踐

          共 49401字,需瀏覽 99分鐘

           ·

          2021-10-18 04:20

          點擊上方藍字關(guān)注我,知識會給你力量


          最近在Medium上看到了Flow開發(fā)者寫的幾篇文章,覺得很不錯,推薦給大家。

          1

          原文鏈接:https://proandroiddev.com/using-livedata-flow-in-mvvm-part-i-a98fe06077a0

          最近,我一直在尋找MVVM架構(gòu)中Kotlin Flow的最佳實踐。在我回答了這個關(guān)于LiveData和Flow的問題后,我決定寫這篇文章。在這篇文章中,我將解釋如何在MVVM模式中使用Flow與LiveData。然后我們將看到如何通過使用Flow來改變應(yīng)用程序的主題。

          sample地址:https://github.com/fgiris/LiveDataWithFlowSample

          什么是Flow?

          Flow是coroutines庫中的一個反應(yīng)式流,能夠從一個Suspend函數(shù)中返回多個值。

          盡管Flow的用法似乎與LiveData非常相似,但它有更多的優(yōu)勢,比如:

          • 本身是異步的,具有結(jié)構(gòu)化的并發(fā)性
          • 用map、filter等操作符簡單地轉(zhuǎn)換數(shù)據(jù)
          • 易于測試

          如何在MVVM中使用Flow

          如果你的應(yīng)用程序有MVVM架構(gòu),你通常有一個數(shù)據(jù)層(數(shù)據(jù)庫、數(shù)據(jù)源等)、ViewModel和View(Fragment或Activity)。你可能會使用LiveData在這些層之間進行數(shù)據(jù)傳輸和轉(zhuǎn)換。但LiveData的主要目的是什么?它是為了進行數(shù)據(jù)轉(zhuǎn)換而設(shè)計的嗎?

          ?

          LiveData從來沒有被設(shè)計成一個完全成熟的反應(yīng)式流構(gòu)建器

          ——Jose Alcérreca在2019年Android Dev峰會上說

          ?

          由于LiveData是一個具有生命周期意識的組件,因此最好在View和ViewModel層中使用它。但數(shù)據(jù)層呢?我認為在數(shù)據(jù)庫層使用LiveData的最大問題是所有的數(shù)據(jù)轉(zhuǎn)換都將在主線程上完成,除非你啟動一個coroutine并在里面進行工作。這就是為什么你可能更喜歡在數(shù)據(jù)層中使用Suspend函數(shù)。

          假設(shè)你想從網(wǎng)絡(luò)上獲取天氣預(yù)報數(shù)據(jù)。那么在你的數(shù)據(jù)庫中使用Suspend函數(shù)就會類似于下面的情況。

          class WeatherForecastRepository @Inject constructor() {
              suspend fun fetchWeatherForecast(): Result<Int> {
                  // Since you can only return one value from suspend function
                  // you have to set data loading before calling fetchWeatherForecast

                  // Fake api call
                  delay(1000)

                  // Return fake success data 
                  return Result.Success((0..20).random())
              }
          }

          你可以在ViewModel中用viewModelScope調(diào)用這個函數(shù)。

          class WeatherForecastOneShotViewModel @Inject constructor(
              val weatherForecastRepository: WeatherForecastRepository
          ) : ViewModel() {

              private var _weatherForecast = MutableLiveData<Result<Int>>()
              val weatherForecast: LiveData<Result<Int>>
                  get() = _weatherForecast

              fun fetchWeatherForecast() {
                  // Set value as loading
                  _weatherForecast.value = Result.Loading

                  viewModelScope.launch {
                      // Fetch and update weather forecast LiveData
                      _weatherForecast.value = weatherForecastRepository.fetchWeatherForecast()
                  }
              }
          }

          這種方法對于每次被調(diào)用時都會運行的單次請求來說效果不錯。但是在獲取數(shù)據(jù)流的時候呢?

          這里就是Flow發(fā)揮作用的地方。如果你想從你的服務(wù)器上獲取實時更新,你可以用Flow來做,而不用擔(dān)心資源的泄露,因為結(jié)構(gòu)化的并發(fā)性迫使你這樣做。

          讓我們轉(zhuǎn)換我們的數(shù)據(jù)庫,使其返回Flow。

          class WeatherForecastRepository @Inject constructor() {

              /**
               * This methods is used to make one shot request to get
               * fake weather forecast data
               */
              fun fetchWeatherForecast() = flow {
                  emit(Result.Loading)
                  // Fake api call
                  delay(1000)
                  // Send a random fake weather forecast data
                  emit(Result.Success((0..20).random()))
              }

              /**
               * This method is used to get data stream of fake weather
               * forecast data in real time
               */
              fun fetchWeatherForecastRealTime() = flow {
                  emit(Result.Loading)
                  // Fake data stream
                  while (true) {
                      delay(1000)
                      // Send a random fake weather forecast data
                      emit(Result.Success((0..20).random()))
                  }
              }
          }

          現(xiàn)在,我們能夠從一個Suspend函數(shù)中返回多個值。你可以使用asLiveData擴展函數(shù)在ViewModel中把Flow轉(zhuǎn)換為LiveData。

          class WeatherForecastOneShotViewModel @Inject constructor(
              weatherForecastRepository: WeatherForecastRepository
          ) : ViewModel() {

              private val _weatherForecast = weatherForecastRepository
                  .fetchWeatherForecast()
                  .asLiveData(viewModelScope.coroutineContext) // Use viewModel scope for auto cancellation

              val weatherForecast: LiveData<Result<Int>>
                  get() = _weatherForecast
          }

          這看起來和使用LiveData差不多,因為沒有數(shù)據(jù)轉(zhuǎn)換。讓我們看看從數(shù)據(jù)庫中獲取實時更新。

          class WeatherForecastDataStreamViewModel @Inject constructor(
              weatherForecastRepository: WeatherForecastRepository
          ) : ViewModel() {

              private val _weatherForecast = weatherForecastRepository
                  .fetchWeatherForecastRealTime()
                  .map {
                      // Do some heavy operation. This operation will be done in the
                      // scope of this flow collected. In our case it is the scope
                      // passed to asLiveData extension function
                      // This operation will not block the UI
                      delay(1000)
                      it
                  }
                  .asLiveData(
                      // Use Default dispatcher for CPU intensive work and
                      // viewModel scope for auto cancellation when viewModel
                      // is destroyed
                      Dispatchers.Default + viewModelScope.coroutineContext
                  )

              val weatherForecast: LiveData<Result<Int>>
                  get() = _weatherForecast
          }

          當你獲取實時天氣預(yù)報數(shù)據(jù)時,map函數(shù)中的所有數(shù)據(jù)轉(zhuǎn)換將在Flow collect的scope內(nèi)以異步方式完成。

          ?

          注意:如果你在資源庫中沒有使用Flow,你可以通過使用liveData builder實現(xiàn)同樣的數(shù)據(jù)轉(zhuǎn)換功能。

          ?
          private val _weatherForecast = liveData {
              val response = weatherForecastRepository.fetchWeatherForecast()
              
              // Do some heavy operation with response
              delay(1000)
              
              emit(transformedResponse)
          }

          再次回到Flow的實時數(shù)據(jù)獲取,我們可以看到它在觀察數(shù)據(jù)流的同時更新文本字段,并沒有阻塞UI。

          class WeatherForecastDataStreamFragment : DaggerFragment() {
              
              ...
              
              override fun onActivityCreated(savedInstanceState: Bundle?) {
                  super.onActivityCreated(savedInstanceState)

                  // Obtain viewModel
                  viewModel = ViewModelProviders.of(
                      this,
                      viewModelFactory
                  ).get(WeatherForecastDataStreamViewModel::class.java)

                  // Observe weather forecast data stream
                  viewModel.weatherForecast.observe(viewLifecycleOwner, Observer {
                      when (it) {
                          Result.Loading -> {
                              Toast.makeText(context, "Loading", Toast.LENGTH_SHORT).show()
                          }
                          is Result.Success -> {
                              // Update weather data
                              tvDegree.text = it.data.toString()
                          }
                          Result.Error -> {
                              Toast.makeText(context, "Error", Toast.LENGTH_SHORT).show()
                          }
                      }
                  })

                  lifecycleScope.launch {
                      while (true) {
                          delay(1000)
                          // Update text 
                          tvDegree.text = "Not blocking"
                      }
                  }
              }
          }

          那么它將看起來像這樣:

          img

          用Flow改變你的應(yīng)用程序的主題

          由于Flow可以發(fā)出實時更新,我們可以把用戶的輸入看作是一種更新,并通過Flow發(fā)送。為了做到這一點,讓我們創(chuàng)建一個主題數(shù)據(jù)源,它有一個用于廣播更新的主題channel。

          class ThemeDataSource @Inject constructor(
              private val sharedPreferences: SharedPreferences
          ) {
              private val themeChannel: ConflatedBroadcastChannel<Theme> by lazy {
                  ConflatedBroadcastChannel<Theme>().also { channel ->
                      // When there is an access to theme channel
                      // get the current theme from shared preferences
                      // and send it to consumers
                      val theme = sharedPreferences.getString(
                          Constants.PREFERENCE_KEY_THEME,
                          null
                      ) ?: Theme.LIGHT.name // Default theme is light

                      channel.offer(Theme.valueOf(theme))
                  }
              }

              @FlowPreview
              fun getTheme(): Flow<Theme> {
                  return themeChannel.asFlow()
              }

              fun setTheme(theme: Theme) {
                  // Save theme to shared preferences
                  sharedPreferences
                      .edit()
                      .putString(Constants.PREFERENCE_KEY_THEME, theme.name)
                      .apply()

                  // Notify consumers
                  themeChannel.offer(theme)
              }
          }

          // Used to change the theme of the app
          enum class Theme {
              DARK, LIGHT
          }

          正如你所看到的,沒有從外部直接訪問themeChannel,themeChannel在被發(fā)送之前被轉(zhuǎn)換為Flow。

          在Activity層面上消費主題更新是更好的,因為所有來自其他Fragment的更新都可以被安全地觀察到。

          讓我們在ViewModel中獲取主題更新。

          class MainViewModel @Inject constructor(
              private val themeDataSource: ThemeDataSource
          ) : ViewModel() {
              // Whenever there is a change in theme, it will be
              // converted to live data
              private val _theme: LiveData<Theme> = themeDataSource
                  .getTheme()
                  .asLiveData(viewModelScope.coroutineContext)

              val theme: LiveData<Theme>
                  get() = _theme

              fun setTheme(theme: Theme) {
                  themeDataSource.setTheme(theme)
              }
          }

          而且在Activity中可以很容易地觀察到這一點。

          class MainActivity : DaggerAppCompatActivity() {
            
              ...

              override fun onCreate(savedInstanceState: Bundle?) {
                  super.onCreate(savedInstanceState)
                  setContentView(R.layout.activity_main)

                  ...

                  observeTheme()
              }

              private fun observeTheme() {
                  // Observe and update app theme if any changes happen
                  viewModel.theme.observe(this, Observer { theme ->
                      when (theme) {
                          Theme.LIGHT -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
                          Theme.DARK -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
                      }
                  })
              }
          }

          剩下的事情就是按下Fragment中的按鈕。

          class MainFragment : DaggerFragment() {

              private lateinit var viewModel: MainViewModel
            
              ...

              override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
                  
                  ...

                  btnDarkMode.setOnClickListener {
                      // Enable dark mode
                      viewModel.setTheme(Theme.DARK)
                  }
              }
          }

          瞧瞧! 剛剛用Flow改變了主題。

          Changing the app theme with using Flow

          2

          原文鏈接:https://proandroiddev.com/using-livedata-flow-in-mvvm-part-ii-252ec15cc93a

          在第一部分中,我們已經(jīng)看到了如何在資源庫層中使用Flow,以及如何用Flow和LiveData改變應(yīng)用程序的主題。在這篇文章中,我們將看到如何移除LiveData(甚至是MediatorLiveData),在所有層中只使用Flow。我們還將深入研究常見的Flow操作,如map、filter、transform等。最后,我們將實現(xiàn)一個搜索欄的例子,這個例子是由Sean McQuillan在 "Fragmented Podcast - 187: 與Manuel Vivo和Sean McQuillan的Coroutines "中給出的例子,使用了Channel和Flow。

          Say ?? to LiveData

          使用LiveData可以確保在生命周期所有者銷毀的情況下,你不會泄露任何資源。如果我告訴你,你幾乎可以(后面會解釋為什么不一樣,但幾乎)用Flow獲得同樣的好處呢?

          讓我們來看看我們?nèi)绾巫龅竭@一點。

          儲存庫

          存儲庫層保持不變,因為我們已經(jīng)在返回Flow。

          /**
               * This method is used to get data stream of fake weather
               * forecast data in real time with 1000 ms delay
               */
              fun fetchWeatherForecastRealTime() : Flow<Result<Int>> = flow {
                  // Fake data stream
                  while (true) {
                      delay(1000)
                      // Send a random fake weather forecast data
                      emit(Result.Success((0..20).random()))
                  }
              }

          ViewModel

          我們不需要用asLiveData將Flow轉(zhuǎn)換為LiveData,而只是在ViewModel中使用Flow。

          之前是這樣的。

          class WeatherForecastDataStreamViewModel @Inject constructor(
              weatherForecastRepository: WeatherForecastRepository
          ) : ViewModel() {

              private val _weatherForecast = weatherForecastRepository
                  .fetchWeatherForecastRealTime()
                  .map {
                      // Do some heavy operation. This operation will be done in the
                      // scope of this flow collected. In our case it is the scope
                      // passed to asLiveData extension function
                      // This operation will not block the UI
                      delay(1000)
                      it
                  }
                  .asLiveData(
                      // Use Default dispatcher for CPU intensive work and
                      // viewModel scope for auto cancellation when viewModel
                      // is destroyed
                      Dispatchers.Default + viewModelScope.coroutineContext
                  )

              val weatherForecast: LiveData<Result<Int>>
                  get() = _weatherForecast
          }

          只用Flow,它就變成了。

          class WeatherForecastDataStreamFlowViewModel @Inject constructor(
              weatherForecastRepository: WeatherForecastRepository
          ) : ViewModel() {

              private val _weatherForecast = weatherForecastRepository
                  .fetchWeatherForecastRealTime()

              val weatherForecast: Flow<Result<Int>>
                  get() = _weatherForecast
          }

          但是,等等。map過程缺少了,讓我們添加它,以便在繪制地圖時將攝氏溫度轉(zhuǎn)換為華氏溫度。

          private val _weatherForecast = weatherForecastRepository
              .fetchWeatherForecastRealTime()
              .map {
                  // Do some heavy mapping
                  delay(500)

                  // Let's add an additional mapping to convert
                  // celsius degree to Fahrenheit
                  if (it is Result.Success) {
                      val fahrenheitDegree = convertCelsiusToFahrenheit(it.data)
                      Result.Success(fahrenheitDegree)
                  } else it // Do nothing if result is loading or error
              }

          /**
           * This function converts given [celsius] to Fahrenheit.
           *
           * Fahrenheit degree = Celsius degree * 9 / 5 + 32
           *
           * @return Fahrenheit integer for [celsius]
           */
          private fun convertCelsiusToFahrenheit(celsius: Int) = celsius * 9 / 5 + 32

          你可能想在用戶界面中顯示加載,那么onStart就是一個完美的地方。

          private val _weatherForecast = weatherForecastRepository
              .fetchWeatherForecastRealTime()
              .onStart {
                  emit(Result.Loading)
              }
              .map { ... }

          如果你想過濾數(shù)值,那就去吧。你有過濾運算符。

          private val _weatherForecast = weatherForecastRepository
                  .fetchWeatherForecastRealTime()
                  .onStart { ... }
                  .filter {
                      // There could be millions of data when filtering
                      // Do some filtering
                      delay(2000)

                      // Let's add an additional filtering to take only
                      // data which is less than 10
                      if (it is Result.Success) {
                          it.data < 10
                      } else true // Do nothing if result is loading or error
                  }
                  .map { ... }

          你也可以用transform操作符對數(shù)據(jù)進行轉(zhuǎn)換,這使你可以靈活地對一個單一的值發(fā)出你想要的信息。

          private val _weatherForecast = weatherForecastRepository
                  .fetchWeatherForecastRealTime()
                  .onStart { ... }
                  .filter { ... }
                  .map { ... }
                  .transform {
                      // Let's send only even numbers
                      if (it is Result.Success && it.data % 2 == 0) {
                          val evenDegree = it.data
                          emit(Result.Success(evenDegree))
                        // You can call emit as many as you want in transform
                        // This makes transform different from filter operator
                      } else emit(it) // Do nothing if result is loading or error
                  }

          由于Flow是順序的,collecting一個值的總執(zhí)行時間是所有運算符的執(zhí)行時間之和。如果你有一個長期運行的運算符,你可以使用buffer,這樣直到buffer的所有運算符的執(zhí)行將在一個不同的coroutine中處理,而不是在協(xié)程中對Flow collect。這使得總的執(zhí)行速度更快。

          private val _weatherForecast = weatherForecastRepository
                  .fetchWeatherForecastRealTime()
                  .onStart { ... }
                  .filter { ... }
                  // onStart and filter will be executed on a different
                  // coroutine than this flow is collected
                  .buffer()
                  // The following map and transform will be executed on the same
                  // coroutine which this flow is collected
                  .map { ... }
                  .transform { ... }

          如果你不想多次收集相同的值呢?那么你就可以使用distinctUntilChanged操作符,它只在值與前一個值不同時發(fā)送。

          private val _weatherForecast = weatherForecastRepository
                .fetchWeatherForecastRealTime()
                .onStart { ... }
                .distinctUntilChanged()
                .filter { ... }
                .buffer()
                .map { ... }
                .transform { ... }

          比方說,你只想在顯示在用戶界面之前緩存修改過的數(shù)據(jù)。你可以利用onEach操作符來完成每個值的工作。

          private val _weatherForecast = weatherForecastRepository
                .fetchWeatherForecastRealTime()
                .onStart { ... }
                .distinctUntilChanged()
                .filter { ... }
                .buffer()
                .map { ... }
                .transform { ... }
                .onEach {
                  // Do something with the modified data. For instance
                  // save the modified data to cache
                  println("$it has been modified and reached until onEach operator")
                }

          如果你在所有運算符中做一些繁重的工作,你可以通過使用flowOn運算符簡單地改變整個運算符的執(zhí)行環(huán)境。

          private val _weatherForecast = weatherForecastRepository
                .fetchWeatherForecastRealTime()
                .onStart { ... }
                .distinctUntilChanged()
                .filter { ... }
                .buffer()
                .map { ... }
                .transform { ... }
                .onEach { ... }
                .flowOn(Dispatchers.Default) // Changes the context of flow

          錯誤怎么處理?只需使用catch操作符來捕捉下行流中的任何錯誤。

          private val _weatherForecast = weatherForecastRepository
                .fetchWeatherForecastRealTime()
                .onStart { ... }
                .distinctUntilChanged()
                .filter { ... }
                .buffer()
                .map { ... }
                .transform { ... }
                .onEach { ... }
                .flowOn(Dispatchers.Default)
                .catch { throwable ->
                    // Catch exceptions in all down stream flow
                    // Any error occurs after this catch operator
                    // will not be caught here
                    println(throwable)
                }

          如果我們有另一個流要與_weatherForecast流合并呢?(你可能會認為這是一個有多個LiveData源的MediatorLiveData)你可以使用合并函數(shù)來合并任何數(shù)量的流量。

          private val _weatherForecast = weatherForecastRepository
                .fetchWeatherForecastRealTime()
                .onStart { ... }
                .distinctUntilChanged()
                .filter { ... }
                .buffer()
                .map { ... }
                .transform { ... }
                .onEach { ... }
                .flowOn(Dispatchers.Default)
                .catch { ... }

          private val _weatherForecastOtherDataSource = weatherForecastRepository
                  .fetchWeatherForecastRealTimeOtherDataSource()

          // Merge flows when consumer gets
          val weatherForecast: Flow<Result<Int>>
              get() = merge(_weatherForecast, _weatherForecastOtherDataSource)

          最后,我們的ViewModel看起來像這樣。

          @ExperimentalCoroutinesApi
          class WeatherForecastDataStreamFlowViewModel @Inject constructor(
              weatherForecastRepository: WeatherForecastRepository
          ) : ViewModel() {

              private val _weatherForecastOtherDataSource = weatherForecastRepository
                  .fetchWeatherForecastRealTimeOtherDataSource()

              private val _weatherForecast = weatherForecastRepository
                  .fetchWeatherForecastRealTime()
                  .onStart {
                      emit(Result.Loading)
                  }
                  .distinctUntilChanged()
                  .filter {
                      // There could be millions of data when filtering
                      // Do some filtering
                      delay(2000)

                      // Let's add an additional filtering to take only
                      // data which is less than 10
                      if (it is Result.Success) {
                          it.data < 10
                      } else true // Do nothing if result is loading or error
                  }
                  .buffer()
                  .map {
                      // Do some heavy mapping
                      delay(500)

                      // Let'
          s add an additional mapping to convert
                      // celsius degree to Fahrenheit
                      if (it is Result.Success) {
                          val fahrenheitDegree = convertCelsiusToFahrenheit(it.data)
                          Result.Success(fahrenheitDegree)
                      } else it // Do nothing if result is loading or error
                  }
                  .transform {
                      // Let's send only even numbers
                      if (it is Result.Success && it.data % 2 == 0) {
                          val evenDegree = it.data
                          emit(Result.Success(evenDegree))
                      } else emit(it) // Do nothing if result is loading or error
                  }
                  .onEach {
                      // Do something with the modified data. For instance
                      // save the modified data to cache
                      println("$it has modified and reached until onEach operator")
                  }
                  .flowOn(Dispatchers.Default) // Changes the context of flow
                  .catch { throwable ->
                      // Catch exceptions in all down stream flow
                      // Any error occurs after this catch operator
                      // will not be caught here
                      println(throwable)
                  }

              // Merge flows when consumer gets
              val weatherForecast: Flow<Result<Int>>
                  get() = merge(_weatherForecast, _weatherForecastOtherDataSource)

              /**
               * This function converts given [celsius] to Fahrenheit.
               *
               * Fahrenheit degree = Celsius degree * 9 / 5 + 32
               *
               * @return Fahrenheit integer for [celsius]
               */
              private fun convertCelsiusToFahrenheit(celsius: Int) = celsius * 9 / 5 + 32
          }

          唯一剩下的就是Fragment中對Flow實現(xiàn)collect。

          class WeatherForecastDataStreamFlowFragment : DaggerFragment() {
            
              ...

              override fun onActivityCreated(savedInstanceState: Bundle?) {
                  super.onActivityCreated(savedInstanceState)

                  // Obtain viewModel
                  viewModel = ViewModelProviders.of(
                      this,
                      viewModelFactory
                  ).get(WeatherForecastDataStreamFlowViewModel::class.java)

                  // Consume data when fragment is started
                  lifecycleScope.launchWhenStarted {
                      // Since collect is a suspend function it needs to be called
                      // from a coroutine scope
                      viewModel.weatherForecast.collect {
                          when (it) {
                              Result.Loading -> {
                                  Toast.makeText(context, "Loading", Toast.LENGTH_SHORT).show()
                              }
                              is Result.Success -> {
                                  // Update weather data
                                  tvDegree.text = it.data.toString()
                              }
                              Result.Error -> {
                                  Toast.makeText(context, "Error", Toast.LENGTH_SHORT).show()
                              }
                          }
                      }
                  }
              }
          }

          這些只是部分Flow運算符。你可以從這里找到整個操作符的列表。

          https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/index.html

          注意:移除LiveData會增加配置變化的額外工作。為了保留配置變化,你需要緩存最新的值。你可以從這里查看Dropbox存儲庫如何處理緩存。

          Search bar using Channel and Flow

          在這個播客中,Sean McQuillan舉了一個例子,說明如何使用Channel和Flow創(chuàng)建一個搜索欄。這個想法是要有一個帶有過濾列表的搜索欄。每當用戶在搜索欄中輸入一些東西時,列表就會被搜索欄中的文本過濾掉。這是通過在channel中保存文本值和觀察通過該channel的流量變化來實現(xiàn)的。

          為了演示這個例子,讓我們有一個城市列表和一個搜索欄。最后,它看起來會是這樣的。

          img

          我們將在Fragment里有一個EditText。每當文本被更新時,我們將把它發(fā)送到存儲在ViewModel中的channel。

          etCity.doAfterTextChanged {
              val key = it.toString()

              // Set loading indicator
              pbLoading.show()

              // Offer the current text to channel
              viewModel.cityFilterChannel.offer(key)
          }

          當channel被更新為最新值時,我們將過濾城市并將列表發(fā)送給訂閱者。

          class SearchCityViewModel @Inject constructor() : ViewModel() {
              val cityList = listOf(
                  "Los Angeles""Chicago""Indianapolis""Phoenix""Houston",
                  "Denver""Las Vegas""Philadelphia""Portland""Seattle"
              )

              // Channel to hold the text value inside search box
              val cityFilterChannel = ConflatedBroadcastChannel<String>()

              // Flow which observes channel and sends filtered list
              // whenever there is a update in the channel. This is
              // observed in UI to get filtered result
              val cityFilterFlow: Flow<List<String>> = cityFilterChannel
                  .asFlow()
                  .map {
                      // Filter cities with new value
                      val filteredCities = filterCities(it)

                      // Do some heavy work
                      delay(500)

                      // Return the filtered list
                      filteredCities
                  }

              override fun onCleared() {
                  super.onCleared()

                  // Close the channel when ViewModel is destroyed
                  cityFilterChannel.close()
              }

              /**
               * This function filters [cityList] if a city contains
               * the given [key]. If key is an empty string then this
               * function does not do any filtering.
               *
               * @param key Key to filter out the list
               *
               * @return List of cities containing the [key]
               */
              private fun filterCities(key: String): List<String> {
                  return cityList.filter {
                      it.contains(key)
                  }
              }
          }

          然后,只需觀察Fragment中的變化。

          lifecycleScope.launchWhenStarted {
              viewModel.cityFilterFlow.collect { filteredCities ->
                  // Hide the progress bar
                  pbLoading.hide()

                  // Set filtered items
                  adapter.setItems(filteredCities)
              }
          }

          好了,我們剛剛實現(xiàn)了一個使用channel和流??的搜索和過濾機制。

          3

          https://proandroiddev.com/using-livedata-flow-in-mvvm-part-iii-8703d305ca73

          第三篇文章主要是針對Flow的測試,這篇文章我相信大家在國內(nèi)幾乎用不上,所以,感興趣的朋友可以自己去看下。

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

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

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



          往期推薦


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

          更文不易,點個“三連”支持一下??


          瀏覽 124
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  学生妹看毛片 | 人人操人人爱人人干 | 成人免费视频 国产免费 | 久久久久亚洲AV成人无在 | 大香蕉www.www |