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,
StateFlow
You have to have an initial value,LiveData
Don’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:
- use
ktx
将Flow
convertLiveData
. - 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:
emit
Method: When the cache policy isBufferOverflow.SUSPEND
When,emit
Methods hang until new cache space is available.tryEmit
Methods:tryEmit
Will return aBoolean
Value,true
It stands for successful delivery,false
Represents 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:
SharingStarted.WhileSubscribed()
: Keeps the upstream provider active when there are subscribers.SharingStarted.Eagerly
: Start the provider immediately.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