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

[Daniel Galpin] Adventures in Navigation

[Daniel Galpin] Adventures in Navigation

Presentation from GDG DevFest Ukraine 2018 - the biggest community-driven Google tech conference in the CEE.

Learn more at: https://devfest.gdg.org.ua

__

Take a spin with the new Android Jetpack Navigation Components library announced at Google I/O. The Navigation Components let you keep all of your navigation information in one place and include support for Material components such as bottom navigation, the app drawer, and the overflow menu.

Dan will explore the basics and then dive into a series of more complex use cases. Learn about advanced topics such as deep-linking, leveraging Navigation to simplify modularization for instant apps and Dynamic App Delivery, using sub-graphs, manipulating the back-stack, sharing a ViewModel between navigation destinations and more. See what Navigation components can do for you!

Google Developers Group Lviv

October 13, 2018
Tweet

More Decks by Google Developers Group Lviv

Other Decks in Technology

Transcript

  1. VS

  2. ???

  3. ???

  4. The Navigation Component // Main Navigation Component code implementation "android.arch.navigation:navigation-fragment:$version"

    // NavigationUI static methods to setup action/toolbars, drawers and menus implementation "android.arch.navigation:navigation-ui:$version" // Safe Args code generation plugin for type safety apply plugin: 'androidx.navigation.safeargs' // Test helpers androidTestImplementation "android.arch.navigation:navigation-testing:$nav_version"
  5. The Navigation Component // Main Navigation Component code implementation "android.arch.navigation:navigation-fragment-ktx:$version"

    // NavigationUI static methods to setup action/toolbars, drawers and menus implementation "android.arch.navigation:navigation-ui-ktx:$version" // Safe Args code generation plugin for type safety (no KTX version) apply plugin: 'androidx.navigation.safeargs' // Test helpers androidTestImplementation "android.arch.navigation:navigation-testing-ktx:$nav_version"
  6. The Navigation Component • Visualizes navigation paths • Defines navigation

    in one place • Reduces nav pattern boilerplate That's a lot of steps for a NavDrawer!
  7. The Navigation Component • Visualizes navigation paths • Defines navigation

    in one place • Reduces nav pattern boilerplate • Implements Material guidelines (including backstack)
  8. Benefits of Single Activity • Single navigation graph • Transition

    animations for property and views • Activity ViewModels can be shared amongst fragments NavHost ViewModel LiveData LiveData LiveData
  9. Use actions • You can see them • Your navigation

    graph will contain more information Transitions between destinations Argument passing Backstack manipulation
  10. Use actions • You can see them • Your navigation

    graph will contain more information • You can use safe args Argument passing
  11. ID's Are Used to Represent Lots of Things • R.id.win_action

    // id for Action • R.id.win_destination // id for Destination • R.id.win_menu_item // id for Menu Item • R.id.win_image_view // id for ImageView • navController.navigate(R.id.win) // navigating to ¯\_(ツ)_/¯
  12. Using sensible naming conventions helps • R.id.win_action // id for

    Action • R.id.win_destination // id for Destination • R.id.win_menu_item // id for Menu Item • R.id.win_image_view // id for ImageView • navController.navigate(R.id.win.action) // id for action
  13. Safe Args //build.gradle (project) classpath "android.arch.navigation:navigation-safe-args -gradle-plugin:$version" //build.gradle (app) apply

    plugin: 'androidx.navigation.safeargs' "Directions" for any destination with Actions, named after the class "Args" for every destination with argument(s)
  14. Navigation Drawer Apps with five or more top-level destinations Apps

    with two or more levels of navigation hierarchy Quick navigation between unrelated destinations
  15. Bottom Navigation Apps with three to five top-level destinations that

    need to be accessible anywhere in the app Mobile and Tablet only
  16. Overflow Menu override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu,

    inflater) inflater.inflate(R.menu.overflow_menu, menu) MenuCompat.setGroupDividerEnabled(menu, true); } override fun onOptionsItemSelected(item: MenuItem): Boolean { return NavigationUI.onNavDestinationSelected(item, findNavController()) || super.onOptionsItemSelected(item) }
  17. Setup Bottom Navigation // In onCreate of Activity // Setup

    the NavDrawer by passing the NavigationView to NavigationUI NavigationUI.setupWithNavController(binding.bottomNav, navController)
  18. The Up Button - Fragment + Toolbar override fun onCreateView(inflater:

    LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { ... val navController = findNavController() NavigationUI.setupWithNavController(binding.toolbar, navController) ... }
  19. The Up Button - Activity + ActionBar override fun onCreate(savedInstanceState:

    Bundle?) { ... val navController = this.findNavController(R.id.myNavHostFragment) NavigationUI.setupActionBarWithNavController(this,navController) ... } override fun onSupportNavigateUp(): Boolean { val navController = this.findNavController(R.id.myNavHostFragment) return NavigationUI.navigateUp(navController) }
  20. override fun onCreate(savedInstanceState: Bundle?) { ... NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout) NavigationUI.setupWithNavController(navView,

    navController) ... } override fun onSupportNavigateUp(): Boolean { return NavigationUI.navigateUp(drawerLayout, navController) } Setup a NavDrawer
  21. public static void setupWithNavController(@NonNull final NavigationView navigationView, @NonNull final NavController

    navController) { navigationView.setNavigationItemSelectedListener(new OnNavigationItemSelectedListener() { public boolean onNavigationItemSelected(@NonNull MenuItem item) { boolean handled = NavigationUI.onNavDestinationSelected(item, navController, true); if (handled) { ViewParent parent = navigationView.getParent(); if (parent instanceof DrawerLayout) { ((DrawerLayout)parent).closeDrawer(navigationView); } } return handled; } });
  22. static boolean onNavDestinationSelected(@NonNull MenuItem item, @NonNull NavController navController, boolean popUp)

    { ... if (popUp) { builder.setPopUpTo(findStartDestination(navController.getGraph()).getId(), false); } NavOptions options = builder.build(); ... popUp
  23. Activity vs. Fragment for Detail • Is the toolbar global

    nav? • Is the bottom nav global nav? • Can we get the up button to function easily?
  24. Activity vs. Fragment for Detail • Is the toolbar global

    nav? ◦ No - it changes radically • Is the bottom nav global nav? ◦ Yes • Can we get the up button to function easily?
  25. Setup Action Bar, Tool Bar // Setting up other fragments

    without CollapsingToolbar NavigationUI.setupWithNavController(toolbar, navController) // Setting up DetailFragment with CollapsingToolbar Navigation NavigationUI.setupWithNavController(collapsingToolbarLayout, binding.toolbar, navController) // No need to do anything else for up button
  26. Activity vs. Fragment for Detail • Is the toolbar global

    nav? ◦ No - it changes radically • Is the bottom nav global nav? ◦ Yes • Can we get the up button to function easily? ◦ Yes • What about transitions? ◦ Looks a little strange
  27. fun selectPlayerProfile( playerName: String, view: ImageView) { val navController =

    view.findNavController() val extras = FragmentNavigatorExtras( view to playerName ) navController.navigate( LeaderboardDirections.actionLeaderboardToUserProfile(playerName), extras) } Supporting Shared Elements in Navigation
  28. Shared Element Transitions // in user profile fragment val enterTransitionSet

    = TransitionSet() enterTransitionSet.addTransition(TransitionInflater.from(context) .inflateTransition(android.R.transition.move)) sharedElementEnterTransition = enterTransitionSet // in leaderboard fragment postponeEnterTransition() recyclerView.getViewTreeObserver() .addOnPreDrawListener { startPostponedEnterTransition() true } val returnTransitionSet = TransitionSet() returnTransitionSet.addTransition(TransitionInflater.from(context) .inflateTransition(android.R.transition.move)) sharedElementReturnTransition = returnTransitionSet
  29. Shared Element Transitions - Waiting for Images @BindingAdapter("sharedElementImageUrl") fun bindSharedImage(imageView:

    ImageView, url: String?) { Glide.with(fragment).load(url) .listener(object : RequestListener<Drawable?> { override fun onResourceReady(_: Drawable?, _: Any?, _: Target<Drawable?>?, _: DataSource?, _: Boolean): Boolean { fragment.startPostponedEnterTransition() return false } override fun onLoadFailed(e: GlideException?, _: Any?, _: Target<Drawable?>?, _: Boolean): Boolean { fragment.startPostponedEnterTransition() return false } }) .into(imageView) }
  30. Less so... • Decide whether to go to win or

    lose screen • Data not usually in the fragment • Avoid having lots of navigation logic in activities/fragments
  31. Guide to App Architecture • One way to do it,

    using Lifecycle libraries • developer.android.com/jetpack/docs/g uide
  32. Fragment Data Layer ViewModel LiveData LiveData LiveData Does actual navigation

    Provides any non-transient data Holds data needed to make navigation decisions Communicated decisions back to fragment via observation Where all the conditional navigation decisions can be made
  33. Navigates User answers last question Fragment Data Layer ViewModel LiveData

    LiveData LiveData Takes the data such as score, difficulty, etc and decides whether it's a win or lose Communicates win/loss to Fragment
  34. LiveData to Navigate Problem • LiveData holds data state ◦

    Ex. "This button is green" • Data state != to an event ◦ Ex. Anything that fires and is done ◦ "Show this notification" • Navigation is an event ◦ You never stay in the "navigating to Win" state Fragment Data Layer ViewModel LiveData LiveData LiveData Communicates win/loss to Fragment
  35. Event (Wrapper around LiveData to only handle once) open class

    Event<out T>(private val content: T) { var hasBeenHandled = false private set // Allow external read but not write fun getContentIfNotHandled(): T? { return if (hasBeenHandled) { null } else { hasBeenHandled = true content } } fun peekContent(): T = content }
  36. Fragment - User answers last question // Call a ViewModel

    method from the Fragment when user finishes the quiz binding.quizFinishedButton.setOnClickListener { viewModel.quizFinished() }
  37. ViewModel - Decide win or lose // ViewModel and LiveData

    val navigateToWin = MutableLiveData<Event<Int>>() val navigateToGameOver = MutableLiveData<Event<Int>>() fun quizFinished() { //Do logic with score, rules, etc to see if game was won }
  38. ViewModel with LiveData Event - Communicate Navigation // ViewModel and

    LiveData val navigateToWin = MutableLiveData<Event<Int>>() val navigateToGameOver = MutableLiveData<Event<Int>>() fun quizFinished() { //Do logic with score, rules, etc to see if game was won if (wonGame) { navigateToWin.value = Event(score) } else { navigateToGameOver.value = Event(score) } }
  39. Fragment - Navigate! // Observer when navigation events happen viewModel.navigateToWin.observe(this,

    Observer { it.getContentIfNotHandled()?.let { score -> val action = InGameDirections.winAction() action.setScore(score) navController.navigate(action) } }) // Similar observer for gameOverAction
  40. Login When I navigate or when my logged in state

    changes, check if I should be sent to the login flow.
  41. Nested Graphs • Good for sub-flows like login • Navigating

    to Nested Graphs will always send you to start destination Start Destination
  42. Global Action • Has a destination, but no starting point

    • Can be used by any destination in the graph
  43. Global Action • Has a destination, but no starting point

    • Can be used by any destination in the graph • An action not nested in a destination is a global action
  44. <navigation android:id="@+id/main_navigation" app:startDestination="@id/fragmentA"> <fragment android:id="@+id/fragmentA".../> <fragment android:id="@+id/fragmentB".../> <fragment android:id="@+id/fragmentC"...> <action

    ... /> </fragment> <fragment android:id="@+id/detailFragment"...> <action ... /> </fragment> <fragment android:id="@+id/fragmentDetailTwo"/> </navigation>
  45. <navigation android:id="@+id/main_navigation" app:startDestination="@id/fragmentA"> <fragment android:id="@+id/fragmentA".../> <fragment android:id="@+id/fragmentB".../> <fragment android:id="@+id/fragmentC"...> <action

    ... /> </fragment> <fragment android:id="@+id/detailFragment"...> <action ... /> </fragment> <fragment android:id="@+id/fragmentDetailTwo"/> <navigation android:id="@+id/login" app:startDestination="@id/loginFragment"> <!-- nested graph fragments here --> </navigation> </navigation> Login nested graph
  46. <navigation android:id="@+id/main_navigation" app:startDestination="@id/fragmentA"> <fragment android:id="@+id/fragmentA".../> <fragment android:id="@+id/fragmentB".../> <fragment android:id="@+id/fragmentC"...> <action

    ... /> </fragment> <fragment android:id="@+id/detailFragment"...> <action ... /> </fragment> <fragment android:id="@+id/fragmentDetailTwo"/> <navigation android:id="@+id/login" app:startDestination="@id/loginFragment"> <!-- nested graph fragments here --> </navigation> <action android:id="@+id/action_global_login" app:destination="@id/login" /> </navigation> Global action
  47. Login • Use nested graph for Login flow • Use

    global action • Use shared Activity ViewModel
  48. LiveData Activity ViewModel currentDestinationId: LiveData<Int> isLoggedIn: LiveData<boolean> navigateToLogin: LiveData<Event<Unit>> Just

    mentioned how to set this up Depends on what you're doing to implement login You can use an onNavigatedListener
  49. OnNavigatedListener • Called as you arrive at a destination ◦

    Includes current destination • Useful for updating Navigation UI • Remember to remove listener
  50. OnNavigatedListener // In Activity onCreate navigationListener = NavController.OnNavigatedListener { controller,

    destination -> // Redirect navigation here } navController.addOnNavigatedListener(navigationListener) // In Activity onDestroy navController.removeOnNavigatedListener(navigationListener) Redirect navigation
  51. Login • Use nested graph for Login flow • Use

    global action • Tracking current destination with OnNavigatedListener • Use popTo for correct login flow behavior
  52. When login finishes - pop entire graph off the back

    stack to go back to whatever was before...
  53. popUpTo with Global Action <navigation android:id="@+id/login" app:startDestination="@id/loginFragment"> <action android:id="@+id/finish_login" app:popUpTo="@id/login"

    app:popUpToInclusive="true" /> <!-- Rest of Login NavGraph --> </navigation> Global action - pops to whatever was on the back stack right before
  54. Creating a Deep Link Pending Intent val pendingIntent = NavDeepLinkBuilder(context!!)

    .setGraph(R.navigation.navigation) .setDestination(R.id.gameWonFragment) .createPendingIntent()
  55. Deep Linking with an Argument <fragment android:id="@+id/user_profile" android:name="com.navsample.ui.UserProfile" android:label="fragment_user_profile" tools:layout="@layout/fragment_user_profile"

    > <argument android:name="selectedUser" app:type="string" /> <deepLink app:uri="http://www.navsample.com/userprofile/{selectedUser}/" /> </fragment>
  56. Deep Linking with an Argument <fragment android:id="@+id/user_profile" android:name="com.navsample.ui.UserProfile" android:label="fragment_user_profile" tools:layout="@layout/fragment_user_profile"

    > <argument android:name="selectedUser" app:type="string" /> <deepLink app:uri="http://www.navsample.com/userprofile/{selectedUser}/" /> </fragment>
  57. Deep Linking with an Argument <fragment android:id="@+id/user_profile" android:name="com.navsample.ui.UserProfile" android:label="fragment_user_profile" tools:layout="@layout/fragment_user_profile"

    > <argument android:name="selectedUser" app:type="string" /> <deepLink app:uri="http://www.navsample.com/userprofile/{selectedUser}/" /> </fragment>
  58. Adding the Intent Filter to the AndroidManifest <activity android:name=".ui.MainActivity"> <intent-filter>

    <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <!-- Handle deep link into user profile --> <nav-graph android:value="@navigation/nav_graph" /> </activity>
  59. Instant App Module Structure base a b c installable nav_graph_a

    nav_graph_b nav_graph_c nav_graph_base None of these graphs should reference each other's ids
  60. Instant App Module Structure base a b c installable nav_graph_a

    nav_graph_b nav_graph_c nav_graph_base Installable main_nav_graph overrides base, but only for the installed app main_nav_graph main_nav_graph
  61. main_nav_graph.xml (installable) <navigation app:startDestination="@id/nav_graph_base"> <include app:graph="@navigation/nav_graph_base" /> <include app:graph="@navigation/nav_graph_a" />

    <include app:graph="@navigation/nav_graph_b" /> <include app:graph="@navigation/nav_graph_c" /> </navigation> Includes all feature graphs
  62. Instant App Module Structure base a b c installable nav_graph_a

    nav_graph_b nav_graph_c nav_graph_base To go from base to a,b and c in the installable app world... main_nav_graph main_nav_graph
  63. main_nav_graph.xml (installable) <navigation app:startDestination="@id/nav_graph_base"> <include app:graph="@navigation/nav_graph_base" /> <include app:graph="@navigation/nav_graph_a" />

    <include app:graph="@navigation/nav_graph_b" /> <include app:graph="@navigation/nav_graph_c" /> </navigation> Includes nav_graph_a
  64. Instant App Module Structure base a b c installable nav_graph_a

    nav_graph_b nav_graph_c nav_graph_base To go from base to a,b and c in the instant app world... main_nav_graph main_nav_graph
  65. main_nav_graph.xml (base) <navigation app:startDestination="@id/base_dest"> <include app:graph="@navigation/nav_graph_base" /> <activity android:id="@id/a_dest" app:data="https://navigation.instantappsample.com/a"

    app:action="android.intent.action.VIEW"/> <!-- similar actions for b and c --> </navigation> Implicit Activity Deeplink, called a_dest Uri https://navigation.instantappsample.com/a
  66. nav_graph_a.xml <navigation android:id="@id/a_dest" app:startDestination="@id/a_fragment"> <fragment android:id="@+id/a_fragment" ...> <deepLink app:uri="navigation.instantappsample.com/a" />

    </fragment> <!-- graph A destinations --> </navigation> Deep link destination for Uri https://navigation.instantappsample.com/a
  67. Instant App Module Structure base a b c installable nav_graph_a

    nav_graph_b nav_graph_c nav_graph_base To go from a to others features... main_nav_graph main_nav_graph
  68. nav_graph_b.xml <navigation ...> <fragment android:id="@+id/b_fragment" ...> <deepLink app:uri="navigation.instantappsample.com/b" /> <...>

    </fragment> <...> </navigation> Deep link destination for Uri https://navigation.instantappsample.com/b
  69. nav_graph_a.xml <navigation ...> <fragment android:id="@+id/a_fragment" ...> <deepLink app:uri="navigation.instantappsample.com/a" /> <action

    android:id="@+id/action_a_to_b" app:destination="@id/b_activity" /> </fragment> <!-- graph A destinations --> <activity android:id="@+id/b_activity" app:data="https://navigation.instantappsample.com/b" app:action="android.intent.action.VIEW" /> <!-- Similar destinations for base and c --> </navigation> Implicit Activity Deeplink, called b_Activity Uri https://navigation.instantappsample.com/a