preface

Earlier in Learn-as-You-Go Android Jetpack-Paging 3, we talked about the pits we encountered because of state logging.

A brief description:

Clicking on the crown button in the image will bring up buttons for Nike, Adidas and other brands. Once selected, the data source in the page will only contain the data for that brand.

Take a quick look at the original structure, using LiveData:

class MainViewModel : ViewModel() {
    private val selectedBrand = MutableLiveData<String>()

    // 2. The data source is automatically switched based on selectedBrand
    val shoes: LiveData<Shoe> = selectedBrand.switchMap {
        / /... Switch to the corresponding data source
    }

    // 1 Select the brand
    fun selectBrand(brand: String) {
        selectedBrand.value = brand
    }
}
Copy the code

The core of the original state record is LiveData. When the state side selectedBrand changes, the extension method switchMap will be used to transform it into LiveData, the corresponding data source in Paging 2.

This set of state management is theoretically feasible in Paging 3, but Paing 3 no longer actively supports switching to subthreads when retrieving data, while Room of the database does not support initiating requests in the main thread.

Maybe we could create a child thread first and then initiate the request, but the data source LiveData will throw another error, because the LiveData is passed through the setValue method, those of you who have used LiveData should know that, SetValue cannot be called in child threads.

Using LiveData doesn’t seem to work anymore. After a reminder from @Yu Jinyan, use StateFlow, which is our hero today.

The official document: developer.android.com/kotlin/flow…

First, cold flow or hot flow

Before I get into the main body, LET me introduce you to the concepts, cold flow and heat flow.

If you’ve read About Kotlin’s coroutine, then you probably know that Flow is cold Flow. What is cold Flow? In simple terms, if the Flow has a subscriber Collector, the emitted value will actually live in memory, much like lazy loading.

In contrast, there are heat flows. StateFlow and SharedFlow are heat flows that exist in memory and are active before garbage collection.

Second, the StateFlow

For StateFlow, the official description is:

StateFlow is a state-container-like stream of observable data that issues current and new state updates to its collector.

At first glance, there is no difference with Flow, but when you look at the code, there is a big difference.

1. The StateFlow use

Step 1: Create MutableStateFlow and set the initialized value.

class MainViewModel : ViewModel() {
    val selected = MutableStateFlow<Boolean> (false)}Copy the code

Step 2: Same as Flow, use collect method:

lifecycleScope.launch {
    viewModel.selected.collect {
        / /... Causes changes to the UI
        // For example, whether a button is selected}}Copy the code

Step 3: We can set the selected value to cause the Ui layer to change:

class MainViewModel : ViewModel() {
    val selected = MutableStateFlow<Boolean> (false)
    fun doSomeThing(value: Boolean) {
        selected.value = value
    }
}
Copy the code

Normal Flow does not have the ability to select. Value = value.

If you look closely, the experience is exactly the same as LiveData, so the usage scenario is similar to LiveData.

2. Compare with LiveData

So what’s the difference between StateFlow and LiveData?

There are two differences:

  • The first point,StateFlowYou have to have an initial value,LiveDataDon’t need.
  • Second, liveData.observe () automatically unregisters the user when the View becomes STOPPED, but collecting data from StateFlow or any other data flow does not.

For StateFlow to remain active when the interface is destroyed, there are two solutions:

  • usektxFlowconvertLiveData.
  • Manually cancel the interface destruction (this is easy to forget).
class LatestNewsActivity : AppCompatActivity() {...// Coroutine listening for UI states
    private var uiStateJob: Job? = null

    override fun onStart(a) {
        super.onStart()
        // Start collecting when the View is visibleuiStateJob = lifecycleScope.launch { latestNewsViewModel.uiState.collect { uiState -> ... }}}override fun onStop(a) {
        // Stop collecting when the View goes to the backgrounduiStateJob? .cancel()super.onStop()
    }
}
Copy the code

3. How to solve the problem at the beginning

The solution to the initial question is as follows:

class MainViewModel : ViewModel() {
    private val selectedKind = MutableStateFlow("All")
    // 2. The data source is automatically switched based on selectedBrand
    val shoes: Flow<Shoe> = selectedKind.flatMapLatest {
        / /... Switch to the corresponding data source
    }
    // 1. Select the brand
    fun selectedBrand(brand: String) {
        selectedKind.value = brand
    }
}
Copy the code

What the flatMapLatest method does: It generates a new Flow, but only the most recently received value. For example, IF I select Nike first and Adidas later, but because of the delay, both signals are received at the same time, then only the data Flow from Adidas will be requested.

New structure:

Because StateFlow is a heat flow, when it has an initial value, it can be converted to the corresponding data source through flatMapLatest at the very beginning, and the value can be set by selectedBrand to dynamically cause the change of data flow shoes.

Third, SharedFlow

Like StateFlow, SharedFlow is a hot flow that can send already-sent data to new subscribers with high configurability.

1. Application scenario of SharedFlow

In general, SharedFlow is similar to StateFlow in that both are hot flows and can be used to store state, but SharedFlow is flexible in configuration.

Use SharedFlow when you have the following scenarios:

  • When a subscription occurs, n values that have been updated in the past need to be synchronized to the new subscriber.
  • Configure a cache policy.

2. Use of SharedFlow

So let’s just write a Demo.

Step 1: Create a MutableSharedFlow. The corresponding parameters are explained in the comments

class MainViewModel : ViewModel() {
    val sharedFlow = MutableSharedFlow<Int> (5 When a new subscriber collects, it sends several already sent data to it
        , 3 // How much data does MutableSharedFlow cache
        , BufferOverflow.DROP_OLDEST // Parameter three: cache policy, three discard the latest value, discard the oldest value and suspend)}Copy the code

Step 2: Use the emit or tryEmit methods

class MainViewModel : ViewModel() { val sharedFlow = MutableSharedFlow<Int>( // .... ) Init {for (I in 0.. 10) {sharedflow.tryemit (I)}} // Call fun doAsClick() {for (I in 11.. 20) { sharedFlow.tryEmit(i) } } }Copy the code

When the amount of cached data in MutableSharedFlow exceeds the threshold, the emit and tryEmit methods will behave differently:

  • emitMethod: When the cache policy isBufferOverflow.SUSPENDWhen,emitMethods hang until new cache space is available.
  • tryEmitMethods:tryEmitWill return aBooleanValue,trueIt stands for successful delivery,falseRepresents a callback that suspends the data launch until new cache space is available.

Step 3: Receive data Receive data in the same way as normal Flow.

Here is my entire code:

class MainActivity : AppCompatActivity() {

    private lateinit var viewModel: MainViewModel

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

        viewModel = ViewModelProvider(this).get(com.example.coroutinedemo.viewmodel.MainViewModel::class.java)

        val tvContent = findViewById<TextView>(R.id.tv_content)
        // Start the first coroutine and receive the initialized data
        lifecycleScope.launch {
            val sb = StringBuffer()
            viewModel.sharedFlow.collect {
                sb.append("< <${it}")
                tvContent.text = sb
            }
        }

        val btnGo = findViewById<Button>(R.id.btn_go)
        val tvTwo = findViewById<TextView>(R.id.tv_2)
        btnGo.setOnClickListener {
            // Send new data
            viewModel.doAsClick()
            // After sending the new data, start the second coroutine
            lifecycleScope.launch {
                val sb = StringBuffer()
                viewModel.sharedFlow.collect {
                    sb.append("< <${it}")
                    tvTwo.text = sb.toString()
                }
            }
        }
    }
}
Copy the code

Click on btnGo and guess what tvContent and tvTwo show respectively.

TvContent on top, tvTwo on the bottom.

3. Turn cold flow into SharedFlow

Use the Flow extension shareIn to use the code from the official website:

class NewsRemoteDataSource(... , private val externalScope: CoroutineScope, ) { val latestNews: Flow<List<ArticleHeadline>> = flow { ... }. ShareIn (externalScope, replay = 1, started = SharingStarted WhileSubscribed () / / start policy)}Copy the code

The focus is on parameter three, which provides three startup policies respectively:

  1. SharingStarted.WhileSubscribed(): Keeps the upstream provider active when there are subscribers.
  2. SharingStarted.Eagerly: Start the provider immediately.
  3. SharingStarted.Lazily: Start sharing data after the first subscriber appears and keep the data flow active forever.

conclusion

Flow strikes me as an ancient printing technique, where a set layout cannot be changed, but multiple pages can be printed on the same layout. StateFlow feels to me like moving-type printing, with the ability to change the layout constantly or to print a lot of content on the same layout.

If you want to use Flow to record the state of data, StateFlow and SharedFlow are good choices. StateFlow and SharedFlow provide the ability to update data liveData-style within flows, but there are lifecycle issues to be aware of if you want to use it at the UI level.

Compared with SharedFlow, StateFlow needs to provide an initial value. SharedFlow has flexible configuration and can provide old data synchronization and cache configuration functions.

Wonderful content

If you think this article is good, “like” is the biggest encouragement to the author ~

Technology more than, the article has material, concern public number nine heart said, a high quality good article every week, and nine hearts in Dachang road side by side.

Reference links:

The official documentation