Keerthi Prasad B V
Keerthi Prasad B V

Reputation: 108

Android Fragment and ViewModel issue

I have created FragmentA and initialized the ViewModel inside onViewCreated(). I have attached the observer in the same method.

In FragmentA, I am making an API call and on the success of API call, I am replacing the FragmentA with FragmentB with addToBackStack.

Now the real problem starts when I press the back button in FragmentB, FragmentA in the back stack is called but immediately replaced by FragmentB again.

class FragmentA : Fragment(){
private var viewModel: TrackingViewModel?=null

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    initViewModel()
    attachObservers()
}

private fun initViewModel(){
    viewModel = ViewModelProvider(requireActivity()).get(TrackingViewModel::class.java)
}

private fun attachObservers() {
    viewModel?.mResult?.observe(viewLifecycleOwner, {
        it?.let { resource ->
            parseResource(resource)
        }
    })
}

//Called this method on Button CLick in UI
private fun validate(data:String){
    viewModel?.coroutineSearch(data)
}

private fun parseResource(resource: Resource<GetsApiResponse>) {
    when (resource.status) {
        Status.SUCCESS -> {
            showLoading(false)
            //replaceFragmentWithBackStack is an Extension function
            replaceFragmentWithBackStack(FragmentB(), R.id.container)
        }
        Status.ERROR -> {
            showLoading(false)
            infoError(resource.responseCode)
        }
        Status.EXCEPTION -> {
            showLoading(false)
            infoException()
        }
        Status.LOADING -> {
            showLoading(true)
        }
    }
}

}

Upvotes: 0

Views: 373

Answers (1)

Jenea Vranceanu
Jenea Vranceanu

Reputation: 4694

It is a common problem at some point for everyone who works with LiveData. The problem here is caused by intentional LiveData behaviour: it returns you the value it stores (if any) as soon as you start observing it. LiveData was designed primarily to be used with view/data-binding solutions. As soon as a UI component is observing LiveData it should receive the value and display it appropriately. So the behaviour you get is intentional.

Many other developers, including me, had encountered this exact problem. I was able to solve it by using event wrapper recommended as a solution to this problem that I found in this post. It is simple and easy to understand and works as described in the post.

Using this event wrapper your observer code will update to:

private fun attachObservers() {
    viewModel?.mResult?.observe(viewLifecycleOwner, {
        it?.getContentIfNotHandled()?.let { resource ->
            // Only proceed if the event has never been handled
            parseResource(resource)
        }
    })
}

If you are wondering why in the first place you immediately receive a result from this LiveData - it is because your view model was cached. When you are using activity as the view model store or store owner (ViewModelProvider(requireActivity())) your view model will live on until the activity you used is not destroyed. It means that even if you leave FragmentA by pressing back button to its previous fragment and then return back to FragmentA by creating a new instance you will get the same view model.

Event wrapper source code

/**
 * Used as a wrapper for data that is exposed via a LiveData that represents an event.
 */
open class Event<out T>(private val content: T) {

    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}

Upvotes: 1

Related Questions