14.3 C
London
Thursday, September 5, 2024

Kind protected navigation for Compose. Jetpack Navigation 2.8.0 enhances… | by Don Turner | Android Builders | Sep, 2024


With the newest launch of Jetpack Navigation 2.8.0, the sort protected navigation APIs for constructing navigation graphs in Kotlin are secure 🎉. This implies that you could outline your locations utilizing serializable sorts and profit from compile-time security.

That is nice information when you’re utilizing Jetpack Compose on your UI as a result of it’s easier 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 acquired 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 adjustments because the first alpha launch. It additionally seems to be at how one can migrate present code and supplies some tips about testing navigation use circumstances.

The brand new sort protected navigation APIs for Kotlin will let you use Any serializable sort 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 challenge.

As soon as achieved, you should utilize the @Serializableannotation to routinely 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 nicely with Fragments (with some slight variations). Should you’re utilizing each, we now have some new interop APIs for that too.

instance of one of many new APIs is composable. It now accepts a generic sort which can be utilized to outline a vacation spot.

@Serializable information object Dwelling

NavHost(navController, startDestination = Dwelling) {
composable<Dwelling> {
HomeScreen()
}
}

Nomenclature is vital right here. In navigation phrases, Dwelling is a route which is used to create a vacation spot. The vacation spot has a route sort and defines what will likely be displayed on display at that vacation spot, on this case HomeScreen.

These new APIs could be summarized as: Any methodology that accepts a route now accepts a generic sort for that route. The examples that observe 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 go them to a vacation spot.

Let’s say we now have an app which shows merchandise on the Dwelling display. Clicking on any product shows the product particulars on a Product display.

Navigation graph with two locations: Dwelling and Product

We are able to outline the Product route utilizing a knowledge class which has a String id discipline which is able to include the product ID.

@Serializable information class Product(val id: String)

By doing so, we’re establishing a few navigation guidelines:

  • The Product route should at all times have an id
  • The kind of id is at all times a String

You should utilize any fundamental sort 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 just isn’t eliminated throughout minified builds, monitoring bug)

Once we use this path to outline a vacation spot in our navigation graph, we are able to receive 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: Should you’re utilizing a ViewModel to supply state to your display, you too can receive the route from savedStateHandle utilizing the toRoute extension perform.

ProductViewModel(personal val savedStateHandle: SavedStateHandle, …) : ViewModel {
personal val product : Product = savedStateHandle.toRoute()
// Arrange UI state utilizing product
}

Notice on testing: As of launch 2.8.0, SavedStateHandle.toRoute depends on Android Bundle. This implies your ViewModel assessments will have to be instrumented (e.g. through the use of Robolectric or by operating them on an emulator). We’re taking a look at methods we are able to take away this dependency in future releases (tracked right here).

Utilizing the path to go navigation arguments is straightforward — 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 = Dwelling
) {
composable<Dwelling> {
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 go information between screens inside your app, let’s have a look at how one can navigate and go information into your app from exterior.

Generally you need to take customers on to a particular display inside your app, relatively than beginning on the dwelling display. For instance, when you’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 after 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 routinely 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 routinely 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 at all times generated for route arguments, besides when:

1. the category discipline has a default worth (the sphere is non-obligatory), or

2. the category discipline represents a set of primitive sorts, like a Listing<String> or Array<Int> (full listing of supported sorts, add your personal by extending CollectionNavType)

In every of those circumstances, a question parameter is generated. Question parameters have a deep hyperlink URL format of ?title=worth.

Right here’s a abstract of the several types of URL parameter:

Path and question parameters

New since alpha: Empty strings for path parameters are actually supported. Within the above instance, when you use a deep hyperlink URL of www.hellonavigation.instance.com/product// then the id discipline could be set to an empty string.

When you’ve arrange your app’s manifest to just accept incoming hyperlinks, a simple strategy to check your deep hyperlinks is to make use of adb. Right here’s an instance (notice that & is escaped):

adb shell am begin -a android.intent.motion.VIEW -d “https://hellonavigation.instance.com/product/ABC?coloration=crimson&variants=var1&variants=var2" com.instance.hellonavigation

🐞Debugging tip: Should 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 while you navigate to that vacation spot:

composable<Product>( … ) { backStackEntry ->
println(backStackEntry.vacation spot.route)
}

We’ve already touched on how one can check deep hyperlinks utilizing adb however let’s dive a bit deeper into how one can check your navigation code. Navigation assessments are normally instrumented assessments which simulate the consumer navigating by means of your app.

Right here’s a easy check which verifies that while you faucet on a product button, the product display is displayed with the right content material.

@RunWith(AndroidJUnit4::class)
class NavigationTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()

@Take a look at
enjoyable onHomeScreen_whenProductIsTapped_thenProductScreenIsDisplayed() {
composeTestRule.apply {
onNodeWithText("View particulars about ABC").performClick()
onNodeWithText("Product particulars for ABC").assertExists()
}
}
}

Basically, you aren’t interacting together with your navigation graph straight — as an alternative, you might be simulating consumer enter as a way to assert that your navigation routes result in the right content material.

🐞Debugging tip: Should you ever need to pause an instrumented check however nonetheless work together with the app, you should utilize composeTestRule.waitUntil(timeoutMillis = 3_600_000, situation = { false }). Paste this right into a check proper earlier than a failure level, then poke round with the app to attempt to perceive why the check fails (you might have an hour — hopefully lengthy sufficient to determine it out!). The format inspector even works on the identical time. You too can simply wrap this in a single check if you wish to examine the app’s state with solely the check 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.

Should you’re already utilizing Jetpack Navigation and defining your navigation graph utilizing the Kotlin DSL, you’ll seemingly need to replace your present code. Let’s have a look at two standard migration use circumstances: string-based routes and prime 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) {
sort = 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")

Notice 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)

Exchange any situations of the string-based route used to create locations with the brand new sort, 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 exchange any situations of the string-based route when calling navigate:

navController.navigate(route = Product(id))

OK, we’re achieved! We’ve been in a position to take away the string constants and boilerplate code, and in addition launched sort 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 sort 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 means, 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 may have some type of navigation UI which is at all times displayed, permitting customers to navigate to completely different prime stage locations.

Materials 3 Navigation Rail

An important duty for this navigation UI is to show which prime stage vacation spot the consumer is presently on. That is normally achieved by iterating by means 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 right navigation UI relying on the obtainable window dimension.

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.Dwelling),
TopLevelRoute(route = SHOPPING_CART_ROUTE, icon = Icons.Default.ShoppingCart),
TopLevelRoute(route = ACCOUNT_ROUTE, icon = Icons.Default.AccountBox),
)

// Inside your major app format
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 sort protected APIs, you don’t outline your prime stage routes as strings, so you’ll be able to’t use string comparability. As an alternative, use the brand new hasRoute extension perform on NavDestination to examine whether or not a vacation spot has a particular route class.

@Serializable information object Dwelling
@Serializable information object ShoppingCart
@Serializable information object Account

information class TopLevelRoute<T : Any>(val route: T, val icon: ImageVector)

val TOP_LEVEL_ROUTES = listOf(
TopLevelRoute(route = Dwelling, icon = Icons.Default.Dwelling),
TopLevelRoute(route = ShoppingCart, icon = Icons.Default.ShoppingCart),
TopLevelRoute(route = Account, icon = Icons.Default.AccountCircle)
)

// Inside your major app format
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 courses and object locations

Can you see the issue with the next code?

@Serializable 
information class Product(val id: String)

NavHost(
navController = navController,
startDestination = Product
) { … }

It’s not instantly apparent however when you have been to run it, you’d see the next error:

kotlinx.serialization.SerializationException: Serializer for sophistication ‘Companion’ just isn't discovered.

It’s because Product just isn’t a legitimate 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 in search of the statically initialized Companion object of the Product class which isn’t outlined as serializable (the truth is, 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 sort is getting used for the route. Whenever you attempt to use the category title as an alternative of the category occasion, you’ll obtain a useful error message: “The route must be a vacation spot class occasion or vacation spot object.”. A comparable lint examine when utilizing popBackStack will likely 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 replica 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 beneficial to create duplicate locations in your navigation graph as a result of 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.

Presently, when you’ve got a route with a String argument and its worth is about to the string literal “null”, the app will crash when navigating to that vacation spot. This concern will likely be fastened in 2.8.1, due in a few weeks.

Within the meantime, when you’ve got unsanitized enter to a String route argument, carry out a examine for “null” first to keep away from the crash.

Don’t use giant objects as routes as it’s possible you’ll run into TransactionTooLargeException. When navigating, the route is saved to persist system-initiated course of dying and the saving mechanism is a binder transaction. Binder transactions have a 1MB buffer so giant objects can simply fill this buffer.

You possibly can keep away from utilizing giant objects for routes by storing information utilizing a storage mechanism designed for giant information, reminiscent of Room or DataStore. When inserting information, receive a novel reference, reminiscent of an ID discipline. You possibly can 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 sort protected navigation APIs. Right here’s a fast abstract of a very powerful capabilities.

  • Outline locations utilizing composable<T> (or navigation<T> for nested graphs)
  • Navigate to a vacation spot utilizing navigate(route = T) for object routes or navigate(route = T(…)) for sophistication occasion routes
  • Get hold of a route from a NavBackStackEntry or SavedStateHandle utilizing toRoute<T>
  • Verify whether or not a vacation spot was created utilizing a given route utilizing hasRoute(route = T::class)

We’d love to listen to your ideas on these APIs. Be at liberty to depart a remark, or when you’ve got any points please file a bug. You possibly can learn extra about how one can 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
Latest news
Related news

LEAVE A REPLY

Please enter your comment!
Please enter your name here