Kotlin SharedFlow ViewModel emits before subscribed - kotlin

I am trying to use SharedFlow as data provider for a Fragment in MVVM architecture.
In the Fragment class:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.data.collect { value ->
handleData(data)
}
}
}
viewModel.init()
}
In the ViewModel class:
private val _data: MutableSharedFlow<DataState> = MutableSharedFlow()
val data: SharedFlow<DataState> = _data
fun init() {
...
//(listen for other data providers that generate data for SharedFlow)
...
viewModelCoroutineScope.launch {
val dataCollection = interactor.getDataCollection()
dataCollection.forEach { data ->
if (data != null) {
_data.emit(DataState(data = data))
}
}
}
}
The problem is that in 50% cases viewmodel.init() starts before subscriber under scope is connected to Flow - which results in some data lost.
Why SharedFlow is used? That is because ViewModel have subscriptions to other data sources which could send a lot of data instances in the irregular way all needed to collect, so StateFlow/LiveData with their "store only last value" is not good for this.
When I've tried to pin viewmodel.init() to subscriber coroutine like this:
val job = viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.data.collect { value ->
handleData(data)
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
job.join()
viewModel.init()
}
the ViewModel emits data, but Fragment is never collects it.
What is right way to guarantee that subscribers is on before call of the ViewModel to start data sending through SharedFlow?

You should give your SharedFlow a replay value of 1 so late subscribers will still get the most recent value. You need this anyway. If the screen rotates, the recreated Fragment will need the latest value to show in the UI.
private val _data: MutableSharedFlow<DataState> = MutableSharedFlow(replay = 1)
But actually, it would be better to use shareIn instead of MutableSharedFlow, because then you can pause collection when there are no active subscribers, so you can avoid unnecessary monitoring of resources when the associated Fragment is off-screen. Like this:
val data: SharedFlow<DataState> = interactor.getDataCollection()
.mapNotNull { it?.let(::DataState) }
.shareIn(viewModelScope, SharingStarted.whileSubscribed(5000L), replay = 1)
If getDataCollection() is a suspend function, you could do it like this:
val data: SharedFlow<DataState> = flow {
interactor.getDataCollection().emitAll()
}
.mapNotNull { it?.let(::DataState) }
.shareIn(viewModelScope, SharingStarted.whileSubscribed(5000L), replay = 1)
If it's not a suspend function, why do you have a getter function at all? Kotlin uses properties instead.

Related

Kotlin flows SharedFlow not received in collectInLifeCycle inside Fragment

I am observing inside a fragment the events of a sharedflow such as this:
myEvent.collectInLifeCycle(viewLifecycleOwner) { event ->
when (state) {
//check the event. The event emited form onStart is never reached here :(
}
}
Whereas in the viewmodel I have
private val _myEvent = MutableSharedFlow<MyEvent>()
val myEvent: SharedFlow<MyEvent> = _myEvent
fun loadData() =
viewModelScope.launch {
getDataUseCase
.safePrepare(onGenericError = { _event.emit(Event.Error(null)) })
.onStart { _event.emit(Event.Loading) }
.onEach { result ->
result.onSuccess { response ->
_event.emit(Event.Something)
}
}
.launchIn(viewModelScope)
}
So the problem is that only the Event.Something is the one being properly collected from the fragment, whereas _event.emit(Event.Loading) is not being collected... If I debug it goes to the onStart, but it is never called in the fragment.
Your SharedFlow needs to have a replay so that collectors always get at least the most recent value. Otherwise, if you emit to the Flow before the collector is registered, it will never see anything emitted. Do this:
private val _myEvent = MutableSharedFlow<MyEvent>(replay = 1)
Personally, unless I'm missing some detail here that would change my mind, I would simplify all your code to avoid having to manually call loadData(). Something like this but I'm guessing a bit because I don't know all your types and functions.
val myEvent: SharedFlow<MyEvent> = flow {
emit(Event.Loading)
emitAll(
getDataUseCase
.transform { result ->
result.onSuccess { response ->
emit(Event.Something)
}
}
.catch { error -> emit(Event.Error(null)) }
)
}.shareIn(viewModelScope, SharingStarted.Lazily, replay = 1)

LiveData Observer isn't triggered for the second time

I'm expecting that the observer will be triggered when I'm hitting API by clicking one of the side menu. When I clicked one of the menu, Retrofit actually gave me the response with the correct value. The problem is, the Observer isn't getting triggered for the second time. I've trace the problem and find out that my Repository isn't returning a value even though my Retrofit already update the MutableLiveData.
RemoteDataSource.kt
override fun getDisastersByFilter(filter: String?): LiveData<ApiResponse<DisastersDTO?>> {
val result = MutableLiveData<ApiResponse<DisastersDTO?>>()
apiService.getDisastersByFilter(filter).enqueue(object : Callback<DisastersResponse> {
override fun onResponse(
call: Call<DisastersResponse>,
response: Response<DisastersResponse>
) {
if(response.isSuccessful) {
val data = response.body()
data?.disastersDTO?.let {
result.postValue(ApiResponse.Success(it))
Log.d("RemoteDataSource", "$it")
} ?: run {
result.postValue(ApiResponse.Error("Bencana alam tidak ditemukan"))
}
} else {
result.postValue(ApiResponse.Error("Terjadi kesalahan!"))
}
}
override fun onFailure(call: Call<DisastersResponse>, t: Throwable) {
result.postValue(ApiResponse.Error(t.localizedMessage!!))
Log.d("RemoteDataSource", t.localizedMessage!!)
}
})
return result
}
Repository.kt
override fun getDisastersByFilter(filter: String?): LiveData<Resource<List<Disaster>>> =
remoteDataSource.getDisastersByFilter(filter).map {
when (it) {
is ApiResponse.Empty -> Resource.Error("Terjadi error")
is ApiResponse.Error -> Resource.Error(it.errorMessage)
is ApiResponse.Loading -> Resource.Loading()
is ApiResponse.Success -> Resource.Success(
DataMapper.disastersResponseToDisasterDomain(
it.data
)
)
}
}
SharedViewModel.kt
fun getDisastersByFilter(filter: String? = "gempa"): LiveData<Resource<List<Disaster>>> =
useCase.getDisastersByFilter(filter)
Here's the **MapsFragment**
private val viewModel: SharedViewModel by activityViewModels()
viewModel.getDisastersByFilter("gempa").observe(viewLifecycleOwner) {
when (it) {
is Resource.Success -> {
Log.d("MapsFragmentFilter", "${it.data}")
it.data?.let { listDisaster ->
if(listDisaster.isNotEmpty()) {
map.clear()
addGeofence(listDisaster)
listDisaster.map { disaster ->
placeMarker(disaster)
addCircle(disaster)
}
}
}
}
is Resource.Error -> Toast.makeText(context, "Filter Error", Toast.LENGTH_SHORT).show()
is Resource.Loading -> {}
}
}
Here's the MainActivity that triggers the function to hit API
private val viewModel: SharedViewModel by viewModels()
binding.navViewMaps.setNavigationItemSelectedListener { menu ->
when (menu.itemId) {
R.id.filter_gempa -> viewModel.getDisastersByFilter("gempa")
R.id.filter_banjir -> viewModel.getDisastersByFilter("banjir")
R.id.about_us -> viewModel.getDisasters()
}
binding.drawerLayoutMain.closeDrawers()
true
}
I can't be sure from what you've posted, but your menu options call getDisastersByFilter on your SharedViewModel, and it looks like that eventually calls through to getDisastersByFilter in RemoteDataSource.
That function creates a new LiveData and returns it, and all your other functions (including the one in viewModel) just return that new LiveData. So if you want to see the result that's eventually posted to it, you need to observe that new one.
I don't know where the fragment code you posted is from, but it looks like you're just calling and observing viewModel.getDisastersByFilter once. So when that first happens, it does the data fetch and you get a result on the LiveData it returned. That LiveData won't receive any more results, from the looks of your code - it's a one-time, disposable thing that receives a result later, and then it's useless.
If I've got that right, you need to rework how you're handling your LiveDatas. The fragment needs to get the result of every viewModel.getDisastersByFilter call, so it can observe the result - it might be better if your activity passes an event to the fragment ("this item was clicked") and the fragment handles calling the VM, and it can observe the result while it's at it (pass it to a function that wires that up so you don't have to keep repeating your observer code)
The other approach would be to have the Fragment observe a currentData livedata, that's wired up to show the value of a different source livedata. Then when you call getDisastersByFilter, that source livedata is swapped for the new one. The currentData one gets any new values posted to this new source, and the fragment only has to observe that single LiveData once. All the data gets piped into it by the VM.
I don't have time to do an example, but have a look at this Transformations stuff (this is one of the developers' blogs): https://medium.com/androiddevelopers/livedata-beyond-the-viewmodel-reactive-patterns-using-transformations-and-mediatorlivedata-fda520ba00b7
What I believe you are doing wrong is using LiveData in the first place while using a retrofit.
You are getting a response asynchronously while your code is running synchronously. So, you need to make use of suspending functions by using suspend.
And while calling this function from ViewModel, wrap it with viewModelScope.launch{}
fun getDisastersByFilter(filter: String? = "gempa") = viewModelScope.launch {
useCase.getDisastersByFilter(filter).collect{
// do something....
// assign the values to MutableLiveData or MutableStateFlows
}
}
You should either be using RxJava or CallbackFlow.
I prefer Flows, given below is an example of how your code might look if you use callback flow.
suspend fun getDisastersByFilter(filter: String?): Flow<ApiResponse<DisastersDTO?>> =
callbackFlow {
apiService.getDisastersByFilter(filter)
.enqueue(object : Callback<DisastersResponse> {
override fun onResponse(
call: Call<DisastersResponse>,
response: Response<DisastersResponse>
) {
if (response.isSuccessful) {
val data = response.body()
data?.disastersDTO?.let {
trySend(ApiResponse.Success(it))
// result.postValue(ApiResponse.Success(it))
Log.d("RemoteDataSource", "$it")
} ?: run {
trySend(ApiResponse.Error("Bencana alam tidak ditemukan"))
// result.postValue(ApiResponse.Error("Bencana alam tidak ditemukan"))
}
} else {
trySend(ApiResponse.Error("Terjadi kesalahan!"))
// result.postValue(ApiResponse.Error("Terjadi kesalahan!"))
}
}
override fun onFailure(call: Call<DisastersResponse>, t: Throwable) {
trySend(ApiResponse.Error(t.localizedMessage!!))
// result.postValue(ApiResponse.Error(t.localizedMessage!!))
Log.d("RemoteDataSource", t.localizedMessage!!)
}
})
awaitClose()
}

How can I get data when I convet a cold flow to hot flow using shareIn in Kotlin?

I know that the Flow is cold, I can use collect to get every data just like Code A.
If I convert a Flow to hot flow using shareIn just like Code B, how can I get every data in a hot flow?
CodeA
val simple: Flow<Int> = flow {
for (i in 1..3) {
delay(100)
emit(i)
}
}
class LatestNewsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
simple.collect{ i- > print(i)}
}
}
}
}
Code B
val hotSimple: SharedFlow<Int> = flow {
for (i in 1..3) {
delay(100)
emit(i)
}
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1)
class LatestNewsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
//I want to print every data in hotSimple.
}
}
}
}
The only way to guarantee it is to give shareIn a replay value that is at least as big as the number of values the source flow will produce. This wouldn’t be a practical solution in most cases because you don’t always know how many values the upstream flow will produce, or it might be infinite. Also, every item in the replay is kept in memory, so memory use might be a concern. It is only the emitted values that are replayed, so for new subscribers the side effects and delays will not be replayed along with the emitted values.
The whole point of using a SharedFlow is so it doesn’t have to restart for new subscribers. So typically you would not use a SharedFlow for something where you want every subscriber to receive every value from the beginning.

Kotlin on Android: How to use LiveData from a database in a fragment?

I use MVVM and have a list of data elements in a database that is mapped through a DAO and repository to ViewModel functions.
Now, my problem is rather banal; I just want to use the data in fragment variables, but I get a type mismatch.
The MVVM introduces a bit of code, and for completeness of context I'll run through it, but I'll strip it to the essentials:
The data elements are of a data class, "Objects":
#Entity(tableName = "objects")
data class Objects(
#ColumnInfo(name = "object_name")
var objectName: String
) {
#PrimaryKey(autoGenerate = true)
var id: Int? = null
}
In ObjectsDao.kt:
#Dao
interface ObjectsDao {
#Query("SELECT * FROM objects")
fun getObjects(): LiveData<List<Objects>>
}
My database:
#Database(
entities = [Objects::class],
version = 1
)
abstract class ObjectsDatabase: RoomDatabase() {
abstract fun getObjectsDao(): ObjectsDao
companion object {
// create database
}
}
In ObjectsRepository.kt:
class ObjectsRepository (private val db: ObjectsDatabase) {
fun getObjects() = db.getObjectsDao().getObjects()
}
In ObjectsViewModel.kt:
class ObjectsViewModel(private val repository: ObjectsRepository): ViewModel() {
fun getObjects() = repository.getObjects()
}
In ObjectsFragment.kt:
class ObjectsFragment : Fragment(), KodeinAware {
private lateinit var viewModel: ObjectsViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this, factory).get(ObjectsViewModel::class.java)
// I use the objects in a recyclerview; rvObjectList
rvObjectList.layoutManager = GridLayoutManager(context, gridColumns)
val adapter = ObjectsAdapter(listOf(), viewModel)
// And I use an observer to keep the recyclerview updated
viewModel.getObjects.observe(viewLifecycleOwner, {
adapter.objects = it
adapter.notifyDataSetChanged()
})
}
}
The adapter:
class ObjectsAdapter(var objects: List<Objects>,
private val viewModel: ObjectsViewModel):
RecyclerView.Adapter<ObjectsAdapter.ObjectsViewHolder>() {
// Just a recyclerview adapter
}
Now, all the above works fine - but my problem is that I don't want to use the observer to populate the recyclerview; in the database I store some objects, but there are more objects that I don't want to store.
So, I try to do this instead (in the ObjectsFragment):
var otherObjects: List<Objects>
// ...
if (condition) {
adapter.objects = viewModel.getObjects()
} else {
adapter.objects = otherObjects
}
adapter.notifyDataSetChanged()
And, finally, my problem; I get type mismatch for the true condition assignment:
Type mismatch: inferred type is LiveData<List> but List was expected
I am unable to get my head around this. Isn't this pretty much what is happening in the observer? I know about backing properties, such as explained here, but I don't know how to do that when my data is not defined in the ViewModel.
We need something to switch data source. We pass switching data source event to viewModel.
mySwitch.setOnCheckedChangeListener { _, isChecked ->
viewModel.switchDataSource(isChecked)
}
In viewModel we handle switching data source
(To use switchMap include implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
class ObjectsViewModel(private val repository: ObjectsRepository) : ViewModel() {
// Best practice is to keep your data in viewModel. And it is useful for us in this case too.
private val otherObjects = listOf<Objects>()
private val _loadDataFromDataBase = MutableLiveData<Boolean>()
// In case your repository returns liveData of favorite list
// from dataBase replace MutableLiveData(otherObjects) with repository.getFavorite()
fun getObjects() = _loadDataFromDataBase.switchMap {
if (it) repository.getObjects() else MutableLiveData(otherObjects)
}
fun switchDataSource(fromDataBase: Boolean) {
_loadDataFromDataBase.value = fromDataBase
}
}
In activity/fragment observe getObjects()
viewModel.getObjects.observe(viewLifecycleOwner, {
adapter.objects = it
adapter.notifyDataSetChanged()
})
You can do something like this:
var displayDataFromDatabase = true // Choose whatever default fits your use-case
var databaseList = emptyList<Objects>() // List we get from database
val otherList = // The other list that you want to show
toggleSwitch.setOnCheckedChangeListener { _, isChecked ->
displayDataFromDatabase = isChecked // Or the negation of this
// Update adapter to use databaseList or otherList depending upon "isChecked"
}
viewModel.getObjects.observe(viewLifecycleOwner) { list ->
databaseList = list
if(displayDataFromDatabase)
// Update adapter to use this databaseList
}

How to emit data to kotlin flow [duplicate]

I wanted to know how can I send/emit items to a Kotlin.Flow, so my use case is:
In the consumer/ViewModel/Presenter I can subscribe with the collect function:
fun observe() {
coroutineScope.launch {
// 1. Send event
reopsitory.observe().collect {
println(it)
}
}
}
But the issue is in the Repository side, with RxJava we could use a Behaviorsubject expose it as an Observable/Flowable and emit new items like this:
behaviourSubject.onNext(true)
But whenever I build a new flow:
flow {
}
I can only collect. How can I send values to a flow?
If you want to get the latest value on subscription/collection you should use a ConflatedBroadcastChannel:
private val channel = ConflatedBroadcastChannel<Boolean>()
This will replicate BehaviourSubject, to expose the channel as a Flow:
// Repository
fun observe() {
return channel.asFlow()
}
Now to send an event/value to that exposed Flow simple send to this channel.
// Repository
fun someLogicalOp() {
channel.send(false) // This gets sent to the ViewModel/Presenter and printed.
}
Console:
false
If you wish to only receive values after you start collecting you should use a BroadcastChannel instead.
To make it clear:
Behaves as an Rx's PublishedSubject
private val channel = BroadcastChannel<Boolean>(1)
fun broadcastChannelTest() {
// 1. Send event
channel.send(true)
// 2. Start collecting
channel
.asFlow()
.collect {
println(it)
}
// 3. Send another event
channel.send(false)
}
false
Only false gets printed as the first event was sent before collect { }.
Behaves as an Rx's BehaviourSubject
private val confChannel = ConflatedBroadcastChannel<Boolean>()
fun conflatedBroadcastChannelTest() {
// 1. Send event
confChannel.send(true)
// 2. Start collecting
confChannel
.asFlow()
.collect {
println(it)
}
// 3. Send another event
confChannel.send(false)
}
true
false
Both events are printed, you always get the latest value (if present).
Also, want to mention Kotlin's team development on DataFlow (name pending):
https://github.com/Kotlin/kotlinx.coroutines/pull/1354
Which seems better suited to this use case (as it will be a cold stream).
Take a look at MutableStateFlow documentation as it is a replacement for ConflatedBroadcastChannel that is going to be deprecated, very soon.
For a better context, look at the whole discussion on the original issue on Kotlin's repository on Github.
UPDATE:
Kotlin Coroutines 1.4.0 is now available with MutableSharedFlow, which replaces the need for Channel. MutableSharedFlow cleanup is also built in so you don't need to manually OPEN & CLOSE it, unlike Channel. Please use MutableSharedFlow if you need a Subject-like api for Flow
ORIGINAL ANSWER
Since your question had the android tag I'll add an Android implementation that allows you to easily create a BehaviorSubject or a PublishSubject that handles its own lifecycle.
This is relevant in Android because you don't want to forget to close the channel and leak memory. This implementation avoids the need to explicitly "dispose" of the reactive stream by tying it to the creation and destruction of the Fragment/Activity. Similar to LiveData
interface EventReceiver<Message> {
val eventFlow: Flow<Message>
}
interface EventSender<Message> {
fun postEvent(message: Message)
val initialMessage: Message?
}
class LifecycleEventSender<Message>(
lifecycle: Lifecycle,
private val coroutineScope: CoroutineScope,
private val channel: BroadcastChannel<Message>,
override val initialMessage: Message?
) : EventSender<Message>, LifecycleObserver {
init {
lifecycle.addObserver(this)
}
override fun postEvent(message: Message) {
if (!channel.isClosedForSend) {
coroutineScope.launch { channel.send(message) }
} else {
Log.e("LifecycleEventSender","Channel is closed. Cannot send message: $message")
}
}
#OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun create() {
channel.openSubscription()
initialMessage?.let { postEvent(it) }
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun destroy() {
channel.close()
}
}
class ChannelEventReceiver<Message>(channel: BroadcastChannel<Message>) :
EventReceiver<Message> {
override val eventFlow: Flow<Message> = channel.asFlow()
}
abstract class EventRelay<Message>(
lifecycle: Lifecycle,
coroutineScope: CoroutineScope,
channel: BroadcastChannel<Message>,
initialMessage: Message? = null
) : EventReceiver<Message> by ChannelEventReceiver<Message>(channel),
EventSender<Message> by LifecycleEventSender<Message>(
lifecycle,
coroutineScope,
channel,
initialMessage
)
By using the Lifecycle library from Android, I can now create a BehaviorSubject that cleans itself up after the activity/fragment has been destroyed
class BehaviorSubject<String>(
lifecycle: Lifecycle,
coroutineScope: CoroutineScope,
initialMessage = "Initial Message"
) : EventRelay<String>(
lifecycle,
coroutineScope,
ConflatedBroadcastChannel(),
initialMessage
)
or I can create a PublishSubject by using a buffered BroadcastChannel
class PublishSubject<String>(
lifecycle: Lifecycle,
coroutineScope: CoroutineScope,
initialMessage = "Initial Message"
) : EventRelay<String>(
lifecycle,
coroutineScope,
BroadcastChannel(Channel.BUFFERED),
initialMessage
)
And now I can do something like this
class MyActivity: Activity() {
val behaviorSubject = BehaviorSubject(
this#MyActivity.lifecycle,
this#MyActivity.lifecycleScope
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
behaviorSubject.eventFlow
.onEach { stringEvent ->
Log.d("BehaviorSubjectFlow", stringEvent)
// "BehaviorSubjectFlow: Initial Message"
// "BehaviorSubjectFlow: Next Message"
}
.flowOn(Dispatchers.Main)
.launchIn(this#MyActivity.lifecycleScope)
}
}
override fun onResume() {
super.onResume()
behaviorSubject.postEvent("Next Message")
}
}