Read this issue and Android Jetpack Navigation Fragment Lost State After Navigation to understand the problem of Jetback Navigation 2.2.0
.
The following solution is derrived from https://github.com/android/architecture-components-samples/tree/master/NavigationAdvancedSample with my observations and notes while implementing the solution.
Copy NavigationExtensions.kt
Copy NavigationExtensions.kt into your project.
Edit layout file
Remove NavHostFragment
NOTE: Assuming you already Setup BottomNavigationView With Jetpack Navigation UI
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/mobile_navigation"
/>
Replace it with FragmentContainerView
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
Navigation Files
Assuming you have 3 tabs in BottomNavigationView
, you need to create 3 navigation files.
BottomNavigationView menu file
The id of each menu item of BottomNavigationView
must match the id of navigation files, else BottomNavigationView
(controlled by NavigationExtensions.kt
) tab switching won't work.
Setup MainActivity
class MainActivity : AppCompatActivity(), NavController.OnDestinationChangedListener { private val viewModel by viewModels<MainViewModel>() private var currentNavController: LiveData<NavController>? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main) setSupportActionBar(toolbar) /* appBarConfiguration = AppBarConfiguration( // navController.graph, setOf( R.id.navigate_home, R.id.navigate_collection, R.id.navigate_profile ), drawerLayout ) setupActionBarWithNavController(navController, appBarConfiguration) navView.setupWithNavController(navController) bottomNavView.setupWithNavController(navController) // make sure appbar/toolbar is not hidden upon fragment switch navController.addOnDestinationChangedListener { controller, destination, arguments -> if (destination.id in bottomNavDestinationIds) { appBarLayout.setExpanded(true, true) } } */ val navGraphIds = listOf(R.navigation.home, R.navigation.albumlist, R.navigation.test) val controller = bottomNavView.setupWithNavController( navGraphIds = navGraphIds, fragmentManager = supportFragmentManager, containerId = R.id.nav_host_container, intent = intent ) // Whenever the selected controller changes, setup the action bar. controller.observe(this, Observer { navController -> setupActionBarWithNavController(navController) // optional NavigationView for Drawer implementation // navView.setupWithNavController(navController) addOnDestinationChangedListener(navController) }) currentNavController = controller } private fun addOnDestinationChangedListener(navController: NavController) { // ensure only one listener is active navController.removeOnDestinationChangedListener(this) navController.addOnDestinationChangedListener(this) } override fun onDestinationChanged( controller: NavController, destination: NavDestination, arguments: Bundle? ) { if (destination.id in bottomNavDestinationIds) { appBarLayout.setExpanded(true, true) } }}
Observations and Notes
- Fragments are destroyed and created at each navigation (navigating from one fragment to another), so be aware of this to design your initialization logic properly.
- Assuming you load a item fragment based on item id (you inspect the navArgs, query item from database and load value into EditText). You decide to change the EditText, and while editing you launch another fragment to pick/view something, and back to the current fragment. You realize the EditText is back to the original value and your edit is lost. As mentioned earlier, fragment is destoryed and created at each navigation, so the initialization code run again to overwrite EditText with value from database. To avoid this from happening, you need to store some state in ViewModel to indicate the fragment is loaded, and avoid re-initializing the UI. This require quite some testing to get it right.
- After implementating the solution is this article, RecyclerView and EditText states are actually preserved as you switch tab using BottomNavigationView or navigating to another fragment. If that doesn't happened, check 1) The view in layout file actually has an id 2) Check if your view initialization code overwrite the state value (did you load an empty adapter to RecylerView before loading the actual items, thus causing previous state lost, or overwriting EditText with database value again).
- If you load EditText in RecyclerView, the state of EditText is not automatically saved. You need to utilize ViewModel to save and restore the last value.
- Since you are using multiple navigation files, and there are probably duplicates declarations. You might want to explore nested navigation graphs or use
<include>
. Becareful not not bump intoStackOverflowError
when you use<include>
in a cyclic manner (A include B and B include A). At such scenario, you probably want to useNested Navigation Graph
instead of<include>
.