Share
[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
Transcript
165 slides
-
Things weren't easy Fragment Transactions Deep Links Passing Arguments Error
Prone Boilerplate Testing -
Things weren't consistent Deep Links Up and Back Navigation Drawer
Bottom Navigation Menus -
!= When Your Activity is Part of the Task of
Another App -
Proprietary + Confidential Proprietary + Confidential Proprietary + Confidential A
(quick) tour of the Navigation Component -
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" -
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" -
The Navigation Component • Visualizes navigation paths • Defines navigation
in one place • Reduces nav pattern boilerplate That's a lot of steps for a NavDrawer! -
The Navigation Component • Visualizes navigation paths • Defines navigation
in one place • Reduces nav pattern boilerplate • Implements Material guidelines (including backstack) -
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 -
Use actions • You can see them • Your navigation
graph will contain more information Transitions between destinations Argument passing Backstack manipulation -
Use actions • You can see them • Your navigation
graph will contain more information • You can use safe args Argument passing -
Safe Args Code Generation Plugin Uses your Navigation Graph to
ensure type safety -
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 ¯\_(ツ)_/¯ -
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 -
val matchAction = RegisterDirections. actionRegisterToMatch("User3466") navController.navigate(matchAction) Example with argument without
default R.id.action_register_to_match -
val args = MatchFragmentArgs.fromBundle(arguments) val opponent = args.opponent Match Fragment
Fetching the arguments from the bundle -
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) -
Navigation Drawer Apps with five or more top-level destinations Apps
with two or more levels of navigation hierarchy Quick navigation between unrelated destinations -
Bottom Navigation Apps with three to five top-level destinations that
need to be accessible anywhere in the app Mobile and Tablet only -
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) } -
Setup Bottom Navigation // In onCreate of Activity // Setup
the NavDrawer by passing the NavigationView to NavigationUI NavigationUI.setupWithNavController(binding.bottomNav, navController) -
The Up Button - Fragment + Toolbar override fun onCreateView(inflater:
LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { ... val navController = findNavController() NavigationUI.setupWithNavController(binding.toolbar, navController) ... } -
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) } -
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 -
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; } }); -
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 -
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? -
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? -
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 -
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 -
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 -
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 -
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) } -
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 -
Guide to App Architecture • One way to do it,
using Lifecycle libraries • developer.android.com/jetpack/docs/g uide -
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 -
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 -
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 -
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 } -
Fragment - User answers last question // Call a ViewModel
method from the Fragment when user finishes the quiz binding.quizFinishedButton.setOnClickListener { viewModel.quizFinished() } -
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 } -
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) } } -
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 -
Login When I navigate or when my logged in state
changes, check if I should be sent to the login flow. -
Nested Graphs • Good for sub-flows like login • Navigating
to Nested Graphs will always send you to start destination Start Destination -
Login • Use nested graph for Login flow • Use
global action -
Global Action • Has a destination, but no starting point
Normal action Global action -
Global Action • Has a destination, but no starting point
• Can be used by any destination in the graph -
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 -
<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> -
<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 -
<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 -
Login • Use nested graph for Login flow • Use
global action • Use shared Activity ViewModel -
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 -
OnNavigatedListener • Called as you arrive at a destination ◦
Includes current destination • Useful for updating Navigation UI • Remember to remove listener -
OnNavigatedListener // In Activity onCreate navigationListener = NavController.OnNavigatedListener { controller,
destination -> // Redirect navigation here } navController.addOnNavigatedListener(navigationListener) // In Activity onDestroy navController.removeOnNavigatedListener(navigationListener) Redirect navigation -
Login • Use nested graph for Login flow • Use
global action • Tracking current destination with OnNavigatedListener • Use popTo for correct login flow behavior -
popUpTo <fragment android:id="@+id/signUpFragment" ...> <action android:id="@+id/back_to_login_action" app:popUpTo="@+id/loginFragment" app:popUpToInclusive="false" /> </fragment>
Removes all on back stack until reaching "loginFragment", not including "loginFragment" -
When login finishes - pop entire graph off the back
stack to go back to whatever was before... -
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 -
Creating a Deep Link Pending Intent val pendingIntent = NavDeepLinkBuilder(context!!)
.setGraph(R.navigation.navigation) .setDestination(R.id.gameWonFragment) .createPendingIntent() -
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> -
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> -
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> -
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> -
base a b c instant instant installable installable Instant App
Module Structure -
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 -
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 -
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 -
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"/> <!-- same deeplinks for b and c --> </navigation> Implicit Activity Deeplink for all features -
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 -
nav_graph_base.xml <navigation android:id="@+id/base_dest" app:startDestination="@id/base_fragment"> <fragment android:id="@+id/base_fragment" android:name="com.instantappsamples.navigation.base.BaseFragment"> <action android:id="@+id/action_base_to_a" app:destination="@+id/a_dest"
/> <!--- similar for b and c ---> <deepLink app:uri="navigation.instantappsample.com/base" /> </fragment> </navigation> Action that triggers navigation Destination a_dest -
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 -
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> Destination a_dest (for installable app) -
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 -
nav_graph_base.xml <navigation android:id="@+id/base_dest" app:startDestination="@id/base_fragment"> <fragment android:id="@+id/base_fragment" android:name="com.instantappsamples.navigation.base.BaseFragment"> <action android:id="@+id/action_base_to_a" app:destination="@+id/a_dest"
/> <!--- similar for b and c ---> <deepLink app:uri="navigation.instantappsample.com/base" /> </fragment> </navigation> Action that triggers navigation Destination a_dest -
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 -
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 -
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 -
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 -
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 -
More Adventure! Documentation developer.android.com/topic/libraries/architecture/navigation/ DIY codelabs.developers.google.com/codelabs/android-navigation Samples github.com/googlesamples/android-sunflower github.com/googlesamples/android-architecture-components …
NavigationBasicSample … GithubBrowserSample -
Proprietary + Confidential Proprietary + Confidential Proprietary + Confidential Thank
You Dan Galpin @dagalpin