Easy debugging with the Android Navigation component
By: Víctor Albertos
Tags: Debugging, Android, Cookpad, Navigation Component

It’s handy to be able to log the navigation path of the users and make its rendering layer dependant on the build type.

For debug builds, during development, printing the navigation flow to the Android logcat can help to get familiar with the relations between fragments and screens: sometimes it is not easy to link a view with its component representation, as for big apps, the teams don’t usually know the details of the implementation of each screen.

For release builds, when a crash is reported on production, having the navigation path alongside the stacktrace can help to spot quicker what’s the cause of the bug. In Cookpad we use Crashlytics, which makes it pretty easy to send the information and attach it to the right user session:

FirebaseCrashlytics.getInstance().log(destinationData.toString())

Creating a mechanism for logging the navigational flow is quite easy if you use the Android Navigation component. In the Cookpad Android app, we have a class called NavTracker which implements NavController.OnDestinationChangedListener and it is in charge of extracting all the relevant data of the current destination and its associated Bundle.

It has NavTrackerLog as a dependency that hides the layer in which the data is printed out making it that way build-dependant. Thus, this interface is conveniently implemented in the release build with the Crashlytics call shown before and in the debug build as follows:

NavTrackerLogDebug : NavTrackerLog {

override fun log(destinationData: JSONObject) {

Log.d(NavTracker::class.simpleName, destinationData.toString())

}

}

And this is the NavTracker class:

class NavTracker(private val context: Context, private val navTrackerLog: NavTrackerLog) :

NavController.OnDestinationChangedListener { override fun onDestinationChanged(controller: NavController, destination: NavDestination, arguments: Bundle?) {

val res = context.resources

navTrackerLog.log(

JSONObject()

.put("destination", res.getResourceEntryName(destination.id))

.put("startDestination", res.getResourceEntryName(controller.graph.startDestination))

.put("bundleJsonData", bundleToJson(arguments ?: Bundle()))

)

} private fun bundleToJson(bundle: Bundle) = JSONObject()

.apply {

bundle.keySet().forEach { key ->

put(key, JSONObject.wrap(bundle.get(key)))

}

}

}

As the last step, NavTracker is set to all the available NavController(s), by calling addOnDestinationChangedListener on them and supplying the instance of NavTracker:

findNavController(R.id.your_nav_host_fragment).addOnDestinationChangedListener(navTracker)

This is the result of a navigational path consisting of three screens:

feed, recipe detail, and user profile

Android Logcat output:

{

"destination":"feedTabFragment",

"startDestination":"feedTabFragment",

"bundleJsonData":{



}

} {

"destination":"recipeViewFragment",

"startDestination":"feedTabFragment",

"bundleJsonData":{

"showTranslatedRecipe":false,

"recipe":null,

"recipeId":"4042",

"deepLinkUri":null,

"deepLinkVia":null,

"isDeepLink":false,

"isLaunchForEditsRestore":false

}

} {

"destination":"userProfileFragment",

"startDestination":"feedTabFragment",

"bundleJsonData":{

"userId":"38",

"deepLinkUri":null,

"showTranslatedProfile":false,

"scrollToRecipes":false

}

}

Firebase console output: