Slide 1

Slide 1 text

Let the button do safe navigation Mao Yufeng

Slide 2

Slide 2 text

Self-Introduction Mao Yufeng • @myf_pa • https://github.com/lcdsmao • https://medium.com/@lcdsmao • CyberAgent/CATS

Slide 3

Slide 3 text

Ensure navigation is safe

Slide 4

Slide 4 text

Navigation Graph:

Slide 5

Slide 5 text

button_navigate_bar.setOnClickListener { findNavController().navigate(R.id.action_fooFragment_to_barFragment) } FooFragment.kt:

Slide 6

Slide 6 text

java.lang.IllegalArgumentException: navigation destination com.github.lcdsmao.uievent:id action_fooFragment_to_barFragment is unknown to this NavController at androidx.navigation.NavController.navigate(NavController.java:789) at androidx.navigation.NavController.navigate(NavController.java:730) at androidx.navigation.NavController.navigate(NavController.java:716) at androidx.navigation.NavController.navigate(NavController.java:704) at com.github.lcdsmao.uievent.FooFragment$onCreateView$$inlined$apply$lambda$1.onClick(FooFragment.kt:22) at android.view.View.performClick(View.java:6256) at android.view.View$PerformClick.run(View.java:24710) at android.os.Handler.handleCallback(Handler.java:789) at android.os.Handler.dispatchMessage(Handler.java:98) at android.os.Looper.loop(Looper.java:251) at android.app.ActivityThread.main(ActivityThread.java:6572) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767) This exception occurs when calling Action that does not belong to the current Fragment

Slide 7

Slide 7 text

nav_main.xml:

Slide 8

Slide 8 text

nav_main.xml:

Slide 9

Slide 9 text

button_navigate_bar.setOnClickListener { findNavController().navigate(R.id.action_fooFragment_to_barFragment) findNavController().navigate(R.id.action_fooFragment_to_barFragment) } FooFragment.kt:

Slide 10

Slide 10 text

NavController holds its own back stack of navigation info and check the state before each navigation private final Deque mBackStack = new ArrayDeque<>(); NavController.java

Slide 11

Slide 11 text

button_navigate_bar.setOnClickListener { [email protected]("Click NavigateBarMaybeCrash") [email protected]("Before Call Navigate") findNavController().navigate(R.id.action_fooFragment_to_barFragment) [email protected]("After Call Navigate") } V/FooFragment: Click NavigateBar V/FooFragment: Before Call Navigate V/FooFragment: After Call Navigate V/BarFragment: onAttach V/BarFragment: onCreate V/BarFragment: onViewCreated V/BarFragment: onActivityCreated V/BarFragment: onStart V/BarFragment: onResume V/FooFragment: onPause V/FooFragment: onStop V/FooFragment: onDestroyView FragmentTransaction will not be performed immediately FooFragment.kt: Pending input events are canceled at onStop

Slide 12

Slide 12 text

The View system is completely independent from any higher level framework such as Fragments or Navigation, so there's not much we can do. https://issuetracker.google.com/issues/136024230

Slide 13

Slide 13 text

button_navigate_bar.setOnClickListener { val navController = findNavController() navController.navigate(R.id.action_fooFragment_to_barFragment) } Check to see if we haven't already navigated away

Slide 14

Slide 14 text

button_navigate_bar.setOnClickListener { val navController = findNavController() if (navController.currentDestination?.id == R.id.fooFragment) { navController.navigate(R.id.action_fooFragment_to_barFragment) } } Check to see if we haven't already navigated away

Slide 15

Slide 15 text

fun NavController.safeNavigate( @IdRes currentId: Int, @IdRes destId: Int, ) { if (currentDestination?.id == currentId) { navigate(destId) } } NavigationExt.kt:

Slide 16

Slide 16 text

BaseFragment.kt: abstract class BaseFragment : Fragment() { @IdRes open val navId: Int = 0 protected fun navigate(@IdRes destId: Int) { require(navId != 0) { "Need fragment id in navGraph for safe navigation" } findNavController().safeNavigate(desId) } } class FooFragment : BaseFragment() { override val navId: Int = R.id.fooFragment override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) button_navigate_bar.setOnClickListener { navigate(R.id.action_fooFragment_to_barFragment) } } } FooFragment.kt:

Slide 17

Slide 17 text

V/FooFragment: Click ShowDialog V/FooFragment: Before Call Navigate V/FooFragment: After Call Navigate V/DialogFragment: onAttach V/DialogFragment: onCreate V/DialogFragment: onCreateDialog V/DialogFragment: onActivityCreated V/DialogFragment: onStart V/DialogFragment: onResume button_navigate_bar.setOnClickListener { [email protected]("Click NavigateBarMaybeCrash") [email protected]("Before Call Navigate") findNavController().navigate(R.id.action_fooFragment_to_barFragment) [email protected]("After Call Navigate") } Fragment will will not enter onStop when showing a dialogFragment

Slide 18

Slide 18 text

button_navigate_bar.setOnClickListener { findNavController().safeNavigate( R.id.fooFragment, R.id.action_fooFragment_to_barFragment ) } Show dialog:

Slide 19

Slide 19 text

Throttle the navigation destination

Slide 20

Slide 20 text

button_navigate_bar.setOnClickListener { findNavController().navigate(R.id.action_fooFragment_to_barFragment) } Show dialog:

Slide 21

Slide 21 text

V/FooFragment: Click NavigateBaz V/FooFragment: Before Call Navigate V/FooFragment: After Call Navigate V/FooFragment: onPause V/BazActivity: onCreate V/BazActivity: onStart V/BazActivity: onResume V/FooFragment: onStop V/FooFragment: Click NavigateBaz V/FooFragment: Before Call Navigate V/FooFragment: After Call Navigate V/FooFragment: onPause V/BazActivity: onCreate V/BazActivity: onStart V/BazActivity: onResume V/FooFragment: Click NavigateBaz V/FooFragment: Before Call Navigate V/FooFragment: After Call Navigate V/BazActivity: onPause V/BazActivity: onCreate V/BazActivity: onStart V/BazActivity: onResume V/BazActivity: onStop V/FooFragment: onStop Navigate to Activity once: Navigate to Activity twice: - No Crash - CurrentDestinationId not changed

Slide 22

Slide 22 text

BaseFragment.kt: abstract class BaseFragment : Fragment() { @IdRes open val navId: Int = 0 protected fun navigate(@IdRes destId: Int) { require(navId != 0) { "Need fragment id in navGraph for safe navigation" } findNavController().safeNavigate(desId) } }

Slide 23

Slide 23 text

BaseFragment.kt: abstract class BaseFragment : Fragment() { @IdRes open val navId: Int = 0 protected fun navigate(@IdRes destId: Int) { require(navId != 0) { "Need fragment id in navGraph for safe navigation" } if (viewLifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { findNavController().safeNavigate(desId) } } }

Slide 24

Slide 24 text

button_navigate_baz.setOnClickListener { navigate(R.id.action_fooFragment_to_bazActivity) navigate(R.id.action_fooFragment_to_bazActivity) } Two Activities will still be launched …

Slide 25

Slide 25 text

button_navigate_baz.setOnClickListener { navigate(R.id.action_fooFragment_to_bazActivity) } Good enough to deal with the general situation

Slide 26

Slide 26 text

• Check current destination ID before navigation to avoid the crash • Do navigation after onResume to avoid launch duplicate Activities Recap

Slide 27

Slide 27 text

Thank you!