Android kotlin set SharingStarted property of MutableSharedFlow without "shareIn" operator - android

My particular implementation concerns the use of kotlin flows in Android, but I guess this is applicable to kotlin in general.
What I would like to do is to set my SharedFlow to be started according to the SharingStarted.WhileSubscribed() started policy, so that the flow materializes only when the number of subscribers is greater than zero.
The recommended way to setup such a flow from the android official guide is to use the shareIn operator:
val latestNews: Flow<List<ArticleHeadline>> = flow {
...
// emit() here
}.shareIn(
externalScope,
replay = 1,
started = SharingStarted.WhileSubscribed()
)
In my case, I want to emit only under specific conditions that are independent from the flow itself, so it is unpractical to emit inside the flow{ ... } body. As a consequence, I created a MutableSharedFlow and use tryEmit to emit whenever I need to, for example upon a method call:
// Backing property to avoid flow emissions from other classes
private val _tickFlow = MutableSharedFlow<Int>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val tickFlow: SharedFlow<Int> = _tickFlow
// called after domain logic deciding which number to emit
fun timeToEmit(num:Int){
_tickerFlow.tryEmit(num)
}
What is the SharingStarted policy (if any) of a flow created through the MutableSharedFlow<>() constructor?
How can I set this flow SharingStarted property to be started (materialized) only when the number of subscribers is greater than 0?

Related

what is the difference between _uiState.stateIn() and _uiState1.asStateFlow() in viewModel Jetpack compose?

In the following code, which shows two different public uiState handler, uiState1 and uiState, what is the difference between the two scenarios?
// UI state exposed to the UI
// Scenario 1
private val _uiState1: MutableStateFlow<InboxUiState> = MutableStateFlow(InboxUiState.default)
val uiState1 = _uiState1.asStateFlow()
// Scenario 2
private val _uiState: MutableStateFlow<InboxUiState> = MutableStateFlow(InboxUiState.default)
val uiState = _uiState
.stateIn(
viewModelScope,
SharingStarted.Eagerly,
InboxUiState.default
)
where asStateFlow() represents a modifiable state flow as a read-only stateflow, stateIn() him converts a cold Flow into a hot StateFlow that is started in the given coroutine scope.
The stateIn operator is useful in situations when there is a cold flow that provides updates to the value of some state and is expensive to create and/or to maintain, but there are multiple subscribers that need to collect the most recent state value.
stateIn : https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/state-in.html
asStateFlow : https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/as-state-flow.html

Flow emits different values when collecting it multiple times

I created a Flow from which I emit data. When I collect this flow twice, there are 2 different sets of data emitted from the same variable instead of emitting the same values to both collectors.
I have a simple Flow that I created myself. The text will be logged twice a second
val demoFlow: Flow<String> = flow {
while (true) {
val text = "Flow ${(0..100).random()}"
Log.d("TAG", text)
emit(text)
delay(1000)
}
}
In my viewModel I have a simple function that gets the previous Flow
fun getSimpleFlow() = FlowRepository.demoFlow
And in my Fragment I collect and display my Flow
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.getSimpleFlow().collect {
binding.tv1.text = it
}
}
launch {
viewModel.getSimpleFlow().collect {
binding.tv2.text = it
}
}
}
}
If I transform the Flow to a StateFlow or a SharedFlow, I no longer have this problem.
I don't understand how or why this happens since I'm using the same 'demoFlow' variable.
Is there a way to get the same values from 'demoFlow' without converting to a StateFlow or a SharedFlow?
Regular Flows are cold, this behaviour is by design.
The demoFlow is the same, so you have the same Flow instance. However, collecting the flow multiple times actually runs the body inside the flow { ... } definition every time from the start. Each independent collection has its own variable i etc.
Using a StateFlow or a SharedFlow allows to share the source of the flow between multiple collectors. If you use shareIn or stateIn on some source flow, that source flow is only collected once, and the items collected from this source flow are shared and sent to every collector of the resulting state/shared flow. This is why it behaves differently.
In short, reusing a Flow instance is not sufficient to share the collection. You need to use flow types that are specifically designed for this.

Create StateFlow from SharedFlow

I have an object where I wish to create hot StateFlow objects from a filtered cold SharedFlow. The intent is that the SharedFlow is an event channel of data changes, but all data can be retrieved to get the current state. This means for a given field, I can find the current state, and then monitor the SharedFlow to get state changes.
I would like to provide an API that (as an example) converts the SharedFlow into a StateFlow in a manner as follows:
var myVariable = DEFAULT_VALUE
val mySharedFlow = MutableSharedFlow<Int>()
val myStateFlow = mySharedFlow
.filter { it < 42 }
.asStateFlow(myVariable)// <- Convert to a StateFlow given a default value
This is obviously an overly simplified example, but my situation is more complex, and currently I have to invoke a function when ever a field changes, but currently I do the following:
myObj.onChange.collect(handler)
handler(myObj.getCurrentValue)
fun handler(data: Int) {
// Handle data change
}
But I would prefer to use a Hot StateFlow and remove the need for the second function call. Especially since many consumers of this are small bits of code (mostly just a single expression) that do not need to be in their own function context, and should just be simple lambdas.
Tenfour04 answered my question in his comment. The function I needed is called stateIn().

Kotlin flow (or something similar) that can be collected with multiple collectors

I tried using Kotlin Flow to be some kind of message container which should pass this message to all observers (collectors). I do not want to use LiveData on purpose because it need to be aware of lifecycle.
Unfortunately I have noticed that if one collector collects message from flow no one else can receive it.
What could I use to achieve "one input - many output".
You can use StateFlow or SharedFlow, they are Flow APIs that enable flows to optimally emit state updates and emit values to multiple consumers.
From the documentation, available here:
StateFlow: is a state-holder observable flow that emits the current and new state updates to its collectors. The current state value can also be read through its value property.
SharedFlow: a hot flow that emits values to all consumers that collect from it. A SharedFlow is a highly-configurable generalization of StateFlow.
A simple example using state flow with view model:
class myViewModel() : ViewModel() {
val messageStateFlow = MutableStateFlow("My inicial awesome message")
}
You can emit a new value using some scope:
yourScope.launch {
messageStateFlow.emit("My new awesome message")
}
You can collect a value using some scope:
yourScope.launch {
messageStateFlow.collect {
// do something with your message
}
}
Attention: Never collect a flow from the UI directly from launch or the launchIn extension function to update UI. These functions process events even when the view is not visible. You can use repeatOnLifecycle as the documentation sugests.
You can try BehaviorSubject from rxJava. Is more comfortable to use than poor kotlin.flow. Seems like this link is for you: BehaviorSubject vs PublishSubject
val behaviorSubject = BehaviorSubject.create<MyObject> {
// for example you can emit new item with it.onNext(),
// finish with error like it.onError() or just finish with it.onComplete()
somethingToEmit()
}
behaviorSubject.subscribe {
somethingToHandle()
}

How to get the value of a Flow outside a coroutine?

How can I get the value of a Flow outside a coroutine similarly to LiveData?
// Suspend function 'first' should be called only from a coroutine or another suspend function
flowOf(1).first()
// value is null
flowOf(1).asLiveData().value
// works
MutableLiveData(1).value
Context
I'm avoiding LiveData in the repository layer in favor of Flow. Yet, I need to set, observe and collect the value for immediate consumption. The later is useful for authentication purpose in a OkHttp3 Interceptor.
You can do this
val flowValue: SomeType
runBlocking(Dispatchers.IO) {
flowValue = myFlow.first()
}
Yes its not exactly what Flow was made for.
But its not always possible to make everything asynchronous and for that matter it may not even always be possible to 'just make a synchronous method'. For instance the current Datastore releases (that are supposed to replace shared preferences on Android) do only expose Flow and nothing else. Which means that you will very easiely get into such a situation, given that none of the Lifecycle methods of Activities or Fragments are coroutines.
If you can help it you should always call coroutines from suspend functions and avoid making runBlocking calls. A lot of the time it works like this. But it´s not a surefire way that works all the time. You can introduce deadlocks with runBlocking.
Well... what you're looking for isn't really what Flow is for. Flow is just a stream. It is not a value holder, so there is nothing for you retrieve.
So, there are two major avenues to go down, depending on what your interceptor needs.
Perhaps your interceptor can live without the data from the repository. IOW, you'll use the data if it exists, but otherwise the interceptor can continue along. In that case, you can have your repository emit a stream but also maintain a "current value" cache that your interceptor can use. That could be via:
BroadcastChannel
LiveData
a simple property in the repository that you update internally and expose as a val
If your interceptor needs the data, though, then none of those will work directly, because they will all result in the interceptor getting null if the data is not yet ready. What you would need is a call that can block, but perhaps evaluates quickly if the data is ready via some form of cache. The details of that will vary a lot based on the implementation of the repository and what is supplying the Flow in the first place.
You could use something like this:
fun <T> SharedFlow<T>.getValueBlockedOrNull(): T? {
var value: T?
runBlocking(Dispatchers.Default) {
value = when (this#getValueBlockedOrNull.replayCache.isEmpty()) {
true -> null
else -> this#getValueBlockedOrNull.firstOrNull()
}
}
return value
}
You can use MutableStateFlow and MutableSharedFlow for emitting the data from coroutine and receiving the data inside Activity/Fragment. MutableStateFlow can be used for state management. It requires default value when initialised. Whereas MutableSharedFlow does not need any default value.
But, if you don't want to receive stream of data, (i.e) your API call sends data only once, you can use suspend function inside coroutine scope and the function will perform the task and return the result like synchronous function call.
To get the value of a Flow outside of a coroutine, the best option is to create the flow as a StateFlow and then call the value property on the StateFlow.
class MyClass {
private val mutableProperty = MutableStateFlow(1)
val property = mutableProperty.asStateFlow()
...
mutableProperty.value = 2
}
...
val readProperty = MyClass().property.value
val propertyAsFlow = MyClass().property as Flow<Int>

Categories

Resources