With the most recent launch of Jetpack Navigation 2.8.0, the kind protected navigation APIs for constructing navigation graphs in Kotlin are secure 🎉. This implies that you may outline your locations utilizing serializable sorts and profit from compile-time security.
That is nice information if you happen to’re utilizing Jetpack Compose in your UI as a result of it’s less complicated and safer to outline your navigation locations and arguments.
The design philosophy behind these new APIs is roofed in this weblog publish which accompanied the primary launch they appeared in — 2.8.0-alpha08.
Since then, we’ve obtained and integrated a lot of suggestions, fastened some bugs and made a number of enhancements to the API. This text covers the secure API and factors out modifications for the reason that first alpha launch. It additionally appears at the right way to migrate current code and offers some recommendations on testing navigation use instances.
The brand new kind protected navigation APIs for Kotlin let you use Any
serializable kind to outline navigation locations. To make use of them, you’ll have to add the Jetpack navigation library model 2.8.0 and the Kotlin serialization plugin to your venture.
As soon as performed, you should use the @Serializable
annotation to robotically create serializable sorts. These can then be used to create a navigation graph.
The rest of this text assumes you’re utilizing Compose as your UI framework (by together with navigation-compose
in your dependencies), though the examples ought to work equally properly with Fragments (with some slight variations). When you’re utilizing each, we’ve some new interop APIs for that too.
A very good instance of one of many new APIs is composable
. It now accepts a generic kind which can be utilized to outline a vacation spot.
@Serializable information object ResidenceNavHost(navController, startDestination = Residence) {
composable<Residence> {
HomeScreen()
}
}
Nomenclature is necessary right here. In navigation phrases, Residence
is a route which is used to create a vacation spot. The vacation spot has a route kind and defines what can be displayed on display at that vacation spot, on this case HomeScreen
.
These new APIs will be summarized as: Any methodology that accepts a route now accepts a generic kind for that route. The examples that comply with use these new strategies.
One of many major advantages of those new APIs is the compile-time security offered through the use of sorts for navigation arguments. For fundamental sorts, it’s tremendous easy to cross them to a vacation spot.
Let’s say we’ve an app which shows merchandise on the Residence display. Clicking on any product shows the product particulars on a Product display.
We will outline the Product route utilizing a knowledge class which has a String id discipline which can comprise the product ID.
@Serializable information class Product(val id: String)
By doing so, we’re establishing a few navigation guidelines:
- The
Product
route should all the time have anid
- The kind of
id
is all the time aString
You should utilize any fundamental kind as a navigation argument, together with lists and arrays. For extra complicated sorts, see the “Customized sorts” part of this text.
New since alpha: Nullable sorts are supported.
New since alpha: Enums are supported (though you’ll want to make use of
@Preserve
on the enum declaration to make sure that the enum class is just not eliminated throughout minified builds, monitoring bug)
After we use this path to outline a vacation spot in our navigation graph, we will get hold of the route from the again stack entry utilizing toRoute
. This will then be handed to no matter is required to render that vacation spot on display, on this case ProductScreen
. Right here’s how our vacation spot is applied:
composable<Product> { backStackEntry ->
val product : Product = backStackEntry.toRoute()
ProductScreen(product)
}
New since alpha: When you’re utilizing a ViewModel to supply state to your display, you can too get hold of the route from
savedStateHandle
utilizing thetoRoute
extension operate.
ProductViewModel(non-public val savedStateHandle: SavedStateHandle, …) : ViewModel {
non-public val product : Product = savedStateHandle.toRoute()
// Arrange UI state utilizing product
}
Be aware on testing: As of launch 2.8.0, SavedStateHandle.toRoute
depends on Android Bundle
. This implies your ViewModel assessments will should be instrumented (e.g. through the use of Robolectric or by working them on an emulator). We’re methods we will take away this dependency in future releases (tracked right here).
Utilizing the path to cross navigation arguments is easy — simply use navigate
with an occasion of the route class.
navController.navigate(route = Product(id = "ABC"))
Right here’s an entire instance:
NavHost(
navController = navController,
startDestination = Residence
) {
composable<Residence> {
HomeScreen(
onProductClick = { id ->
navController.navigate(route = Product(id))
}
)
}
composable<Product> { backStackEntry ->
val product : Product = backStackEntry.toRoute()
ProductScreen(product)
}
}
Now that you know the way to cross information between screens inside your app, let’s take a look at how one can navigate and cross information into your app from outdoors.
Typically you need to take customers on to a particular display inside your app, relatively than beginning on the dwelling display. For instance, if you happen to’ve simply despatched them a notification saying “take a look at this new product”, it makes excellent sense to take them straight to that product display once they faucet on the notification. Deep hyperlinks allow you to do that.
Right here’s the way you add a deep hyperlink to the Product
vacation spot talked about above:
composable<Product>(
deepLinks = listOf(
navDeepLink<Product>(
basePath = "www.hellonavigation.instance.com/product"
)
)
) {
…
}
navDeepLink
is used to assemble the deep hyperlink URL from each the category, on this case Product
, and the equipped basePath
. Any fields from the equipped class are robotically included within the URL as parameters. The generated deep hyperlink URL is:
www.hellonavigation.instance.com/product/{id}
To check it, you possibly can use the next adb command:
adb shell am begin -a android.intent.motion.VIEW -d "https://www.hellonavigation.instance.com/product/ABC" com.instance.hellonavigation
This can launch the app straight on the Product
vacation spot with the Product.id
set to “ABC”.
We’ve simply seen an instance of the navigation library robotically producing a deep hyperlink URL containing a path parameter. Path parameters are generated for required route arguments. our Product
once more:
@Serializable information class Product(val id: String)
The id
discipline is necessary so the deep hyperlink URL format of /{id}
is appended to the bottom path. Path parameters are all the time generated for route arguments, besides when:
1. the category discipline has a default worth (the sector is elective), or
2. the category discipline represents a set of primitive sorts, like a Record<String>
or Array<Int>
(full listing of supported sorts, add your personal by extending CollectionNavType
)
In every of those instances, a question parameter is generated. Question parameters have a deep hyperlink URL format of ?identify=worth
.
Right here’s a abstract of the various kinds of URL parameter:
New since alpha: Empty strings for path parameters at the moment are supported. Within the above instance, if you happen to use a deep hyperlink URL of www.hellonavigation.instance.com/product// then the id discipline can be set to an empty string.
When you’ve arrange your app’s manifest to just accept incoming hyperlinks, a straightforward solution to take a look at your deep hyperlinks is to make use of adb
. Right here’s an instance (be aware that &
is escaped):
adb shell am begin -a android.intent.motion.VIEW -d “https://hellonavigation.instance.com/product/ABC?coloration=pink&variants=var1&variants=var2" com.instance.hellonavigation
🐞Debugging tip: When you ever need to examine the generated deep hyperlink URL format, simply print the NavBackStackEntry.vacation spot.route out of your vacation spot and it’ll seem in logcat once you navigate to that vacation spot:
composable<Product>( … ) { backStackEntry ->
println(backStackEntry.vacation spot.route)
}
We’ve already touched on how one can take a look at deep hyperlinks utilizing adb however let’s dive a bit deeper into how one can take a look at your navigation code. Navigation assessments are normally instrumented assessments which simulate the person navigating by way of your app.
Right here’s a easy take a look at which verifies that once you faucet on a product button, the product display is displayed with the proper content material.
@RunWith(AndroidJUnit4::class)
class NavigationTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()@Check
enjoyable onHomeScreen_whenProductIsTapped_thenProductScreenIsDisplayed() {
composeTestRule.apply {
onNodeWithText("View particulars about ABC").performClick()
onNodeWithText("Product particulars for ABC").assertExists()
}
}
}
Basically, you aren’t interacting along with your navigation graph straight — as a substitute, you might be simulating person enter with the intention to assert that your navigation routes result in the proper content material.
🐞Debugging tip: When you ever need to pause an instrumented take a look at however nonetheless work together with the app, you should use composeTestRule.waitUntil(timeoutMillis = 3_600_000, situation = { false })
. Paste this right into a take a look at proper earlier than a failure level, then poke round with the app to attempt to perceive why the take a look at fails (you’ve got an hour — hopefully lengthy sufficient to determine it out!). The structure inspector even works on the identical time. You can even simply wrap this in a single take a look at if you wish to examine the app’s state with solely the take a look at setup code. That is notably helpful when your instrumented app makes use of faux information which could trigger variations in conduct out of your manufacturing construct.
When you’re already utilizing Jetpack Navigation and defining your navigation graph utilizing the Kotlin DSL, you’ll possible need to replace your current code. Let’s take a look at two common migration use instances: string-based routes and high stage navigation UI.
In earlier releases of Navigation Compose, you wanted to outline your routes and navigation argument keys as strings. Right here’s an instance of a product route outlined this fashion.
const val PRODUCT_ID_KEY = "id"
const val PRODUCT_BASE_ROUTE = "product/"
const val PRODUCT_ROUTE = "$PRODUCT_BASE_ROUTE{$PRODUCT_ID_KEY}"// Inside NavHost
composable(
route = PRODUCT_ROUTE,
arguments = listOf(
navArgument(PRODUCT_ID_KEY) {
kind = NavType.StringType
nullable = false
}
)
) { entry ->
val id = entry.arguments?.getString(PRODUCT_ID_KEY)
ProductScreen(id = id ?: "Not discovered")
}
// When navigating to Product vacation spot
navController.navigate(route = "$PRODUCT_BASE_ROUTE$productId")
Be aware how the kind of the id
argument is outlined in a number of locations (NavType.StringType
and getString
). The brand new APIs enable us to take away this duplication.
Emigrate this code, create a serializable class for the route (or an object if it has no arguments).
@Serializable information class Product(val id: String)
Change any cases of the string-based route used to create locations with the brand new kind, and take away any arguments:
composable<Product> { … }
When acquiring arguments, use toRoute
to acquire the route object or class.
composable<Product> { backStackEntry ->
val product : Product = backStackEntry.toRoute()
ProductScreen(product.id)
}
Additionally change any cases of the string-based route when calling navigate
:
navController.navigate(route = Product(id))
OK, we’re performed! We’ve been in a position to take away the string constants and boilerplate code, and likewise launched kind security for navigation arguments.
Incremental migration
You don’t need to migrate all of your string-based routes in a single go. You should utilize strategies which settle for a generic kind for the route interchangeably with strategies which settle for a string-based route, so long as your string format matches that generated by the Navigation library out of your route sorts.
Put one other manner, the next code will nonetheless work as anticipated after the migration above:
navController.navigate(route = “product/ABC”)
This allows you to migrate your navigation code incrementally relatively than being an “all or nothing” process.
Most apps can have some type of navigation UI which is all the time displayed, permitting customers to navigate to completely different high stage locations.
A vital accountability for this navigation UI is to show which high stage vacation spot the person is at the moment on. That is normally performed by iterating by way of the top-level locations and checking whether or not its route is the same as any route within the present again stack.
For the next instance, we’ll use NavigationSuiteScaffold
which shows the proper navigation UI relying on the out there window measurement.
const val HOME_ROUTE = "dwelling"
const val SHOPPING_CART_ROUTE = "shopping_cart"
const val ACCOUNT_ROUTE = "account"information class TopLevelRoute(val route: String, val icon: ImageVector)
val TOP_LEVEL_ROUTES = listOf(
TopLevelRoute(route = HOME_ROUTE, icon = Icons.Default.Residence),
TopLevelRoute(route = SHOPPING_CART_ROUTE, icon = Icons.Default.ShoppingCart),
TopLevelRoute(route = ACCOUNT_ROUTE, icon = Icons.Default.AccountBox),
)
// Inside your predominant app structure
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.vacation spot
NavigationSuiteScaffold(
navigationSuiteItems = {
TOP_LEVEL_ROUTES.forEach { topLevelRoute ->
merchandise(
chosen = currentDestination?.hierarchy?.any {
it.route == topLevelRoute.route
} == true,
icon = {
Icon(
imageVector = topLevelRoute.icon,
contentDescription = topLevelRoute.route
)
},
onClick = { navController.navigate(route = topLevelRoute.route) }
)
}
}
) {
NavHost(…)
}
Within the new kind protected APIs, you don’t outline your high stage routes as strings, so you may’t use string comparability. As a substitute, use the brand new hasRoute
extension operate on NavDestination
to examine whether or not a vacation spot has a particular route class.
@Serializable information object Residence
@Serializable information object ShoppingCart
@Serializable information object Accountinformation class TopLevelRoute<T : Any>(val route: T, val icon: ImageVector)
val TOP_LEVEL_ROUTES = listOf(
TopLevelRoute(route = Residence, icon = Icons.Default.Residence),
TopLevelRoute(route = ShoppingCart, icon = Icons.Default.ShoppingCart),
TopLevelRoute(route = Account, icon = Icons.Default.AccountCircle)
)
// Inside your predominant app structure
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.vacation spot
NavigationSuiteScaffold(
navigationSuiteItems = {
TOP_LEVEL_ROUTES.forEach { topLevelRoute ->
merchandise(
chosen = currentDestination?.hierarchy?.any {
it.hasRoute(route = topLevelRoute.route::class)
} == true,
icon = {
Icon(
imageVector = topLevelRoute.icon,
contentDescription = topLevelRoute.route::class.simpleName
)
},
onClick = { navController.navigate(route = topLevelRoute.route)}
)
}
}
) {
NavHost(…)
}
It’s simple to confuse lessons and object locations
Can you notice the issue with the next code?
@Serializable
information class Product(val id: String)NavHost(
navController = navController,
startDestination = Product
) { … }
It’s not instantly apparent however if you happen to had been to run it, you’d see the next error:
kotlinx.serialization.SerializationException: Serializer for sophistication ‘Companion’ is just not discovered.
It’s because Product
is just not a sound vacation spot, solely an occasion of Product
is (e.g. Product(“ABC”)
). The above error message is complicated till you notice that the serialization library is searching for the statically initialized Companion
object of the Product
class which isn’t outlined as serializable (actually, we didn’t outline it in any respect, the Kotlin compiler added it for us), and therefore doesn’t have a corresponding serializer.
New since alpha: A lint examine was added to identify locations the place an incorrect kind is getting used for the route. Whenever you attempt to use the category identify as a substitute of the category occasion, you’ll obtain a useful error message: “The route ought to be a vacation spot class occasion or vacation spot object.”. A related lint examine when utilizing
popBackStack
can be added within the 2.8.1 launch.
Utilizing duplicate locations used to end in undefined conduct. This has now been fastened, and (new since alpha) navigating to a reproduction vacation spot will now navigate to the closest vacation spot within the navigation graph which matches, relative to your present vacation spot.
That mentioned, it’s nonetheless not really useful to create duplicate locations in your navigation graph because of the ambiguity it creates when navigating to a type of locations. If the identical content material ought to seem in two locations, create a separate vacation spot class for every one and simply use the identical content material composable.
At the moment, you probably have a route with a String
argument and its worth is ready to the string literal “null”
, the app will crash when navigating to that vacation spot. This concern can be fastened in 2.8.1, due in a few weeks.
Within the meantime, you probably have unsanitized enter to a String
route argument, carry out a examine for “null” first to keep away from the crash.
Don’t use massive objects as routes as you might run into TransactionTooLargeException. When navigating, the route is saved to persist system-initiated course of loss of life and the saving mechanism is a binder transaction. Binder transactions have a 1MB buffer so massive objects can simply fill this buffer.
You may keep away from utilizing massive objects for routes by storing information utilizing a storage mechanism designed for giant information, reminiscent of Room or DataStore. When inserting information, get hold of a singular reference, reminiscent of an ID discipline. You may then use this, a lot smaller, distinctive reference within the route. Use the reference to acquire the information on the vacation spot.
That’s about it for the brand new kind protected navigation APIs. Right here’s a fast abstract of crucial capabilities.
- Outline locations utilizing
composable<T>
(ornavigation<T>
for nested graphs) - Navigate to a vacation spot utilizing
navigate(route = T)
for object routes ornavigate(route = T(…))
for sophistication occasion routes - Acquire a route from a
NavBackStackEntry
orSavedStateHandle
utilizingtoRoute<T>
- Examine whether or not a vacation spot was created utilizing a given route utilizing
hasRoute(route = T::class)
You may take a look at a working implementation of those APIs within the Now in Android app. The migration from string-based routes occurred on this pull request.
We’d love to listen to your ideas on these APIs. Be at liberty to go away a remark, or you probably have any points please file a bug. You may learn extra about the right way to use Jetpack Navigation within the official documentation.
The code snippets on this article have the next license:
// Copyright 2024 Google LLC. SPDX-License-Identifier: Apache-2.0