So I recently migrated to navigation components (2.2.0-alpha01). As I was working on a high end device I didn't really noticed any slowdowns, but as soon as I finished, testers started reporting slugish app navigation.
In my navigation code I use calls like findNavController().navigate(CustomFragmentDirections.actionEtc()) or findNavController().popBackStack(fragmentId, false) I also use safeargs with navigation. In my navigation xml I have actions that heavily rely on popUpTo and app:launchSingleTop="true"
To investigate I made very basic profiler in my BaseFragment class:
private var lastTimestamp = System.currentTimeMillis()
protected fun getEllapsedTime(): String {
val currTime = System.currentTimeMillis()
val elapsedTime = currTime - lastTimestamp
lastTimestamp = currTime
return "${elapsedTime}ms"
}
override fun onAttach(context: Context) {
Timber.d("onAttach(${javaClass.simpleName})(${getEllapsedTime()})")
super.onAttach(context)
}
override fun onCreate(savedInstanceState: Bundle?) {
Timber.d("onCreate(${javaClass.simpleName})(${getEllapsedTime()})")
super.onCreate(savedInstanceState)
savedInstanceState?.let { restoreState(it) }
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
Timber.d("onCreateView(${javaClass.simpleName})(${getEllapsedTime()})")
return inflater.inflate(layoutRes, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Timber.d("onViewCreated(${javaClass.simpleName})(${getEllapsedTime()})")
super.onViewCreated(view, savedInstanceState)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
Timber.d("onActivityCreated(${javaClass.simpleName})(${getEllapsedTime()})")
super.onActivityCreated(savedInstanceState)
}
override fun onStart() {
Timber.d("onStart(${javaClass.simpleName})(${getEllapsedTime()})")
super.onStart()
}
override fun onResume() {
Timber.d("onResume(${javaClass.simpleName})(${getEllapsedTime()})")
super.onResume()
requireActivity().window?.decorView?.post {
firstFrameRendered()
}
}
private fun firstFrameRendered() {
Timber.d("onFrameRendered(${javaClass.simpleName})(${getEllapsedTime()})")
}
override fun onPause() {
Timber.d("onPause(${javaClass.simpleName})(${getEllapsedTime()})")
super.onPause()
}
override fun onStop() {
Timber.d("onStop(${javaClass.simpleName})(${getEllapsedTime()})")
super.onStop()
}
override fun onDestroyView() {
Timber.d("onDestroyView(${javaClass.simpleName})(${getEllapsedTime()})")
super.onDestroyView()
}
override fun onDestroy() {
Timber.d("onDestroy(${javaClass.simpleName})(${getEllapsedTime()})")
super.onDestroy()
}
override fun onDetach() {
Timber.d("onDetach(${javaClass.simpleName})(${getEllapsedTime()})")
super.onDetach()
}
I tried profiling using android studio profiler, but didn't really noticed anything out of the ordinary. I also tried window.addOnFrameMetricsAvailableListener but it pretty much gave me same results as my profiler code. The main method of importance is onFrameRendered. Basic Idea is to let layout inflate and render and immediately after screen is rendered count how many milliseconds passed since onResume was called.
I tried different devices and timings were not very consistent, but after measuring same transitions many times I noticed some tendency, that all my layouts now take almost twice as long to load when compared to previous app navigation which was using simple supportFragmentManager transactions.
I tried isolating navigation from one fragment to the other and I would always get this poor performance.
At the moment I know it has something to do with the way navigation switches fragments, because if I mock NavController with my custom code that just directly uses FragmentManager I get the same good performance as the old code. Will update the question if I'll find the exact problem.
Meanwhile, does anyone have any ideas what might be wrong?