Fragment Recreation causes Observer to trigger onChanged() with Androidx Navigation library - android

Issue:
While working with Navigation Library, I observed when I navigate back to the previous fragment, it recreates the fragment and thus re-registering all my Observers which triggers OnChanged() again
I have a Snackbar which shows some error messages example if I am looking for no more data present or no Internet connection to the server:
deliveriesListViewModel.isMoreDataPresent.observe(this, Observer {
if (!it) showSnackBar(getString(R.string.no_more_data))
})
Source of above code here
And on navigating back and forth, the SnackBar pops up every time, and also every time I change the orientation or rotate my device.
My architecture has a single Activity with startDestination as my ListFragment in the navigation graph and a DetailFragment as destination. SupportNavigationUp or a simple OnBackPressed on DetailFragment returns me to my ListFragment and then recreates the fragment and thus re-registering all my Observers which triggers OnChanged() again and the SnackBar pops up when noMoreDataPresent LiveData is false
Now I tried the solution from here but unfortunately, it doesn't work
I have also tried to switch my LifecycleOwner to my activity by that also doesn't work.
Tried moving the ViewModelProviders.of to OnCreate and onActivityCreated - doesn't work
Please suggest corrections or any ideas what can be done to prevent SnackBar popping up after navigation and orientation change.
Footnotes
I have gone through these issues:
Multiple LiveData Observers After Popping Fragment
How to avoid fragment recreation when tap back button using navigation architecture actions?
Is there a way to keep fragment alive when using BottomNavigationView with new NavController?
here is my complete source code

This article, especially item 1, might be relevant to what you're experiencing. Basically, what happens is that you may have multiple spawned Observers every time you navigate back to your fragment, thus executing onChanged multiple times. Using the fragment's view lifecycle as the LifecycleOwner should prevent this from happening, so your code above would look like this:
deliveriesListViewModel.isMoreDataPresent.observe(viewLifecycleOwner, Observer {
if (!it) showSnackBar(getString(R.string.no_more_data))
})

Related

how ViewPager2 handle fragments

In my app I have a ViewPager2, the fragment inside this ViewPager have some code on onViewCreated. I have 2 question:
If I update the dataset and then return to old one the onViewCreated is not called, because of fragment cache, how can I intercept the re-show? I am ok with cache, bui I want restore the initial focus of the fragment when re-showed.
How can I intercept the hiding from the fragment? My fragment show a dialog, then an external event can change the data set, I want in this case dismiss the dialog.
Are 2 days that I am trying to fix those 2 issues.
EDIT: I solved the 2 observing the changing of data set.
Data sharing by ViewModel looks like a good solution for both cases. Fragments will rely on data change in shared ViewModel (depends on the structure of your screen it can be data sharing with host activity or parent fragment).
If it's not what you are looking for, provide more details.

Android - Using Live Data and View model (MVVM) to interact between fragments and activity.. dynamically causes issues

I have been facing issues while using live data and shared view model as a medium to interact between my fragments and activity. Here is the issue..
Activity A (Has a view model X shared across two fragments)
---Displays----> Fragment A on startup (dashboard type of view) --- on select in A (viewmodel updated)--> Live data Triggered
--view model X in the activity observes changes and adds Fragment B dynamically to the back stack-->
Fragment B is active now.
Couple of issues, that I'm facing
I see that, on back press from fragment B, back to Fragment A and vice versa, the previous value of the livedata is observed at the beginning before fetching the latest data.
On rotation/ state change, my activity observes for the fragment changes the second time ( kind of same as above)
Any workaround for this ? or is there anything that i'm missing
Thanks in advance..
as a quick workaround I went with this approach wherein I observe my changes only if my viewLifeCycleOwner is in a resumed state.
getViewLifeCycleOwner().getCurrentState() == Lifecycle.State.RESUMED.
Ideally it should be a single event approach, but yea for a quick fix this should work.

With two fragments on one screen (activity), how can one fragment update the other with EventBus

Ok, so I'm new to android development.
I'm making a recording app. On the same screen as the recording button (to record things) I also have a Fragment to show how many recordings I have. I can tap the recording button to make a recording, but the recording count does not update until the activity's state is refreshed. I want the recording count to update in real time. The fragment is going to be visible in another activity as well, so the logic cannot be in the main recording activity.
I have just integrated EventBus into my project. I have it set up to where the event is a successful recording, and the subscriber is the fragment, so far.
The fragment value correctly gets the event message, but the fragment will not update until it's "refreshed" or rather, goes through its life-cycle of onDestroyView() to onCreateView().
Please help, I want to be able to update the fragment in real time without having to use the built-in life-cycle functions.
THANKS!
EDIT:
I found a solution by just removing the fragment, then re-adding it.
supportFragmentManager
.beginTransaction()
.remove(statsFragmentTwo)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE)
.commit()
supportFragmentManager
.beginTransaction()
.add(R.id.main_stats_container_two, statsFragmentTwo)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.commit()
But still, this doesn't look very efficient! Is there a less costly way?
The fragment value correctly gets the event message, but the fragment
will not update until it's "refreshed" or rather, goes through its
life cycle of onDestroyView() to onCreateView().
I do not understand the idea of the - "Fragment will not update". If you could listen to the event successfully, you can update the UI elements immediately where you catch the event in your fragment. I am just posing a pseudo code.
if(eventListened)
updateUIElementsHere();
You do not have to wait for the lifecycle functions to be called. In your updateUIElementsHere function, you might consider having a similar code that you have in your onCreateView function.
Hope that helps!

Fragment lifecycle overlap on navigate

I have a single Activity application with multiple Fragments that are being switched by using Navigation components. When I switch between two fragments their onCreate() and onDestroy() methods seem to overlap. Thus making it difficult for me to write initialization and clean up code for fragments when they access the same global objects.
Navigating from Framgent_A to Fragment_B has the following order of methods:
Fragment_B.onCreate()
Fragment_A.onDestroy()
In Fragment_A.onDestroy() I reverse the operations I do in Fragment_A.onCreate(). And in Fragment_B I expect things to be in a neutral state when onCreate() is called. However that is not the case since Fragment_A.onDestroy() has not yet been called.
Is the overlap normal on Android or did I configure something wrong in my Navigation components? Is there another way I could achieve what I am trying to do? I know I could couple both Fragments and make it work, but I don't want either Fragment to know about each other. To me it seems weird that Framgnet_A is still alive when Fragment_B is created, when Fragment_B is supposed to replace Fragment_A.
Any help is greatly appreciated!
Edit:
After groing through the source code while debugging I have found out that in FragmentNavigator.navigate() FragmentTransaction.setReorderingAllowed() is called, which allows reordering of operations, even allowing onCreate() of a new fragment to be called before onDestroy() of the previous. The question still remains, how can I solve my problem of correctly cleaning up global state in one Fragment before initializing the same global state in the next Fragment.
The Android Fragment life-cycle is not really an appropriate callback host for your needs. The navigation controller will replace the two fragments with animation, so both are somehow visible the same time and eventually even onPause() of the exiting fragment is called after onResume() of the entering one.
Solution 1: Use OnDestinationChangedListener
The onDestinationChanged() callback is called before any of the life-cycle events. As a very simplified approach (look out for leaks) you could do the following:
findNavController().addOnDestinationChangedListener { _, destination, _ ->
if(shouldCleanupFor(destination)) cleanup()
}
Solution 2: Abstract the global changes away
Instead of having single navigation points change the global state, have a single point of truth for it. This could be another fragment independent of the navigation hierarchy. This then observes the navigation as before:
findNavController(R.id.nav_graph).addOnDestinationChangedListener { _, destination, _ ->
resetAll()
when(distination.id) {
R.id.fragment_a -> prepareForA()
R.id.fragment_b -> prepareForB()
else -> prepareDefault()
}
}
As an additional advantage you could implement the state changes idempotently as well.
Since you have an activity that controls the inflation of your Fragments you can manually control the lifecycles of the fragment that are being inflated. By calling into below methods you can control which fragment is ready to use global data. You will at this point have to, some how pass data back to Mainactivity to establish which fragment is active since your asking about how to inflate 2 fragment simultaneously which will share an object. Better approach would be to have the MainActivity implement FragmentA and FragmentB-detail with specific classes to do Stuff this way you have to treat your app like Tablet and determine 2 pane mode and which point you can use appropriate classes out of those fragments controlled by your Activity. The included link matches what your trying to accomplish
private void addCenterFragments(Fragment fragment) {
try {
removeActiveCenterFragments();
fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add(R.id.content_fragment, fragment);
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
activeCenterFragments.add(fragment);
fragmentTransaction.commit();
}catch (Exception e){
Crashlytics.logException(e);
}
}
private void removeActiveCenterFragments() {
if (activeCenterFragments.size() > 0) {
fragmentTransaction = fragmentManager.beginTransaction();
for (Fragment activeFragment : activeCenterFragments) {
fragmentTransaction.remove(activeFragment);
}
activeCenterFragments.clear();
fragmentTransaction.commit();
}
}
Perhaps you could move some the code related to initialization where you assume a neutral state to that fragments onStart() or onCreateView() method. According to the developer documentation this is where initialization should take place.
Another option available is using an Observer /Observable pattern, where you could notify your Activity once onDestroy() in Fragment A is completed. The Activity would then notify Fragment B that it is safe to assume a cleaned up state and begin initialization.
My case was a little bit different, and I would like to share it in case anyone faced the same issue.
I wanted to do an action in onPause() of the current fragment, but not execute that code when one navigates from a fragment to another. What I had to do was to call isRemoving() method to check if the current fragment is being removed or not. It is set to true when NavController.navigate(...) method is called.
override fun onPause() {
super.onPause()
if (!isRemoving()) {
// Write your code here
}
}
Per Google's Fragment.isRemoving() documentation:
Return true if this fragment is currently being removed from its activity. This is not whether its activity is finishing, but rather whether it is in the process of being removed from its activity.

Fragment does not call lifecycle methods

I've a fragment A. I add() it with tag like this:
fragmentTransaction.addToBackStack(special_tag);
Then I simply add() fragment B on top of fragment A. After that, I decide to remove fragment B and go back to fragment A using:
activity.fragmentManager.popBackStackImmediate(special_tag, 0)
When I reach the fragment A, it seems that fragment doesn't re-run it's lifecycle methods: onAttach(), onResume(), onCreate() ect.
Can someone explain this behavior and maybe suggest an alternative?
(I need to "refresh" the data when I come back to fragment A second time)
What is causing this result?
Is there a clean solution/work-around?
Update
Fragment B is GuidedStepFragment and does not have a .replace() function. I found that it has finishGuidedStepFragments(), but it behaves the same (it does not call fragment life cycle functions)
Situation (again):
Fragment A (Simple fragment) -> .add(Fragment B) (GuidedStepFragment) -> popBackStackImmediate() or finishGuidedStepFragments()
I add Fragment B like this:
GuidedStepFragment.add(activity.fragmentManager, fragmentB.createInstance())
Using fragmentTransaction.add(Fragment) doesn't remove Fragment A. What is actually happening is that Fragment A is still running behind Fragment B. Since Fragment A never stopped running, it's lifecycle has no need to retrigger.
Consider using fragmentTransaction.replace(Fragment) and replace the fragment in the container (fragment A) with fragment B. If you pop that transaction from the back stack, then Fragment A will reattach and follow your expected lifecycle.
Update
Since you seem to be using GuidedStepFragments from the leanback library, this is a little tricky. GuidedStepFragment actually performs replace(...) under the hood, but you're adding fragment B to a different container so the original behavior I mentioned doesn't apply.
I'm not super familiar with leanback (since it's usually only used for android tv), but I do know that you can at least do the following. If you keep track of your backstack size, when all of the GuidedStepFragments have been popped, you will have returned to your original fragment. For example, let's assume your backstack starts at zero:
activity.fragmentManager.addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
#Override
public void onBackStackChanged() {
if (activity.fragmentManager.getBackStackEntryCount() == 0){
// handle your updates
}
}
});
// the next line of code will add an entry to the backstack
GuidedStepFragment.add(activity.fragmentManager, fragmentB.createInstance());
// eventually when back is pressed and the guided fragment is removed, the backstack listener should trigger

Categories

Resources