Why google-iosched can save fragment state while navigating with navigation component - android

Overall look at the code, I don't understand why it can be done.
https://github.com/google/iosched
from apps/iosched/ui/MainActivity.kt, It initialized NavController and NavHostFragment, but seems that there is no special treatment.
https://github.com/google/iosched/blob/main/mobile/src/main/java/com/google/samples/apps/iosched/ui/MainActivity.kt
val appBarConfiguration = AppBarConfiguration(TOP_LEVEL_DESTINATIONS)
private val TOP_LEVEL_DESTINATIONS = setOf(
R.id.navigation_feed,
R.id.navigation_schedule,
R.id.navigation_map,
R.id.navigation_info,
// R.id.navigation_agenda, comment will not stop saving the statement.
R.id.navigation_codelabs,
R.id.navigation_settings
)
from apps/iosched/ui/AgendaFragment.kt: the most simplest fragment, BindingAdapter method will always init AgendaAdapter(), but It can save the position of RecyclerView after init.
https://github.com/google/iosched/blob/main/mobile/src/main/java/com/google/samples/apps/iosched/ui/agenda/AgendaFragment.kt
Why can it save the State of each fragment?

IOSched depends on Navigation 2.4.1. As per the release notes of Navigation 2.4.0:
The NavController ... has been expanded to support saving and restoring the back stack.
As part of this change, the NavigationUI methods of onNavDestinationSelected(), BottomNavigationView.setupWithNavController() and NavigationView.setupWithNavController() now automatically save and restore the state of popped destinations, enabling support for multiple back stacks without any code changes. When using Navigation with Fragments, this is the recommended way to integrate with multiple back stacks.
And IOSched uses setupWithNavController, which means each tab is automatically going to save and restore its state correctly.
That includes the state of a RecyclerView, which has always supported saving and restoring its position automatically.

Related

NavGraph with dynamic destinations - restore after process kill

This is the setup:
View-based Android UI
Using androidx.navigation library (tested with versions 2.4.1 and 2.5.0-beta01)
Activity, consisting of a bottom bar and a NavHostFragment
What's special is that the navigation structure is defined by the server. The top-level destinations are available during Activity.onCreate(). Other detail screens (=children of the top-level destinations) are only known when performing more server calls while navigating down in the hierarchy.
This is an excerpt of the body of Activity.onCreate():
val navController = findNavController(R.id.navigationHostFragment)
val topLevelDestinations = getTopLevelDestinations()
// some additional destinations are defined statically in navigation.xml (e.g. settings)
val staticNavGraph = navInflater.inflate(R.navigation.navigation)
graph = staticNavGraph.apply {
setStartDestination(topLevelDestinations.first())
addDynamicDestinations(staticNavGraph, topLevelDestinations)
}
This code works initially. The NavGraph contains the top-level destinations.
Some top-level destinations offer navigation to children elements. These destinations are added to the NavGraph in a just-in-time manner, i.e. just before navigate() is called.
When the user navigated to a detail screen, the app process is killed and the app is re-opened, then onCreate() is called again and the app crashes during setGraph()/graph = with the following error:
java.lang.IllegalStateException: Restoring the Navigation back stack failed: destination -1178236840 cannot be found from the current destination Destination(0x1a356ec2) ...
at androidx.navigation.NavController.onGraphCreated(NavController.kt:1128)
at androidx.navigation.NavController.setGraph(NavController.kt:1086)
at androidx.navigation.NavController.setGraph(NavController.kt:100)
To solve this I'd be fine with either of these options:
Find a way, the entire NavGraph is saved persistently and restored
Prevent NavController from trying to recover the last destination, but show the initial start destination again.
Regarding 2. I tried calling navController.popBackStack(startDestination, false) and navController.clearBackStack(startDestination) before setGraph() is called but this doesn't seem to have the desired effect.

How to kill all fragments created by a NavHostFragment?

I have 2 navigation files, and in my Activity, 2 fragments. One of the navigations is always shown inside one of the fragments, but I show the other one only when I need it.
The way they're drawn is the always showing fragment is inside a relativeLayout, and the other fragment is inside the same relativeLayout with it's visibility set as gone. When I need the second navigation, I set the visibility to visible and when I don't need it, I set it to gone again.Visually this works well, but what I want to accomplish is that when I don't want the second navigation, I want to completely kill it and redraw it the next time I need it.
What I've done so far was to get a hold of the NavHostFragment used to start the navigation, and when I dont need it anymore, call popBackStack() on it's navController, but it doesn't work:
val navHost: NavHostFragment? = null
fun createSecondNav() {
navHostLogin = NavHostFragment.create(R.navigation.navigation_second)
theFragment.visibility = View.VISIBLE
supportFragmentManager.beginTransaction()
.replace(R.id.theFragment, navHostLogin!!)
.commit()
}
fun killSecondNav() {
theFragment.visibility = View.GONE
navHostLogin?.navController?.popBackStack() // returns false
navHostLogin = null
}
So how can I completely kill the fragments created by the second navHost?
NavController maintains it's own back-stack, independent form the FragmentManager back-stack.
And popBackStack() without arguments only pops that back-stack once:
Attempts to pop the controller's back stack. Analogous to when the user presses the system Back button when the associated navigation host has focus.
While popBackStack(int destinationId, boolean inclusive) reads:
Attempts to pop the controller's back stack back to a specific destination.
destinationId int: The topmost destination to retain
inclusive boolean: Whether the given destination should also be popped.
So this should be:
navController.popBackStack(R.id.startDestination, true)
I'd wonder why even using two NavController, because one can set the graph at run-time with setGraph(NavGraph graph, Bundle startDestinationArgs):
Sets the navigation graph to the specified graph.
Any current navigation graph data (including back stack) will be replaced.

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

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))
})

Android Navigation Components

I added Android new Navigation Component inside my app and created navGraph and added few fragments as a destinations. I figure out every time i navigates into destinations previous destination (fragment) is begin restarted. And my last state of previous destination (fragment) has lost. Is there any options to retain this?
As i already used retainInstance in fragment.
I have been using the navigation component with fragments in my project and I found that the easiest way to save a fragment state is using the view model component:
https://developer.android.com/topic/libraries/architecture/viewmodel?gclid=EAIaIQobChMI442XtIuR4wIVhuiaCh0uGAFZEAAYASAAEgIRIfD_BwE
I have been using MVP Architecture and in my model I extend the view model.
That way as long as your activity wasn't destroyed the model instance is being saved and you can access it when the fragment reload so all it's parameters are still there so you can reinitialise your fragment with the saved values inside your model.

how not to destroy fragment with androidX

now ,I try androidX's navigation and bottom-navigationbar,when I use it like below
supportFragmentManager = getSupportFragmentManager();
navHostFragment =(NavHostFragment)supportFragmentManager.findFragmentById(R.id.frag_nav);
navController = navHostFragment.getNavController();
NavigationUI.setupWithNavController(navigation, navController);
I found an issue,everytime switch the bottomNavigationBar,the fragment will be recreate,all network task in target fragment will be redo,how to keep fragment's state when it switch in androidX?
I also had the same issue. I'm using BottomNavigationView with navigation architecture components and every time fragment is recreated. This is intended behaviour as you can see in Google issue tracker (https://issuetracker.google.com/issues/109856764).
So the right way to approach this is to save current view state in ViewModel and observe that state in every newly created fragment. You need to do all your network tasks in ViewModel.
Also take care of the way you fetch ViewModel inside fragment. If you do it like this viewModel = ViewModelProviders.of(this, viewModelFactory).get(MyViewModel::class.java), ViewModel will get fragment scope(it will be destroyed when fragment gets destroyed). You have to get ViewModel with activity scope so it can survive switch between fragments. You can do it like this viewModel = ViewModelProviders.of(activity!!, viewModelFactory).get(MyViewModel::class.java)

Categories

Resources