Issue
Whenever you call .observe()
on LiveData, the Observer receives the last value of that LiveData. This may be useful in some cases, but not in mine.
Whenever I call
.observe()
, I want the Observer to receive only future LiveData changes, but not the value it holds when.observe()
is called.I may have more than one Observer for a LiveData instance. I want them all to receive LiveData updates when they happen.
I want each LiveData update to be consumed only once by each Observer. I think is just a re-phrasing of the first requirement, but my head is spinning already and I'm not sure about it.
While googling this problem, I came upon two common approaches:
Wrap the data in an
LiveData<SingleEvent<Data>>
and check in thisSingleEvent
class if it was already consumed.Extend
MediatorLiveData
and use a look-up-map if the Observer already got the Event
Examples for these approaches can be found here: https://gist.github.com/JoseAlcerreca/5b661f1800e1e654f07cc54fe87441af#gistcomment-2783677 https://gist.github.com/hadilq/f095120348a6a14251a02aca329f1845#file-liveevent-kt https://gist.github.com/JoseAlcerreca/5b661f1800e1e654f07cc54fe87441af#file-event-kt
Unfortunately none of these examples solves all my requirements. Most of the time, the problem is that any new Observer still receives the last LiveData value upon subscribing. That means that a Snackbar which was already shown is displayed again and again whenever the user navigates between screens.
To give you some insights what I am talking about / what I am coding about:
I am following the LiveData MVVM design of the Android Architecture Componentns:
- 2 ListFragment are showing a list of entries.
- They are using 2 instances of the same ViewModel class to observe UI-related LiveData.
- The user can delete an entry in such a ListFragment. The deletion is done by the ViewModel calling
Repository.delete()
- The ViewModel observes the Repository for
RepositoryEvents
.
So when the deletion is done, the Repository informs the ViewModel about it and the ViewModel inform the ListFragment about it.
Now, when the user switches to the second ListFragment the following happens:
- The second Fragment gets created and calls
.observe()
on its ViewModel The ViewModel gets created and calls
.observe()
on the RepositoryThe Repository sends its current
RepositoryEvent
to the ViewModel- The ViewModel send the according UI Event to the Fragment
- The Fragment shows a confirmation Snackbar for a deletion that happened somewhere else.
Heres some simplified code:
Fragment:
viewModel.dataEvents.observe(viewLifecycleOwner, Observer { showSnackbar() })
viewModel.deleteEntry()
ViewModel:
val dataEvents: LiveData<EntryListEvent> = Transformations.switchMap(repository.events, ::handleRepoEvent)
fun deleteEntry() = repository.deleteEntry()
private fun handleRepoEvent(event: RepositoryEvent): LiveData<EntryListEvent> {
// convert the repository event to an UI event
}
Repository:
private val _events = MutableLiveData<RepositoryEvent>()
val events: LiveData<RepositoryEvent>
get() = _events
fun deleteEntry() {
// delete it from database
_events.postValue(RepositoryEvent.OnDeleteSuccess)
}
Solution
UPDATE 2021:
Using the coroutines library and Flow it is now very easy to achieve this by implementing Channels
:
MainActivity
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
import com.plcoding.kotlinchannels.databinding.ActivityMainBinding
import kotlinx.coroutines.flow.collect
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
binding.btnShowSnackbar.setOnClickListener {
viewModel.triggerEvent()
}
lifecycleScope.launchWhenStarted {
viewModel.eventFlow.collect { event ->
when(event) {
is MainViewModel.MyEvent.ErrorEvent -> {
Snackbar.make(binding.root, event.message, Snackbar.LENGTH_LONG).show()
}
}
}
}
}
}
MainViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
class MainViewModel : ViewModel() {
sealed class MyEvent {
data class ErrorEvent(val message: String): MyEvent()
}
private val eventChannel = Channel<MyEvent>()
val eventFlow = eventChannel.receiveAsFlow()
fun triggerEvent() = viewModelScope.launch {
eventChannel.send(MyEvent.ErrorEvent("This is an error"))
}
}
Answered By - muetzenflo
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.