NavController currentDestination is null? - android

I am using NavController to manages app navigation:
findNavController().navigate(action)
I got a few crashes in Crashlytics: I found it is because:
MyFragment {
...
myLiveData.observer(viewLifecycleOwner, Observer) {
findNavController().navigate(myAction) // currentDestination is null ...
})
...
navController.currentDestination? is an optional, When it is null, app crashes with unhandled exception.
Since currentDestination is declared as optional, I guess there must be some legit reason why it could be null, that I don't know. Appreciate in advance for any pointer.

I was experiencing the same issue.
At seemingly random times, the navigate to my destination fragment would crash due to the currentDestination being null.
Similar to the OP, I was triggering the nav through a Flow (not a live data).
Despite collecting the flow with the viewLifecycleOwner, it almost seemed like the fragment wasn't ready to navigate. What I found that fixed the issue was a little surprising. It was how the previous fragment was "popping" itself.
FragA -> FragB
FragB.popBackStack()
FragA -> VERY Quickly re-nav to FragB (null currentDestination == Crash)
However, as a test, I tried using
FragB.popBackStack(fragA.id, false)
And the crashes stopped. The currentDestination was never null again.
This must be a bug in the navComponent library.
My fix was as follows, and is still working (fingers crossed).
Instead of "findNavController.popBackStack()" I use
findNavController().previousBackStackEntry?.let {
findNavController().popBackStack(it.destination.id, false)
} ?: run {
findNavController().popBackStack()
}
Hope that works for someone else also.
edit Left in for posterity.. but.. I was wrong. this didn't fix it afterall. My mistake. Carry on.

Destination represents the node in the NavGraph that's being hosted by the NavHost. NavController just manages the flow. There are few ocasions when NavHost is not showing any destination e.g.:
before you set the NavGraph (because destination represents position in the graph)
when you manually inflate something in the NavHost using transaction (outside of the graph's scope)
If you have multiple graphs in one app (e.g. nested graphs, but can also be independent) you may have one NavController giving main graph destination and a secondary one returning null, etc.

Thanks Stachu, any relationship to fragment viewLifecycle?
In my case, the navigation is triggered from a liveData observer, i.e.,
MyFragment {
...
myLiveData.observer(viewLifecyucleOwner, Observer) {
findNavController().navigate(myAction) // currentDestination is null ...
}
...

Related

Jetpack Compose navigation NavController.popBackStack() not working properly

When working with Compose Navigation and calling NavController.popBackStack() multiple times on the first shown Composable (startDestination) the backnavigation does not work anymore. For example when navigating to another Composable from this point on and then calling popBackStack does not have an effect.
For some Reason the size of the NavController.backQueue is at least 2 even though it's supposed to only show one Composable. If popping the backstack lower than that, the navigation does not seem to work anymore. (I don't know why)
Therefore I wrote the following simple extension function which prevents popping the BackQueue lower than 2:
fun NavController.navigateBack(onIsLastComposable: () -> Unit = {}) {
if (backQueue.size > 2) {
popBackStack()
} else {
onIsLastComposable()
}
}
You can use it like this:
val navController = rememberNavController()
...
navController.navigateBack {
//do smth when Composable was last one on BackStack
}

Navigation Component - Starting destination is incorrect

I started noticing something in my app the other day and its wildly inconsistent. Sometimes it happens and sometimes it doesn't.
I am using the Navigation Component to handle navigation in the app and I started noticing that sometimes, when popping the backstack via the action bar back button or the device back button, it returns to a fragment that is no longer the starting destination (or at least shouldn't be).
In my case the app starts in MainFragment and once authenticated moves to DashboardFragment. This is a pretty common scenario.
Navigation in the app is pretty flat. most of the time its only 1 level deep so nearly all views are accessible from the dashboard.
The app starts at a login view as many do and then to a dashboard where the session will remain as the "start destination". To accomplish this, its done in the nav_graph using popUpTo and popUpToInclusive.
<fragment
android:id="#+id/mainFragment"
android:name="com.example.view.fragments.MainFragment"
android:label="Welcome">
<action
android:id="#+id/action_mainFragment_to_dashboardFragment"
app:destination="#id/dashboardFragment"
app:popUpTo="#id/mainFragment"
app:popUpToInclusive="true"/>
</fragment>
<fragment
android:id="#+id/dashboardFragment"
android:name="com.example.view.fragments.dashboard.DashboardFragment"
android:label="#string/dashboard_header" >
<action
android:id="#+id/action_dashboardFragment_to_notificationsFragment"
app:destination="#id/notificationsFragment" />
</fragment>
When the user successfully authenticates and its time to go to the dashboard, I use NavController.navigate() to send them there.
findNavController().navigate(
MainFragmentDirections.actionMainFragmentToDashboardFragment()
)
// This should have the same result and it does appear to be affected by the same issue
// findNavController().navigate(R.id.action_mainFragment_to_dashboardFragment)
I have an action bar with a back arrow and a navigation drawer. In the main activity I need to define the AppBarConfiguration and override onSupportNavigateUp()
lateinit var appBarConfiguration: AppBarConfiguration
...
override fun onCreate(savedInstanceState: Bundle?) {
Timber.d("onCreate()")
super.onCreate(savedInstanceState)
_binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// There is 2 different drawer menu's respectfully.
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.mainFragment,
R.id.dashboardFragment
), binding.drawerLayout
)
setSupportActionBar(binding.toolbar)
setupActionBarWithNavController(navController, appBarConfiguration)
}
...
override fun onSupportNavigateUp(): Boolean {
Timber.d("-> onSupportNavigateUp()")
val breadcrumb = navController
.backStack
.map { it.destination }
.filterNot { it is NavGraph }
.joinToString(" > ") { it.displayName.split('/')[1] }
Timber.d("Backstack: $breadcrumb")
Timber.d("Previous backstack entry: ${navController.previousBackStackEntry?.destination?.displayName}")
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
...
The logs look like this when we step back and it is working correctly
D/MainActivity: -> onSupportNavigateUp()
D/MainActivity: Backstack: dashboardFragment > testingFragment
D/MainActivity: Previous backstack entry: com.example:id/dashboardFragment
D/DashboardFragment: -> onCreateView()
D/BaseFragment: -> onCreateView()
D/DashboardFragment: -> onViewCreated()
I also noticed when using the hamburger in the action bar that it also calls onSupportNavigateUp()
D/MainActivity: -> onSupportNavigateUp()
D/MainActivity: pendingAction: false
D/MainActivity: Backstack: dashboardFragment
D/MainActivity: Previous backstack entry: null
When I use the drawer to navigate to a destination I do see this in the logs and im not sure where/why this is returned or if it has any importance
I/NavController: Ignoring popBackStack to destination com.example:id/mainFragment as it was not found on the current back stack
Now, when its NOT working correctly, this is what the logs look like
D/MainActivity: -> onSupportNavigateUp()
D/MainActivity: Backstack: mainFragment > testingFragment
D/MainActivity: Previous backstack entry: com.example:id/mainFragment
D/MainFragment: -> onCreateView()
D/BaseFragment: -> onCreateView()
D/MainFragment: -> onViewCreated()
This really feels like the popUpTo and popUpToInclusive properties are not being applied (sometimes) when performing the navigation from main fragment to dashboard fragment. It's also suspicious that even though the dashboard fragment is not set as the new starting destination but also its missing from the backstack. Assuming the properties were NOT applied I would expected to see the breadcrumb Backstack: mainFragment > dashboardFragment > testingFragment
Any help would be greatly appreciated!
While there may certainly be a better procedure to follow (as outlined in the Principles of Navigation in the previous comments) the overhead of the change introduces a plethora of new errors and the scope is too large at this time.
Its still unknown why popUpTo and popUpToInclusive are not reliable via XML when navigating (even using NavDirections) however, thus far, passing the NavOptions while navigating seems to resolve the issue.
findNavController().navigate(
MainFragmentDirections.actionMainFragmentToDashboardFragment(),
NavOptions.Builder().setPopUpTo(R.id.mainFragment, true).build()
)
Thus far the issue has yet to arise again.
As it says in Ian's first link:
The back stack always has the start destination of the app at the bottom of the stack.
Your start destination is the "home" one in a navigation graph, the one identified with the little house icon. Set with the startDestination attribute in the XML. When the Navigation library creates a backstack, it always has that destination at the bottom. And it will always be there, even if you try to avoid it with the poUpTo attributes.
That's why if there's a fragment you consider your "home" one, like your dashboard, that needs to explicitly be your startDestination. It's the last thing a user will see if they back out of your app. If you have something like a login or welcome screen as the start destination, they'll back out to that.
Which is why you need to set the "home" fragment as the start destination, and then handle any extra navigation to a login or welcome screen from there. It's just how the Navigation stuff is designed to work. If you try and work around it (I did!) you'll run into other problems, and a lot of the nice features like automatic backstack recreation might not work properly

Basic testing NavigationController in Android

I'm currently new to testing, so I decided to start off with some basic stuff.
I handle all my navigations from a DrawerLayout that is connected to an Activity.
So for my testing I launch an ActivityScenarioRule, create a testNavController object and then I set this testNavController to the current view that handles the navigation (The container fragment).
So the test consists on opening the drawer, clicking on menu item(Will navigate to a fragment) and therefore check if navigated to the fragment.
Then I check if that happened, but the testNavController stays on the same destination which is weird because it performs the click, so I decided to check the navController (The real one inside the activity), and it shows me that navigated to the correct fragment.
Here's the needed code:
#LargeTest
#RunWith(AndroidJUnit4::class)
class MapsActivityTest {
#get:Rule
var activityScenarioRule = ActivityScenarioRule(MapsActivity::class.java)
#Test
fun clickOnDrawerMaps_NavigateToAboutAppFragment() {
//Create TestNavHostController
val testNavController = TestNavHostController(ApplicationProvider.getApplicationContext())
UiThreadStatement.runOnUiThread { // This needed because it throws a exception that method addObserver must be called in main thread
testNavController.setGraph(R.navigation.nav_graph)
}
val scenario = activityScenarioRule.scenario
var navcontroller : NavController? = null
scenario.onActivity {mapsActivity ->
navcontroller = mapsActivity.navController //Get the real navController just to debug
mapsActivity.navController = testNavController //Set the test navController
Navigation.setViewNavController(mapsActivity.binding.containerFragment, testNavController)
}
onView(withId(R.id.drawerLayout)).perform(DrawerActions.open()).check(matches(isOpen()))
onView(withId(R.id.aboutAppFragment)).perform(click())
assertThat(testNavController.currentDestination?.id).isEqualTo(R.id.aboutAppFragment)
}
}
In the example they use a Fragment, which they set the fragment.requireView() on the launch of the fragment, but I think it's exactly the same.
What am I doing wrong here?
When you use ActivityScenario (or ActivityScenarioRule), your activity is brought all the way up to the resumed state before any onActivity calls are made. This means that your real NavController has already been created and used when you call setupWithNavController. This is why your call to setViewNavController() has no effect.
For these types of integration tests (where you have a real NavController), you should not use TestNavHostController.
As per the Test Navigation guide, TestNavHostController is designed for unit tests where you do not have any real NavController at all, such as when testing one fragment in isolation.

Androidx Navigation IllegalStateException after onSaveInstanceState

I have an app using AndroidX's Navigation library, but I'm getting odd behavior. Particularly around my app going in/out of the background. Here are two examples:
In a simple on click listener in a Fragment I have:
(Kotlin)
button.setOnClickListener {
findNavController().popBackStack()
}
From this, I see crashes saying it threw an IllegalStateException since it ran after onSaveInstanceState.
I have a ViewModel associated with my Fragment and I register my observers to the fragment view's lifecycle. This means that I get notified during onStart. Some key events, such as login state determine the app's navigation. In my case I have a splash screen that could go to either a login screen or the main screen. Once a user completes login, I reset the navigation (taking me back to the splash screen). Now the auth state is ready and I want to navigate to the main fragment, this throws an error often because onResume must be called before the FragmentManager is considered ready. I get an error saying I'm in the middle of a transaction and I can't add a new one. To mediate this I had to write this strange bit of code:
(Kotlin)
private fun safeNavigateToMain() {
if (fragmentManager == null) {
return
}
if (!isResumed) {
view?.post { safeNavigateToMain() }
return
}
try {
findNavController().navigate(R.id.main)
} catch (tr: Throwable) {
view?.post { safeNavigateToMain() }
}
}
Does anyone know how I can get the navigation controller to play nice with the fragment lifecycles without having to add these workarounds?
As per the Navigation 1.0.0-alpha03 release notes:
FragmentNavigator now ignores navigation operations after FragmentManager has saved state, avoiding “Can not perform this action after onSaveInstanceState” exceptions b/110987825
So upgrading to alpha03 should remove this error.

How to get NavHostFragment

I'm integrating Android's Navigation Architecture Components into my app. I ran into some problems with passing data to the start of a fragment from an activity, so I was following this answer: Navigation Architecture Component- Passing argument data to the startDestination.
The logic seems sound to me, but I'm struggling to determine how to actually get the NavHostFragment. Elliot Shrock used this line -
val navHostFragment = navFragment as NavHostFragment
But I haven't found a Java equivalent that works.
I tried getting the fragment of my navHost by using
getSupportFragmentManager().findFragmentById(R.id.[mynavhostid])
but this command is returning null. Any ideas?
I solved this by first inflating the view which contains the NavHost, then using the supportFragmentManager to get the navHost Fragment and pass in my default args.
Hoping that Google's Android team provides a cleaner solution to this in the future.
This thing worked for me
val navHostFragment = nav_host_fragment as NavHostFragment
Note, for some reason the debugger will sometimes return null for a NavHostFragment where the code can actually find it without issue. I have no idea why but it's occupied probably 3 hours of my time, make sure it is in fact null by printing or using the fragment!
Actually, verify that the NavHostFragement id in XML matches up with the one you are refering to in your code.
If you want to inflate NavHost via Fragment then use this code in app.
btnClick.setOnClickListener(view -> {
NavHostFragment.findNavController(getParentFragment())
.navigate(R.id.activity_login_to_homeFragment);
}
on this code ID MUST BE SAME AS NAVIGATION.XML File attachment.
☻♥ Done.
So, attempting to retrieve the NavController in onCreate() of the Activity using Navigation.findNavController(Activity, #IdRes int) will fail. You will have to directly retrieve it from the support fragment manager. First, make sure your activity extends AppCompatActivity. Otherwise, you will not be able to use getSupportFragmentManager(). Then, this code should work:
NavHostFragment navHostFragment =
(NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
NavController navController = navHostFragment.getNavController();
Further reading: https://developer.android.com/guide/navigation/navigation-getting-started

Categories

Resources