Compose Destinations
A KSP library to use alongside compose navigation. It reduces boilerplate code and is less error-prone since passing arguments between screens is type-safe. You won't need to update multiple source files every time you add or remove a screen composable, the navigation graph will be updated automatically.
Table of contents
Usage
- Start by annotating the
Composable
functions that you want to add to the navigation graph with@Destination
.
@Destination(route = "home", start = true)
@Composable
fun HomeScreen(
navController: NavController
) {
//...
}
NOTE: You can use DestinationsNavigator
instead of NavController
to make these Composables testable and "previewable". Read more in Going deeper
-
Build the project (f.e: Build > Make Project) to make KSP generate
HomeScreenDestination
andDestinations
files. Each@Destination
will generate a Destination object, so do this everytime you need to access new Destination files. -
Replace your
NavHost
call withDestinations.NavHost
(or if using aScaffold
, then replace it withDestinations.Scaffold
). You can also remove the builder blocks, you won't be needing them anymore.
Destinations.NavHost(
navController = myNavController
)
OR
Destinations.Scaffold(
scaffoldState = myScaffoldState
)
- If the destination has arguments, then simply add them to the
Composable
function!
@Destination(route = "user")
@Composable
fun UserScreen(
userId: Long
)
- Then, to navigate to the User Screen, anywhere you have the
NavController
(orDestinationsNavigator
).
navController.navigate(UserScreenDestination.withArgs(userId = 1))
That's it! No messing with NavType
, weird routes, bundles and strings. All this will be taken care for you.
- Oh and by the way, what if the destination has default arguments? Wouldn't it be nice if we could just use Kotlin default parameters feature? Well, that is exactly how we do it:
@Destination(route = "user")
@Composable
fun UserScreen(
userId: Long,
isOwnUser: Boolean = false
)
Now the IDE will even tell you the default arguments of the composable when calling the withArgs
method!
Notes about arguments:
- They must be one of
String
,Boolean
,Int
,Float
,Long
to be considered navigation arguments.NavController
,DestinationsNavigator
,NavBackStackEntry
orScaffoldState
(only if you are usingScaffold
) can also be used by all destinations. - Navigation arguments' default values must be resolvable from the generated
Destination
class since the code written after the "=
" will be copied into it as is. Unfortunately, this means you won't be able to use a constant or a function call as the default value of a nav argument. However, if the parameter type is not a navigation argument type, then everything is valid since it won't be considered a navigation argument of the destination. For example:
@Destination(route = "greeting", start = true)
@Composable
fun Greeting(
navigator: DestinationsNavigator,
coroutineScope: CoroutineScope = rememberCoroutineScope() //valid because CoroutineScope is not a navigation argument type
)
@Destination(route = "user")
@Composable
fun UserScreen(
navigator: DestinationsNavigator,
id: Int = getDefaultUserId() //not valid because Int is a navigation argument type so we need to resolve the default value in the generated classes
)
//As a temporary workaround, you could define the argument as nullable (or lets say -1)
@Destination(route = "user")
@Composable
fun UserScreen(
navigator: DestinationsNavigator,
id: Int? = null
) {
//then here do:
val actualId = id ?: getDefaultUserId()
}
We'll be looking for ways to improve this.
Deep Links
You can define deeps links to a destination like this:
@Destination(
route = "user",
deepLinks = [
DeepLink(
uriPattern = "https://myapp.com/user/{id}"
)
]
)
@Composable
fun UserScreen(
navigator: DestinationsNavigator,
id: Int
)
You can also use the placeholder suffix FULL_ROUTE_PLACEHOLDER
in your uriPattern
. In the code generation process it will be replaced with the full route of the destination which contains all the destination arguments. So, for example, this would result in the same uriPattern
as the above example:
@Destination(
route = "user",
deepLinks = [
DeepLink(
uriPattern = "https://myapp.com/$FULL_ROUTE_PLACEHOLDER"
)
]
)
@Composable
fun UserScreen(
navigator: DestinationsNavigator,
id: Int
)
Setup
Compose destinations is available via maven central.
- Add the ksp plugin:
plugins {
//...
id("com.google.devtools.ksp") version "1.5.21-1.0.0-beta07" // This will change to the stable ksp version when compose allows us to use kotlin 1.5.30
}
- Add the dependencies:
implementation 'io.github.raamcosta.compose-destinations:core:0.7.0-alpha04'
ksp 'io.github.raamcosta.compose-destinations:ksp:0.7.0-alpha04'
- And finally, you need to make sure the IDE looks at the generated folder. See KSP related issue. An example for the debug variant would be:
sourceSets {
//...
main {
java.srcDir(file("build/generated/ksp/debug/kotlin"))
}
}
Going deeper
- It is good practice to not depend directly on
NavController
on your Composeables. You can choose to depend onDestinationsNavigator
instead ofNavController
, which is an interface wrapper ofNavController
that allows to easily pass an empty implementation (one is available alreadyEmptyDestinationsNavigator
) for previews or testing. All above examples can replacenavController: NavController
withnavigator: DestinationsNavigator
, in order to make use of this dependency inversion principle. - All annotated composables will generate an implementation of
Destination
which is a sealed interface that contains the full route, navigation arguments,Content
composable function and thewithArgs
implementation. Destination
annotation can receive anavGraph
parameter for nested navigation graphs. This will be the route of the nested graph and all destinations with the samenavGraph
will belong to it. If this parameter is not specified, then theDestination
will belong to the root navigation graph (which is the norm when not using nested nav graphs)Scaffold
composable lambda parameters will be given a currentDestination
. This makes it trivial to have top bar, bottom bar and drawer depend on the current destination.- Besides the
NavHost
andScaffold
wrappers, the generatedDestinations
class contains allNavGraphs
. EachNavGraph
contains the startDestination
as well as all its destinations and its nestedNavGraphs
. - If you would like to have additional properties/functions in the
Destination
(for example a "title" which will be shown to the user for each screen) you can make an extension property/function ofDestination
for a similar effect. Since it is a sealed interface, awhen
expression will make sure you always have a definition for each screen (check this file for an example).
Current state
This lib is still in its alpha stage, APIs can change. I'm looking for all kinds of feedback, issues, feature requests and help in improving the code. So please, if you find this interesting, try it out in some sample projects and let me know how it goes!
License
Copyright 2021 Rafael Costa
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.