15.1 C
New York
Sunday, September 8, 2024

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


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

That is nice information in case you’re utilizing Jetpack Compose on 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 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 at the way to migrate current code and supplies some tips about testing navigation use circumstances.

The brand new kind secure navigation APIs for Kotlin permit you to use Any serializable kind to outline navigation locations. To make use of them, you’ll must add the Jetpack navigation library model 2.8.0 and the Kotlin serialization plugin to your venture.

As soon as accomplished, 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 properly with Fragments (with some slight variations). Should you’re utilizing each, we have now some new interop APIs for that too.

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 House

NavHost(navController, startDestination = House) {
composable {
HomeScreen()
}
}

Nomenclature is necessary right here. In navigation phrases, House 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 screen at that vacation spot, on this case HomeScreen.

These new APIs could be summarized as: Any technique that accepts a route now accepts a generic kind 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 by utilizing sorts for navigation arguments. For fundamental sorts, it’s tremendous easy to go them to a vacation spot.

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

Navigation graph with two locations: House and Product

We will outline the Product route utilizing a knowledge class which has a String id discipline which can 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 all the time have an id
  • The kind of id is all the time a String

You need to use any fundamental kind as a navigation argument, together with lists and arrays. For extra advanced 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 @Hold on the enum declaration to make sure that the enum class isn’t eliminated throughout minified builds, monitoring bug)

After we use this path to outline a vacation spot in our navigation graph, we will acquire the route from the again stack entry utilizing toRoute. This may then be handed to no matter is required to render that vacation spot on display screen, on this case ProductScreen. Right here’s how our vacation spot is applied:

composable { backStackEntry ->
val product : Product = backStackEntry.toRoute()
ProductScreen(product)
}

New since alpha: Should you’re utilizing a ViewModel to offer state to your display screen, you may as well acquire 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 relies on Android Bundle. This implies your ViewModel checks will must be instrumented (e.g. by utilizing Robolectric or by working them on an emulator). We’re taking a look at methods we will 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 a whole instance:

NavHost(
navController = navController,
startDestination = House
) {
composable {
HomeScreen(
onProductClick = { id ->
navController.navigate(route = Product(id))
}
)
}
composable { 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 outdoors.

Typically you wish to take customers on to a selected display screen inside your app, slightly than beginning on the dwelling display screen. For instance, in case 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 screen 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(
deepLinks = listOf(
navDeepLink(
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 provided basePath. Any fields from the provided 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 would 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 may launch the app instantly 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 all the time generated for route arguments, besides when:

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

2. the category discipline represents a group of primitive sorts, like a Listing or Array (full checklist 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 ?identify=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, in case 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 simply accept incoming hyperlinks, a straightforward option to take a look at 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?shade=crimson&variants=var1&variants=var2" com.instance.hellonavigation

🐞Debugging tip: Should you ever wish to test the generated deep hyperlink URL format, simply print the NavBackStackEntry.vacation spot.route out of your vacation spot and it’ll seem in logcat whenever you navigate to that vacation spot:

composable( … ) { 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 checks are normally instrumented checks which simulate the consumer navigating by means of your app.

Right here’s a easy take a look at which verifies that whenever you faucet on a product button, the product display screen is displayed with the proper content material.

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

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

Primarily, you aren’t interacting along with your navigation graph instantly — as a substitute, you might be simulating consumer enter with a view to assert that your navigation routes result in the proper content material.

🐞Debugging tip: Should you ever wish to pause an instrumented take a look at however nonetheless work together with the app, you should utilize 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 may have an hour — hopefully lengthy sufficient to determine it out!). The structure inspector even works on the similar time. It’s also possible to 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 significantly helpful when your instrumented app makes use of faux information which could trigger variations in habits out of your manufacturing construct.

Should you’re already utilizing Jetpack Navigation and defining your navigation graph utilizing the Kotlin DSL, you’ll probably wish to replace your current code. Let’s have a look at two standard migration use circumstances: string-based routes and high degree 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 manner.

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

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 cases of the string-based route used to create locations with the brand new kind, and take away any arguments:

composable { … }

When acquiring arguments, use toRoute to acquire the route object or class.

composable { backStackEntry ->
val product : Product = backStackEntry.toRoute()
ProductScreen(product.id)
}

Additionally exchange any cases of the string-based route when calling navigate:

navController.navigate(route = Product(id))

OK, we’re accomplished! We’ve been capable of take away the string constants and boilerplate code, and likewise launched kind security for navigation arguments.

Incremental migration

You don’t must migrate all of your string-based routes in a single go. You need to use 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 slightly 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 degree locations.

Materials 3 Navigation Rail

A vital accountability for this navigation UI is to show which high degree vacation spot the consumer is at the moment on. That is normally accomplished 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 proper navigation UI relying on the out there 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.House),
TopLevelRoute(route = SHOPPING_CART_ROUTE, icon = Icons.Default.ShoppingCart),
TopLevelRoute(route = ACCOUNT_ROUTE, icon = Icons.Default.AccountBox),
)

// Inside your major 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 secure APIs, you don’t outline your high degree routes as strings, so you possibly can’t use string comparability. As a substitute, use the brand new hasRoute extension perform on NavDestination to test whether or not a vacation spot has a selected route class.

@Serializable information object House
@Serializable information object ShoppingCart
@Serializable information object Account

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

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

// Inside your major 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 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 in case you have been to run it, you’d see the next error:

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

It’s because Product 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 (in reality, 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 test was added to identify locations the place an incorrect kind is getting used for the route. If 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 comparable lint test when utilizing popBackStack can be added within the 2.8.1 launch.

Utilizing duplicate locations used to lead to undefined habits. 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 advisable to create duplicate locations in your navigation graph as a result of ambiguity it creates when navigating to a kind 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, if in case you 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 subject can be fastened in 2.8.1, due in a few weeks.

Within the meantime, if in case you have unsanitized enter to a String route argument, carry out a test 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, resembling Room or DataStore. When inserting information, acquire a novel reference, resembling 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 secure navigation APIs. Right here’s a fast abstract of crucial capabilities.

  • Outline locations utilizing composable (or navigation for nested graphs)
  • Navigate to a vacation spot utilizing navigate(route = T) for object routes or navigate(route = T(…)) for sophistication occasion routes
  • Receive a route from a NavBackStackEntry or SavedStateHandle utilizing toRoute
  • Examine 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 happy to depart a remark, or if in case you have any points please file a bug. You may learn extra about the 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

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles