Using emit with Kotlin StateFlow - android

Until now I was using Flow and mapping it to LiveData like below -
The MyService looks like this -
override fun provideData(action: MyAction) = flow {
emit(MyResult.Loading)
emit(dataRepository.getNewData())
}
The ViewModel looks like this -
fun getData() = myService.provideData(MyAction.GetData).map {
}.asLiveData(Dispatchers.Default + viewModelScope.coroutineContext)
I want to move to StateFlow. How can I use emit function with StateFlow like I used it with Flow.

You can write your flow as before, but replace .asLiveData(scope) with .stateIn(scope, SharingStarted.Eagerly, null) to get an instance of StateFlow running in the corresponding scope with a similar behavior that you were getting with LiveData before — sharing is started immediately and the initial value is null (just like with LiveData).
You can read here for more details and explanation of all stateIn operator parameters here https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/state-in.html

Related

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.

Send data from repository to view model in MVVM architecture

I'm confused about using MVVM architecture! In some tutorials, LiveData is stored in a repository and then passed to the ViewModel. In many others, LiveData is defined within a function of repository and passed to the ViewModel using the result of the function. But Google says:
It may be tempting to work LiveData objects in your data layer class, but LiveDatais not designed to handle asynchronous streams of data. ........ If you need to use streams of data in other layers of your app, consider using Kotlin Flows and then converting them to LiveData in the ViewModel using asLiveData(). .... For codebases built with Java, consider using Executors in conjuction with callbacks or RxJava.
I prefer to use Kotlin coroutines or Flows. But I do not know if in my case is really needed or not. I'm working on a chat application. When a message is received, a listener is called in the repository and the message data is received in it. (The listener runs in the background thread) Now I want to send the message object to the ViewModel and add it to a LiveData that stores the list of messages.
object ChatsRepo {
fun listen(socket: Socket) {
socket.on(Config.ON_MESSAGE, onMessage)
}
fun sendMessage(socket: Socket, json: String) {
socket.emit(Config.ON_MESSAGE, json)
}
private val onMessage = Emitter.Listener { args: Array<Any> ->
//This message object must be sent to ViewModel
val message = Gson().fromJson(args[0].toString(), Message::class.java)
}
}
I can easily do this using the higher-order function:
object ChatsRepo {
lateinit var listener: (Message) -> Unit
private val onMessage = Emitter.Listener { args: Array<Any> ->
val message = Gson().fromJson(args[0].toString(), Message::class.java)
listener(message)
}
}
But is it better to use Kotlin coroutines or Flows? In some similar cases, a list needs to be sent to the ViewModel.
I can easily do this using the higher-order function.
Right, this is a callback you can use to notify ViewModel about new messages. Kotlin Coroutines help to avoid callbacks and to have a sequential code.
In your case the onMessage is a hot stream of data, we can convert it to a hot Flow using SharedFlow:
private val _messagesFlow = MutableSharedFlow<Message>(extraBufferCapacity = 64)
val messagesFlow: SharedFlow<Message> = _messagesFlow
private val onMessage = Emitter.Listener { args: Array<Any> ->
val message = Gson().fromJson(args[0].toString(), Message::class.java)
messagesFlow.tryEmit(message)
}
In ViewModel if need it is easy to convert it to LiveData using method asLiveData:
ChatsRepo.messagesFlow.asLiveData()
Dependency to use asLiveData() extension function:
def lifecycle_version = "2.4.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
If you are a Rxjava master, I will not recommend you to use flow/livedata. I think flow/livedata is designed for UI Reactive, not for underlying data transform. So you can use rxjava in data repository, and in viewmodel, you can convert it to livedata and use it.
Livedata was never design for reactive streams it was always been for last layer (viewmodel to view), there were/are workaround when livedata was use with retrofit(using calladapter) and room(which google did it).
And now since the rise of coroutines recommended ways is to use kotlin Flows when dealing with business logic but things to be notice:
1- regular flow are not observables but livedata is observable.
2- it's not good to manage UI state with regular flows but you can do it with livedata.
3- livedata is lifecycle aware but flows are not.
4- livedata is not reactive but flows are.
5- livedata gives you the only latest value what it receives but regular flows throws values from bucket one by one
you might have notice i said alot of regular flows not flow cause there are other mechanism related to flows you can manage UI state that are stateFlow and sharedFlow.
Stateflow is the replacement of livedata the downside is you have to code a little bit more to make it lifecycle aware cause it is not prebuilt and stateflow always gives you the last value it persists during the configuration changes or screen switching(fragment navigation).
sharedFlows is good for managing the one time event like toastMsgs, snackbar etc. sharedFlow was actually the replacement of BroadcastChannels.

How to know when job from viewModel is done

I am trying to figure out how jobs with coroutines work. Basically, I want to launch this coroutine from FirstFragment and after that navigate to SecondFragment and get notified when this job is done. I call getData() in FirstFragment onViewCreated() and navigate to SecondFragment. Whether I write getData().isCompleted or getData().invokeOnCompletion { } in SecondFragment nothing happens. I don't know if I am missing something or not starting job correctly or something else.
private val _data = MutableStateFlow<GetResource<String>?>(null)
val data: StateFlow<GetResource<String>?> = _data
fun getData() = viewModelScope.launch {
repository.getData().collect {
_data.value = it
}
}
A Flow from a database never completes because it is supposed to monitor the database for changes indefinitely. It only stops when the coroutine is cancelled. Therefore the Job that collects such a Flow will never complete. Also, if you call getData() on the repo again, you are getting a new Flow instance each time.
Regardless of what you're doing, you need to be sure you are using the same ViewModel instance between both fragments by scoping it to the Activity. (Use by activityViewModels() for example.) This is so the viewModelScope won't be cancelled during the transition between Fragments.
If all you need is a single item from the repo one time, probably the simplest thing to do would be to expose a suspend function from the repo instead of a Flow. Then turn it into a Deferred. Maybe by making it a Lazy, you can selectively decide when to start retrieving the value. Omit the lazy if you just want to start retrieving the value immediately when the first Fragment starts.
// In the shared view model:
val data: Deferred<GetResource<String>> by lazy {
viewModelScope.async {
repository.getData() // suspend function returning GetResource<String>
}
}
fun startDataRetrieval() { data } // access the lazy property to start its coroutine
// In second fragment:
lifecycleScope.launch {
val value = mySharedViewModel.data.await()
// do something with value
}
But if you have to have the Flow because you’re using it for other purposes:
If you just want the first available value from the Flow, have the second Fragment monitor your data StateFlow for its first valid value.
lifecycleScope.launch {
val value = mySharedViewModel.data.filterNotNull().first()
// do something with first arrived value
}
And you can use SharedFlow so you don’t have to make the data type nullable. If you do this you can omit filterNotNull() above. In your ViewModel, it’s easier to do this with shareIn than your code that has to use a backing property and manually collect the source.
val data: SharedFlow<GetResource<String>> = repository.getData()
.shareIn(viewModelScope, replay = 1, SharingStarted.Eagerly)
If you need to wait before starting the collection to the SharedFlow, then you could make the property lazy.
Agreed with #Tenfour04 's answer, I would like to contribute a little more.
If you really want to control over the jobs or Structured Concurrency, i would suggest use custom way of handling the coroutine rather than coupled your code with the viewModelScope.
There are couple of things you need to make sure:
1- What happen when cancellation or exception occurrs
2- you have to manage the lifecycle of the coroutine(CoroutineScope)
3- Cancelling scope, depends on usecase like problem facing you are right now
4- Scope of ViewModel e.g: Either it is tied to activity(Shared ViewModel) or for specific fragment.
If you are not handling either of these carefully specifically first 3, your are more likely to leaking the coroutine your are gurenteed gonna get misbehavior of you app.
Whenever you start any coroutine in Custom way you have to make sure, what is going to be the lifecycle, when it gonna end, This is so important, it can cause real problems
Luckily, i have this sample of CustomViewModel using Jobs: Structured Concurrency sample code

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