Create StateFlow from SharedFlow - android

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().

Related

Should Kotlin Flows be used when passing a single object to ViewModel in Android?

On various occasions I've seen people use Kotlin Flows when retrieving a list of objects from a local database, but nowhere have I noticed any warning/error about using them when retrieving single objects (as opposed to a list), or even specification that Flows are to be used only on lists
My question is, will using Flows on single objects cause an error/problem? If not does it hinder performance? In case no, why is it generally not used then?
Sample code of what I mean:
Single object retrieval with Flows:
#Query("SELECT * FROM objects WHERE id = :id")
fun getObjectById(id: Int): Flow<Object>
Retrieving function in ViewModel:
fun objectRetrieval(id: Int) {
// ... More code
getObjectById(id)
.onEach { object: Object ->
// Operations
}
.launchIn(viewModelScope)
}
Thank you for your time!
I think there are multiple misconceptions here, both in the question and in comments/answer.
Flows are indeed related to sequences of values processed asynchronously. In simple words that means: if in the future we plan to receive some data multiple times, flows are just for this purpose. Examples are: user events, incoming messages in an instant messaging application, etc.
If we plan to receive the data only once, we can still use flow, but this is an overkill and it adds unnecessary complication. In these cases it is better to use a suspend function as it is easier to use and it is very clear about the fact the data is received only once.
However, it doesn't really matter, if the data we receive is a single object or a list of objects. As a matter of fact, list is an object as well:
suspend fun foo(): String - receive a single object once.
suspend fun foo(): List<String> - receive a list of objects, all at once.
fun foo(): Flow<String> - receive a single object multiple times.
fun foo(): Flow<List<String>> - receive a list of objects multiple times.
All of above cases make sense and can be used in different scenarios.
Now, going to your specific example, there is another misconception there. Your getObjectById() doesn't receive only a single value, but a sequence of values. By using Flow we say to Room that we want to observe the data for changes. First, it sends you the current data for the object, but if the object ever changes, you receive updated data again.
If you want to only get the current data and do not observe for changes, use a suspend function instead.
Yes you should flow single objects Singe Source of Truth Architecture
val numLightsObserver: Observer<Int> = Observer { i: Int ->
i.let { jx.setTitle(i.toString()) }
}
wordViewModel.numLights.observe(this, numLightsObserver)
Now I have a fully automated object for my UI what will I do with the number of lights. Above I convert it to a string for this demo commit.
here's a link to commit diff I had to modify 4 files to make it work
diff on github where I try it flow a single object

Retrieving latest value of a Flow which is not a StateFlow

How can I get the latest value of a Flow? I don't have a StateFlow where I need that latest value. This is the condensed scenario:
There is a repository exposing a StateFlow
val repositoryExposedStateFlow: StateFlow<SomeType> = MutableStateFlow(...)
Additionally there are mappers transforming that StateFlow like
val mappedFlow: Flow<SomeOtherType> = repositoryExposedStateFlow.flatMapLatest { ... }
mappedFlow is no StateFlow anymore, but just a Flow. Thus, I cannot get the latest/current value as I can when there's StateFlow.
Anyhow, I need the latest value in that Flow at some point. Since this point is not in a ViewModel, but some Use Case implementation, I cannot simply perform a stateIn and hold the latest value in the ViewModel all the time the ViewModel is alive -- otherwise I had to pass on the value to all Use Cases. Actually, within a Use Case I trigger a network refresh which leads to emitting of new values on the StateFlow and thus on the mappedFlow, too.
In the Use Cases I have CoroutineScopes though. So I came up with
suspend fun <T> Flow<T>.getState(): T {
return coroutineScope {
val result = stateIn(
scope = this
).value
coroutineContext.cancelChildren()
result
}
}
Without using coroutineContext.cancelChildren() the method will never return, because coroutineScope blocks the caller until all child coroutines have finished. As stateIn never finishes, I manually cancel all children.
Apparently this is a bad thing to do.
But how can I solve this problem in a better way? In my perception the problem arises from StateFlow mapping resulting in regular Flow instances.
Yes, all you need is to call first() on the flow. Since it is backed by a StateFlow upstream, the first() call will get the current value of that backing StateFlow, run it through whatever transformations happen from the downstream operators, and return that value.
This effectively gets you the same result as your attempt above.
The downside is that all the downstream operators must be run, so it is potentially expensive.
This is only possible if there is an upstream StateFlow. Otherwise, there is no concept of a latest value for you to be able to retrieve.
I would challenge your need to get the latest value, though. Typically, you collect flows, so you're already working with a current value. Flows are intended for reactive programming.

Converting livedata to stateflow

Android, Kotlin
I have the following livedata in my datasource class, I cannot change this to StateFlow, so need to convert it to StateFlow in my viewModel
val trackingCatalogInitialLoadLiveData: LiveData<Pair<CatalogTracking, Int>> by lazy {
instantSearchDataSourceLiveData.switchMap { instantSearchDataSource ->
instantSearchDataSource.initialLoadLiveData
}
}
In My ViewModel I have the following, and this is the part I am not sure about if this is the correct way to convert LiveData to StateFlow:
val trackingCatalogInitialLoadStateFlow: StateFlow<Pair<CatalogTracking, Int>> by lazy {
instantSearchDataSourceFactory.trackingCatalogInitialLoadLiveData.asFlow()
.stateIn(viewModelScope, SharingStarted.Lazily, Pair(CatalogTracking(), 0))
}
Then in my fragment I just collect the results
coroutineScope.launch {
mInstantSearchViewModel.trackingCatalogInitialLoadStateFlow.collect { trackingPair ->
// code here
}
Is this the best practice to convert LiveData to StateFlow? Anything I should be looking out for?
You don't need to use by lazy. asFlow() and stateIn() both create simple wrappers, so they are trivial to call directly in the property initializer.
As #Joffrey said, if you use SharingStarted.Lazily, inspecting the flow's value before it has any collectors will incorrectly show your provided initial value. Since LiveData is hot, starting your StateFlow lazily doesn't buy you a lot. The underlying coroutine that transfers LiveData values to the StateFlow is doing a trivial amount of work.
If you don't need to inspect the value (in most cases you probably don't), then it should be fine to leave it as a cold Flow. Even though the Flow from asFlow() is cold, the underlying LiveData is still hot, so when collectors of the flow collect it, they'll always get the latest value. The main behavior difference would be if your data source does not provide a guaranteed initial value for the LiveData, then a StateFlow gives you the opportunity to emit your provided default initially without waiting for the LiveData to publish its first value.

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

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?

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