I started playing around with the Navigation Architecture Component for Android. I work on an app with complicated navigation that relies on the back stack for nested fragments and deep links. Different users can experience different interfaces, reach areas through various paths and jump around from the drawer and bottom nav. There’s a lot of data passed in and out, and transitions can be overridden to keep everything pretty. Needless to say, I’m skeptical of anything flexible enough for all this.
As my first test, I wanted to try to use the Navigation Component for Fragment A going to Fragment B. The latter will be a form that has 4 states of its own—that is, it has text, fields and buttons that show or hide using animateLayoutChanges for effect. An example of this might be a product intro fragment (A), going to a fragment (B) that quadruples as the forms for signup (B1), signin (B2), forgot password (B3) and more-info (B4) screens.
The navigation graph can be represented as A->B1 [->B2->B3, ->B4].
I ended up trying three approaches:
- A single graph, NavHostFragment and custom destinations for B1-B4.
- NavHostFragment with a graph for A->B, then make B implement NavHost with its own graph using custom destinations for B states.
- A graph using NavHostFragment and use child fragment manager with invisible fragments to represent the B states.
Attempt 1
The first problem I ran into with this approach was that NavHostFragment didn’t know about my custom destinations. So I extended it, and, in onCreate, I called NavController.navigatorProvider.addNavigator to add my custom navigator. Another issue I ran into was that I could not set the graph until after adding the navigator. This meant the layout could not include app:navGraph and I instead called setGraph programmatically. Success! Sort of. Everything worked with this approach until I rotated. Because NavHostFragment.oncreate restores the graph from its saved state, I wasn’t able to add my Navigator before it failed. When I found myself thinking about modifying the state before calling the super, I knew this was not going well.
Attempt 2
So if NavHostFragment is locked down, maybe we can use a different host and graph to handle the B fragment states. With a slight modification, I gave B its own NavController, called into setGraph and wired it to save state. The problem now was that the back button did not know to use the new NavController. I solved this by overriding Activity.onBackPressed to look at what was active in the NavHostFragment, see if it had its own NavController to offer and call into popBackStack if it did. This solution appeared to work entirely. But, yuck.
Attempt 3
In tracing through the library code, Stack Overflow and the Issue Tracker, I knew that child fragments got special treatment to allow them to play nice with the back/up buttons (thanks Ian Lake!). This got me thinking… maybe I can add invisible fragments to the child fragment manager of B. Whenever I did this, I could update the state of the view and no one would be the wiser. This turned my problem into a much more common use case. And, what do you know? It worked and required no changes to the Activity, no subclasses, no custom Navigator—just an invisible view group in the B layout, an Enum to represent the states, a child fragment backstack listener to refresh the view when the state changes and two small functions to find and change the state.
1 2 3 4 5 6 |
<androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/faketainer" android:layout_width="1dp" android:layout_height="1dp" android:visibility="invisible"> </androidx.constraintlayout.widget.ConstraintLayout> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
enum class NavState { SIGN_UP, SIGN_IN, FORGOT_PASSWORD, MORE_INFO } fun changeState(navState: NavState) { childFragmentManager.beginTransaction() .replace(R.id.faketainer, Fragment().apply { this.arguments = bundleOf(Pair("state", navState)) }) .addToBackStack(null).commit() } fun findCurrentState() = childFragmentManager.findFragmentById(R.id.faketainer) ?.arguments?.getSerializable("state") as? NavState ?: NavState.SIGN_UP onCreateView { // ... view.btn1.setOnClickListener { changeState(if (findCurrentState() == SIGN_UP) SIGN_IN else FORGOT_PASSWORD) } view.btn2.setOnClickListener { changeState(MORE_INFO) } } onActivityCreated { // ... refreshViewForNavState() childFragmentManager.addOnBackStackChangedListener { refreshViewForNavState() } } fun refreshViewForNavState() { when (findCurrentState()) { NavState.SIGN_UP -> { ... } // setVisibilities and setTexts NavState.SIGN_IN -> { ... } // etc } } |
Pretty simple! All that’s left is the usual Navigation Component stuff: NavHostFragment in the activity layout, the graph xml, a button in the intro fragment layout with a click listener calling to NavController.navigate to launch Fragment B.
If anyone reads this, has questions or finds this helpful, let me know!