I'm struggling with a LiveData observer which is firing twice. In my fragment I'm observing a LiveData as below, using viewLifeCycleOwner as LifeCycleOwner
private lateinit var retailViewModel: RetailsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retailViewModel = ViewModelProviders.of(this).get(RetailsViewModel::class.java)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
retailViewModel.retailLiveData.observe(viewLifecycleOwner, Observer {
// updating UI here, but firing twice!
}
retailViewModel.getRetailById(retail.id)
}
And this is my view model:
class RetailsViewModel(override val service: MyFoodyApiService = MyFoodyApiService.service) :
BaseViewModel(service) {
var retailLiveData: MutableLiveData<Retail> = MutableLiveData()
fun getRetailById(id: Int) {
scope.launch {
try {
val response =
service.getRetailById(authString, id).await()
when (response.isSuccessful) {
true -> {
response.body()?.let { payload ->
retailLiveData.postValue(payload.data)
} ?: run {
errorLiveData.postValue("An error occurred: ${response.message()}")
}
}
false -> errorLiveData.postValue("An error occurred: ${response.message()}")
}
} catch (e: Exception) {
noConnectionLiveData.postValue(true)
}
}
}
}
When I run the fragment for the first time, everything works fine, however when I go to its DetailFragment and come back, retailLiveData Observer callback is fired twice. According to this article this was a known problem solved with the introduction of viewLifeCycleOwner who should be helpful to remove active observers once fragment's view is destroyed, however it seems not helping in my case.
This happens because view model retains value when you open another fragment, but the fragment's view is destroyed. When you get back to the fragment, view is recreated and you subscribe to retailLiveData, which still holds the previous value and notifies your observer as soon as fragment moves to started state. But you are calling retailViewModel.getRetailById(retail.id) in onViewCreated, so after awhile the value is updated and observer is notified again.
One possible solution is to call getRetailById() from view model's init method, the result will be cached for view model lifetime then.
Related
So I have this in an onCreate in an activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
lifecycleScope.launch(Dispatchers.IO) {
repeatOnLifecycle(Lifecycle.State.CREATED) {
viewModel.state.collect() { state ->
println(state)
when (state) {
MyViewModel.State.First -> launch(Dispatchers.Main) {
supportFragmentManager.commit {
replace(R.id.nav_host_fragment_content_main, FirstFragment())
}
}
MyViewModel.State.Second -> launch(Dispatchers.Main) {
supportFragmentManager.commit {
replace(R.id.nav_host_fragment_content_main, SecondFragment())
}
}
MyViewModel.State.Init -> {}
}
}
}
}
}
and in the viewmodel I have a stateflow
class MyViewModel : ViewModel() {
enum class State {
First,
Second,
Init; }
val state = MutableStateFlow(State.Init)
fun goToFirst() {
viewModelScope.launch {
println("go to first")
state.emit(State.First)
}
}
fun goToSecond() {
viewModelScope.launch {
println("go to second")
state.emit(State.Second)
}
}
}
the app displays the list fragment and I can add and remove users its great... until the list is empty. The activity stops collecting from the stateflow and never switches to the empty. Its gets weird though. In the viewModel, as an experiment, I added
init {
while (isActive) {
delay(1000)
state.emit(State.First)
delay(1000)
state.emit(State.Second)
}
}
and it switches back forth between fragments. It just doesn't switch to the empty state fragment when I use the buttons on the screen to clear the list. I've tried using SharedFlow and I've tried using a stateflow of an enum class that had two vales list and empty. Samething. The collector in the activity doesn't fire everytime. I know about conflation. the values are different. I've tried almost every combination of Dispatchers and add in a coroutine exception handler to every launch that never catches anything. I've also tried using globalscope. Why doesn't the collector in the activity fire everytime a different value is emitted?
I was using a viewModel factory in the activity and in the fragments. So the fragment would call a function it's viewmodel to change the state, but the viewmodel the activity was listening to never sent any state change.
I'm using the PagedList architectural components using a custom Paged datasource and I am having trouble returning results to the observe method.
below doesn't work:
fragment onviewcreated method
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel?.searchDetails?.observe(viewLifecycleOwner, {
populateSearchResults(it)
})
}
viewmodel code where I update livedata from LivePagedListBuilder upon a user click event(not in viewmodel's init(){} method)
fun onProcessSearch(vararg searchValue: Array<String?>?) {
val pagedListConfig = PagedList.Config.Builder()
.setEnablePlaceholders(true)
.setPageSize(PageSize).build()
searchDetails = LivePagedListBuilder(PageKeyedSearchDataFactory(viewModelScope, searchValue[0]), pagedListConfig).build()
}
below works,
just moving the observe part after calling the above view model function works,
Fragment code:
override fun onClick(view: View) {
viewModel?.onProcessSearch(searchValue)
if (view.id == R.id.match_patient_search_button) {
patientDetailsViewModel?.patientSearchAddedDetails?.observe(viewLifecycleOwner, {
populateSearchResults(it)
GlobalHelper.hideKeyBoard(view)
})
}
}
Could anyone help why the above works, because as per doc we should observer livedata on onViewCreated method.
I am working with the MVVM architecture.
The code
When I click a button, the method orderAction is triggered. It just posts an enum (further logic will be added).
ViewModel
class DashboardUserViewModel(application: Application) : SessionViewModel(application) {
enum class Action {
QRCODE,
ORDER,
TOILETTE
}
val action: LiveData<Action>
get() = mutableAction
private val mutableAction = MutableLiveData<Action>()
init {
}
fun orderAction() {
viewModelScope.launch(Dispatchers.IO) {
// Some queries before the postValue
mutableAction.postValue(Action.QRCODE)
}
}
}
The fragment observes the LiveData obj and calls a method that opens a new fragment. I'm using the navigator here, but I don't think that the details about it are useful in this context. Notice that I'm using viewLifecycleOwner
Fragment
class DashboardFragment : Fragment() {
lateinit var binding: FragmentDashboardBinding
private val viewModel: DashboardUserViewModel by lazy {
ViewModelProvider(this).get(DashboardUserViewModel::class.java)
}
private val observer = Observer<DashboardUserViewModel.Action> {
// Tried but I would like to have a more elegant solution
//if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED)
it?.let {
when (it) {
DashboardUserViewModel.Action.QRCODE -> navigateToQRScanner()
DashboardUserViewModel.Action.ORDER -> TODO()
DashboardUserViewModel.Action.TOILETTE -> TODO()
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentDashboardBinding.inflate(inflater, container, false)
binding.viewModel = viewModel
binding.lifecycleOwner = this
viewModel.action.observe(viewLifecycleOwner, observer)
// Tried but still having the issue
//viewModel.action.reObserve(viewLifecycleOwner, observer)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
// Tried but still having the issue
//viewModel.action.removeObserver(observer)
}
private fun navigateToQRScanner() {
log("START QR SCANNER")
findNavController().navigate(LoginFragmentDirections.actionLoginToPrivacy())
}
}
The problem
When I close the opened fragment (using findNavController().navigateUp()), the Observe.onChanged of DashboardFragment is immediately called and the fragment is opened again.
I have already checked this question and tried all the proposed solutions in the mentioned link (as you can see in the commented code). Only this solution worked, but it's not very elegant and forces me to do that check every time.
I would like to try a more solid and optimal solution.
Keep in mind that in that thread there was no Lifecycle implementation.
The issue happens because LiveData always post the available data to the observer if any data is readily available. Afterwords it will post the updates. I think it is the expected working since this behaviour has not been fixed even-though bug raised in issue tracker.
However there are many solutions suggested by developers in SO, i found this one easy to adapt and actually working just fine.
Solution
viewModel.messagesLiveData.observe(viewLifecycleOwner, {
if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
//Do your stuff
}
})
That's how LiveData works, it's a value holder, it holds the last value.
If you need to have your objects consumed, so that the action only triggers once, consider wrapping your object in a Consumable, like this
class ConsumableValue<T>(private val data: T) {
private val consumed = AtomicBoolean(false)
fun consume(block: ConsumableValue<T>.(T) -> Unit) {
if (!consumed.getAndSet(true)) {
block(data)
}
}
}
then you define you LiveData as
val action: LiveData<ConsumableValue<Action>>
get() = mutableAction
private val mutableAction = MutableLiveData<ConsumableValue<Action>>()
then in your observer, you'd do
private val observer = Observer<ConsumableValue<DashboardUserViewModel.Action>> {
it?.consume { action ->
when (action) {
DashboardUserViewModel.Action.QRCODE -> navigateToQRScanner()
DashboardUserViewModel.Action.ORDER -> TODO()
DashboardUserViewModel.Action.TOILETTE -> TODO()
}
}
}
UPDATE
Found a different and still useful implementation of what Frances answered here. Take a look
In short: when Observe is active it works correctly when I do notify, but when I go back to the previous fragment (I use the navigation component) and again navigate to the current fragment, there is a creation of the fragment, and for some reason the Observe is called.
Why is the Observe not deleted when going back? It should behave according to the fragment's lifecycle.
I tried removing on onStop and still the observe called.
More detail:
Each of my project fragments is divided into 3 parts: model, viewModel, view
In the view section, I first set the viewModel.
class EmergencyFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
emergencyFragmentViewModel = ViewModelProviders.of(this).get(EmergencyFragmentViewModel::class.java)
}
And in onViewCreated I set the Observer object so that any changes made in LiveData I get a change notification here:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
emergencyFragmentViewModel.isEmergencyEventMediaLDSuccess.observe(viewLifecycleOwner, Observer {
Log.d("sendEmergencyEvent", "isEmergencyEventMediaLDSuccess observer called")
}
})
}
In the ViewModel class, I set the LiveData parameter as follows:
EmergencyFragmentViewModel: ViewModel() {
var isEmergencyEventMediaLDSuccess: LiveData<Boolean> = MutableLiveData()
private set
private val observerEventMedia = Observer<Boolean> { (isEmergencyEventMediaLDSuccess as MutableLiveData).value = it}
And in the init I set an observer:
init {
EmergencyFragmentModel.isEmergencyEventMediaLDSuccessModel.observeForever(observerEventMedia)
}
And of course removes when needed
override fun onCleared() {
super.onCleared()
EmergencyFragmentModel.isEmergencyEventMediaLDSuccessModel.removeObserver(observerEventMedia)
}
The part of the model is defined as follows:
class EmergencyFragmentModel {
companion object{
val isEmergencyEventMediaLDSuccessModel: LiveData<Boolean> = MutableLiveData()
And I do request network and when a reply comes back I perform a notify
override fun onResponse(call: Call<Int>, response: Response<Int>) {
if(response.isSuccessful) {
(isEmergencyEventLDModelSuccess as MutableLiveData).postValue(true)
Log.d("succeed", "sendEmergencyEvent success: ${response.body().toString()}")
}
Can anyone say what I'm missing? Why when there is an active Observe and I go back to the previous fragment (I use the navigation component) and navigate to the current fragment again, the Observe is called? I can understand that when a ViewModel instance is created and it executes setValue for the LiveData parameter, then it is notified. But Why is the observe not removed when I go back? I tried removing the Observe on the onStop and it keeps happening.
override fun onStop() {
super.onStop()
emergencyFragmentViewModel.isEmergencyEventMediaLDSuccess.removeObservers(viewLifecycleOwner)
emergencyFragmentViewModel.isEmergencyEventMediaLDSuccess.removeObserver(observeEmergencyEventLDSuccess)
}
#Pawel is right. LiveData stores the value and everytime you observe it (in your onViewCreated, in this case), it'll emit the last value stored.
Maybe you want something like SingleLiveEvent, which clean its value after someone reads it.
So when you go back and forth, it won't emit that last value (once it was cleaned).
As I understand your question, you only want to run the observer, when the new value differs from the old one. That can be done by retaining the value in another variable in the viewModel.
if (newValue == viewModel.retainedValue) return#observe
viewModel.retainedValue = newValue
I fixed this by creating an extension in kotlin by checkin the lifecycle state.
fun <T> LiveData<T>.observeOnResumedState(viewLifecycleOwner: LifecycleOwner, observer: Observer<T>) {
this.observe(viewLifecycleOwner) {
if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
observer.onChanged(it)
}
}
}
And here is how i observe
viewModel.result.observeOnResumedState(viewLifecycleOwner) {
// TODO
}
I am just not able to figure out what is wrong in this code and why the observer is not called when the value is updated. I am using Fragement with livedata and here is the complete code. When app starts fragment gets it value from default data which in this case is 100. But after the value is updated using queueChannelId(channelId) method the observer is not called. I put a print statement and I can see method is executed in main thread. Please help
Fragment:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel =
ViewModelProviders.of(this).get(SomeViewModel::class.java)
viewModel.getChannelId().observe(this, Observer {
// Only called with default value of mutablelivedata
})
}
I can assure that onDestroyView and onDestroy have not been called anytime.
ViewModel:
fun getChannelId() : MutableLiveData<Int> {
return repository.getChannelId()
}
Repository:
var channelIdObservable = MutableLiveData(100)
fun queueChannelId(channelId: Int) {
channelIdObservable.value = channelId
}
fun getChannelId() : MutableLiveData<Int> = channelIdObservable
if you are calling queueChannelId from some other Thread try
channelIdObservable.postValue (channelId)
P.S: I cant see any other issue here.Share your code of how are u calling queueChannelId.