Multiple LiveData Observers After Popping Fragment - android

Issue
Summary: Multiple LiveData Observers are being triggered in a Fragment after navigating to a new Fragment, popping the new Fragment, and returning to the original Fragment.
Details: The architecture consists of MainActivity that hosts a HomeFragment as the start destination in the MainActivity's navigation graph. Within HomeFragment is a programmatically inflated PriceGraphFragment. The HomeFragment is using the navigation component to launch a new child Fragment ProfileFragment. On back press the ProfileFragment is popped and the app returns to the HomeFragment hosting the PriceGraphFragment. The PriceGraphFragment is where the Observer is being called multiple times.
I'm logging the hashcode of the HashMap being emitted by the Observer and it is showing 2 unique hashcodes when I go to the profile Fragment, pop the profile Fragment, and return to the price Fragment. This is opposed to the one hashcode seen from the HashMap when I rotate the screen without launching the profile Fragment.
Implementation
Navigation component to launch new ProfileFragment within HomeFragment.
view.setOnClickListener(Navigation.createNavigateOnClickListener(
R.id.action_homeFragment_to_profileFragment, null))
ViewModel creation in Fragment (PriceGraphFragment). The ViewModel has been logged and the data that has multiple Observers only has data initialized in the ViewModel once.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
priceViewModel = ViewModelProviders.of(this).get(PriceDataViewModel::class.java)
}
Listen for data from ViewModel in original Fragment (PriceGraphFragment). This is being called multiple times, however it is only expected to have one Observer when the Fragment is loaded.
priceViewModel.graphLiveData.observe(
this, Observer { priceGraphDataMap: HashMap<Exchange, PriceGraphLiveData>? ->
// This is being called multiple times.
})
Attempted Solutions
Creating the Fragment's ViewModel in the onCreate() method.
priceViewModel = ViewModelProviders.of(this).get(PriceDataViewModel::class.java)
Creating the ViewModel using the Fragment's activity and the child Fragment's parent Fragment.
priceViewModel = ViewModelProviders.of(activity!!).get(PriceDataViewModel::class.java)
priceViewModel = ViewModelProviders.of(parentFragment!!).get(PriceDataViewModel::class.java)
Moving methods that create the Observers to the Fragment's onCreate() and onActivityCreated() methods.
Using viewLifecycleOwner instead of this for the LifecycleOwner in the method observe(#NonNull LifecycleOwner owner, #NonNull Observer<? super T> observer).
Storing the HashMap data that is showing as duplicates in the ViewModel opposed to the Fragment.
Launching the child Fragment using the ChildFragmentManager and the SupportFragmentManager (on the Activity level).
Similar Issues and Proposed Solutions
https://github.com/googlesamples/android-architecture-components/issues/47
https://medium.com/#BladeCoder/architecture-components-pitfalls-part-1-9300dd969808
https://plus.google.com/109072532559844610756/posts/Mn9SpcA5cHz
Next Steps
Perhaps the issue is related to creating the nested ChildFragment (PriceGraphFragment) in the ParentFragment's (HomeFragment) onViewCreated()?
ParentFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
user = viewModel.getCurrentUser()
if (savedInstanceState == null) {
fragmentManager
?.beginTransaction()
?.replace(binding.priceDataContainer.id,
PriceGraphFragment.newInstance())
?.commit()
}
Test replacing the LiveData objects with RxJava observables.

This is basically a bug in the architecture. You can read more about it here. You can solve it by using getViewLifecycleOwner instead of this in the observer.
Like this:
mViewModel.methodToObserve().observe(getViewLifecycleOwner(), new Observer<Type>() {
#Override
public void onChanged(#Nullable Type variable) {
And put this code in onActivityCreated() as the use of getViewLifecycleOwner requires a view.

First off, thank you to everyone who posted here. It was a combination of your advice and pointers that helped me solve this bug over the past 5 days as there were multiple issues involved.
Issues Resolved
Creating nested Fragments properly in parent Fragment (HomeFragment).
Before:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
if (savedInstanceState == null) {
fragmentManager
?.beginTransaction()
?.add(binding.priceDataContainer.id, PriceGraphFragment.newInstance())
?.commit()
fragmentManager
?.beginTransaction()
?.add(binding.contentFeedContainer.id, ContentFeedFragment.newInstance())
?.commit()
}
...
}
After:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (savedInstanceState == null
&& childFragmentManager.findFragmentByTag(PRICEGRAPH_FRAGMENT_TAG) == null
&& childFragmentManager.findFragmentByTag(CONTENTFEED_FRAGMENT_TAG) == null) {
childFragmentManager.beginTransaction()
.replace(priceDataContainer.id, PriceGraphFragment.newInstance(),
PRICEGRAPH_FRAGMENT_TAG)
.commit()
childFragmentManager.beginTransaction()
.replace(contentFeedContainer.id, ContentFeedFragment.newInstance(),
CONTENTFEED_FRAGMENT_TAG)
.commit()
}
...
}
Creating ViewModels in onCreate() as opposed to onCreateView() for both the parent and child Fragments.
Initializing request for data (Firebase Firestore query) data of child Fragment (PriceFragment) in onCreate() rather than onViewCreated() but still doing so only when saveInstanceState is null.
Non Factors
A couple items were suggested but turned out to not have an impact in solving this bug.
Creating Observers in onActivityCreated(). I'm keeping mine in onViewCreated() of the child Fragment (PriceFragment).
Using viewLifecycleOwner in the Observer creation. I was using the child Fragment (PriceFragment)'s this before. Even though viewLifecycleOwner does not impact this bug it seems to be best practice overall so I'm keeping this new implementation.

It's better to initialize the view model and observe live data objects in onCreate.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(MyFragmentViewModel::class.java)
// 'viewLifecycleOwner' is not available here, so use 'this'
viewModel.myLiveData.observe(this) {
// Do something
}
}
However, no matter where you initialize the view model, whether in onCreate or onViewCreated, it will still give you the same view model object as it's created only once for the lifecycle of the Fragment.
The important part is observing the live data in onCreate. Because onCreate is called only on fragment creation, you're calling observe only once.
onViewCreated is called both when the fragment is created and when it is brought back from the back stack (after popping the fragment on top of it). If you observe live data in onViewCreated it will get the existing data that your live data is holding from the previous call immediately on returning from the back stack.
Instead, use onViewCreated only to fetch data from the view model. So whenever the fragment appears, either on the creation or returning from the back stack, it will always fetch the latest data.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.fetchData()
...
}

Related

.commitNow() vs .commit(), which is more adequate?

I´m trying to clean up my code and I have a more basic question.
In my Activity, I'm just adding a Fragment ->
class OrderActivity : AppCompatActivity(), HasAndroidInjector {
private lateinit var binding: ActivityOrderBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_order)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction().add(R.id.container, OrderFragment())
.commitNow()
}
}
}
I´m using commitNow() only if the Activity is "new" ( asking for the bundle state ), as I read in Android´s Documentation, commitNow make the commit syncronously -- >
Commits this transaction synchronously. Any added fragments will be initialized and brought completely to the lifecycle state of their host and any removed fragments will be torn down accordingly before this call returns. Committing a transaction in this way allows fragments to be added as dedicated, encapsulated components that monitor the lifecycle state of their host while providing firmer ordering guarantees around when those fragments are fully initialized and ready. Fragments that manage views will have those views created and attached.
Now, I just want to know if it is safe to replace commitNow for ".commit()".
I understand that commit can throw an exception if the Activity State was already saved, but, if I wrote it like this .. >
class OrderActivity : AppCompatActivity(), HasAndroidInjector {
private lateinit var binding: ActivityOrderBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_order)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction().add(R.id.container, OrderFragment())
.commit()
}
}
}
Is it safe?
This may not cause an error, but you are relying on an unrelated happen stance to manage your Fragment. When you could check if it already exists if not add it.
How to check if the fragment exists
And whether commit is safe or not is up to how you are using the Fragment.
Is the Fragment strongly coupled with the containing Activity i.e. does OrderActivity talk directly to the OrderFragment?
If so then you could talk to the Fragment before it has been added which is generally a no-no if it has anything to do with a view.
If the Fragment is independent and manages itself then you should be fine.
They can still be used together but it needs to be 1 way Fragment -> Activity and NOT Activity -> Fragment as again commit() is async and the containing Activity could be talking to something that isn't "there".

Fragment Get Destroyed And Created Again Using Android Jetpack Navigation Controller

I have an activity with three fragments. It will navigate between fragments using navigation controller. But everytime i move to other fragment, the previous fragment destroyed.
When I back (using back key or app bar back button), it will called onCreateView again.
The problem is, I have a method called fetchProducts() that should run once when view created on fragment. Because the fragment alwasy get destroyed, so my fetchProducts always get called again and I dont wanna do that.
Im using viewBinding btw.
Here some of my code:
#AndroidEntryPoint
class HomeMainFragment : Fragment(R.layout.fragment_main_home) {
private var _binding: FragmentMainHomeBinding? = null
private val binding get() = _binding!!
private val viewModel: HomeMainViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentMainHomeBinding.bind(view)
setupRecyclerView()
observe()
goToCreateProductPage()
fetchProducts()
}
//...
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
How to keep the fragment so it will not destroyed? Especially using viewBinding
As this issues:
Support multiple back stacks for Bottom tab navigation
You can use navigation library version 2.4.0-alpha04 and fragment version 1.4.0-alpha04 for back stacks support.
And yes you should consider using ViewModel to get data that you fetch.

Does an instance of a SharedViewmodel never dies?

I have an app that has a main activity and fragments depend on it, so this is normal.
Now, two of my 10 fragments need to communicate, which I use the example given here
https://developer.android.com/topic/libraries/architecture/viewmodel.html#sharing
class SharedViewModel : ViewModel() {
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
}
class MasterFragment : Fragment() {
private lateinit var itemSelector: Selector
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
itemSelector.setOnClickListener { item ->
// Update the UI
}
}
}
class DetailFragment : Fragment() {
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
// Update the UI
})
}
}
Now, if MasterFragment and DetailFragment dies (both does a popBackStack()) does that instance of the viewmodel keep active untill I finish the MainActivity containing this Fragments ? Because now I dont need anymore that viewmodel instance, but as per documentation says, this instance will be retained from the Activity that contains these fragments
This is not what I'm looking for to communicate between fragments since now a new instance of that viewmodel will be the same as the past one I have created, I mean, it will reuse the instance that I used with the already poped fragments, in which I will need to extra handling a deletion or reset of all the data inside this viewmodel instead of getting a new fresh viewmodel.
Does it works this way or that instance automatically dies when no fragments depending on it are in the stack anymore ?
Now, if MasterFragment and DetailFragment dies (both does a popBackStack()) does that instance of the viewmodel keep active untill I finish the MainActivity containing this Fragments ?
Correct. While it so happens that only two of your fragments use it, that ViewModel is scoped to the activity.
I mean, it will reuse the instance that I used with the already poped fragments, in which I will need to extra handling a deletion or reset of all the data inside this viewmodel instead of getting a new fresh viewmodel.
Then perhaps you should not be using activityViewModels(). For example, you could isolate these two fragments into a nested navigation graph and set up a viewmodel scoped to that graph.
Does it works this way or that instance automatically dies when no fragments depending on it are in the stack anymore ?
The ViewModel system does not know about what is or is not "depending on it". It is all based on the ViewModelStore and the ViewModelStoreOwner that supplies it. activityViewModels() uses the activity as the ViewModelStoreOwner, so viewmodels in that ViewModelStore are tied to the activity.

SharedViewModel not clearing when poping fragment

I'm using a shared view model like here
But the problem is that when I clear my last fragment, I want to clear the viewmodel, or kill its instance, but somehow it survives when I leave the last fragment that uses it
How can I programatically clear this viewmodel ?
I use it like this
Fragment A
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated() {
model.getTotal().observe(viewLifecycleOwner, Observer { cartTotal ->
total = cartTotal
})
}
From fragment B I sent the total
Fragment B
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated() {
model.setTotal = 10
}
But when leaving Fragment A with that data (doing popBackStack since I'm using navigation components) it does not clear the viewmodel, instead when I open again my fragment , the data stills there
I suspect that the viewmodel is tied with my Container Activity and not the lifecycle of the fragments itself, so
How can I remove the instance or clear my viewmdel when I hit my last fragment ?
Thanks
If you want to get a ViewModel associated with a parent fragment, your inner fragment should follow the by viewModels JavaDoc and use:
val viewmodel: MYViewModel by viewmodels ({requireParentFragment()})
This says to use the parent Fragment as the owner of your ViewModel.
(The parent fragment would use by viewModels() as it is accessing its own ViewModels)
you can also clear viewModelStore manually after Fragment A destroyed.
something like this :
override fun onDetach() {
super.onDetach()
requireActivity().viewModelStore.clear()
}
then your viewModel instance will be cleared. for checking this work you can debug onCleared method of your viewModel.

Use viewLifecycleOwner as the LifecycleOwner

I have a fragment:
class MyFragment : BaseFragment() {
// my StudentsViewModel instance
lateinit var viewModel: StudentsViewModel
override fun onCreateView(...){
...
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this).get(StudentsViewModel::class.java)
updateStudentList()
}
fun updateStudentList() {
// Compiler error on 'this': Use viewLifecycleOwner as the LifecycleOwner
viewModel.students.observe(this, Observer {
//TODO: populate recycler view
})
}
}
In my fragment, I have a instance of StudentsViewModel which is initiated in onViewCreated(...).
In, StudentsViewModel, students is a LiveData:
class StudentsViewModel : ViewModel() {
val students = liveData(Dispatchers.IO) {
...
}
}
Back to MyFragment, in function updateStudentList() I get compiler error complaining the this parameter I passed in to .observe(this, Observer{...}) that Use viewLifecycleOwner as the LifecycleOwner
Why I get this error? How to get rid of it?
Why I get this error?
Lint is recommending that you use the lifecycle of the fragment's views (viewLifecycleOwner) rather than the lifecycle of the fragment itself (this). Ian Lake and Jeremy Woods of Google go over the difference as part of this Android Developer Summit presentation, and Ibrahim Yilmaz covers the differences in this Medium post In a nutshell:
viewLifecycleOwner is tied to when the fragment has (and loses) its UI (onCreateView(), onDestroyView())
this is tied to the fragment's overall lifecycle (onCreate(), onDestroy()), which may be substantially longer
How to get rid of it?
Replace:
viewModel.students.observe(this, Observer {
//TODO: populate recycler view
})
with:
viewModel.students.observe(viewLifecycleOwner, Observer {
//TODO: populate recycler view
})
In your current code, if onDestroyView() is called, but onDestroy() is not, you will continue observing the LiveData, perhaps crashing when you try populating a non-existent RecyclerView. By using viewLifecycleOwner, you avoid that risk.
viewLifeCycleOwner is LifecycleOwner that represents the Fragment's View lifecycle. In most cases, this mirrors the lifecycle of the Fragment itself, but in cases of detached Fragments, the lifecycle of the Fragment can be considerably longer than the lifecycle of the View itself.
Fragment views get destroyed when a user navigates away from a fragment, even though the fragment itself is not destroyed. This essentially creates two lifecycles, the lifecycle of the fragment, and the lifecycle of the fragment's view. Referring to the fragment's lifecycle instead of the fragment view's lifecycle can cause subtle bugs when updating the fragment's view.
Instead of this use viewLifecycleOwner to observe LiveData
viewModel.students.observe(viewLifecycleOwner, Observer {
//TODO: populate recycler view
})
Captain obvious here, also useful could be this:
viewModel.searchConfiguration.observe(requireParentFragment().viewLifecycleOwner, Observer {}

Categories

Resources