Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Let the button do safe navigation

Let the button do safe navigation

Explore behaviors of Jetpack Navigation Component.
Share some tips when doing navigation from a click event.

MAO YUFENG

August 04, 2019
Tweet

More Decks by MAO YUFENG

Other Decks in Technology

Transcript

  1. 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
  2. NavController holds its own back stack of navigation info and

    check the state before each navigation private final Deque<NavBackStackEntry> mBackStack = new ArrayDeque<>(); NavController.java
  3. 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
  4. 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
  5. 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
  6. fun NavController.safeNavigate( @IdRes currentId: Int, @IdRes destId: Int, ) {

    if (currentDestination?.id == currentId) { navigate(destId) } } NavigationExt.kt:
  7. 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:
  8. <fragment android:id="@+id/fooFragment" android:name="com.github.lcdsmao.uievent.FooFragment" android:label="FooFragment"> <action android:id="@+id/action_fooFragment_to_dialogFragment" app:destination="@id/dialogFragment" /> </fragment> <dialog

    android:id="@+id/dialogFragment" android:name="com.github.lcdsmao.uievent.DialogFragment" android:label="DialogFragment" /> 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
  9. 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
  10. 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) } }
  11. 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) } } }
  12. • Check current destination ID before navigation to avoid the

    crash • Do navigation after onResume to avoid launch duplicate Activities Recap