Successfully ran in two projects with over +1M installs, archived in favor of https://developer.android.com/guide/navigation/design/type-safety
A KSP library that helps you generate type safe code with minimal effort and provides out of the box solutions to bundle everything together and scale your app in easy manner.
- Typesafe navigation arguments (special thanks for parsing the arguments to compose destinations)
- Simple setup with minimal code
- ViewModel type safe arguments with injection support when using Hilt
- Screen "level" type safe arguments
- Callback arguments
- Bottom sheet support through Accompanist Material
- Nested navigation support out of the box
- Transitions enter/exit to and from screens
- Deep links
- Graph aggregator factory
- Custom navigator to open screens easily
- Global navigation
- Multi (out of the box) and single module support
Everything you can do with the Official Jetpack Compose Navigation but in a type safe way.
The library is available through JitPack.
- Add JitPack to your project's settings.gradle
dependencyResolutionManagement {
...
repositories {
...
maven { setUrl("https://jitpack.io") }
}
}
- Add the dependency in the build.gradle
[versions]
foSho = <version>
ksp = <version>
[libraries]
foSho-android = { module = "com.github.FunkyMuse.foSho:navigator-android", version.ref = "foSho" }
foSho-codegen = { module = "com.github.FunkyMuse.foSho:navigator-codegen", version.ref = "foSho" }
[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
Inside the project build.gradle make sure to add the KSP plugin
plugins {
...
alias(libs.plugins.ksp).apply(false)
}
Inside the :app
module
plugins {
....
alias(libs.plugins.ksp)
}
dependencies {
implementation(libs.foSho.android)
ksp(libs.foSho.codegen)
}
If you intend to use Hilt/Dagger/Anvil
with foSho
and want your generated ViewModel arguments injectable,
inside :app
module set foSho.injectViewModelArguments
to "true", default is "false"
ksp {
arg("foSho.injectViewModelArguments", "true")
}
Single module
Inside the :app
module make sure to add the KSP argument, the library is multi module by default.
ksp {
arg("foSho.singleModule", "true")
}
- Make magic happen
There are three things you need to write code for and it's pretty natural
- Graph
- Destination (Argument, Callback Argument)
- Content
All you need to do is click build or just run ./gradlew kspDebugKotlin
as a faster way to get the generated code.
Graph
: Starting point is a@Graph
, eachGraph
has astartingDestination
and otherdestinations
, also aGraph
can be arootGraph
(only one throughout your app).Destination
: every destination that's part ofstartingDestination
anddestinations
is marked with@Destination
and you implement one of the three UI typesScreen
,Dialog
for which you can control the properties andBottomSheet
for each one you can additionally control whether to generate view model arguments or nav stack entry arguments,val generateViewModelArguments: Boolean = false
,val generateScreenEntryArguments: Boolean = false
, you can annotate a destination with@Argument
and@CallbackArgument
in order to control to/from arguments.Content
: everytime you click build aDestination
implementation is generated for you, it's your responsibility to implement it and annotate that object with a@Content
In code this will look like this
@Graph(
startingDestination = Home::class,
destinations = [HomeDetails::class],
rootGraph = true
)
internal object HomeGraph
@Destination
@Argument(
name = "hideBottomNav",
argumentType = ArgumentType.BOOLEAN,
defaultValue = DefaultBooleanValueFalse::class
)
internal object Home : Screen
@Destination(generateViewModelArguments = true, generateScreenEntryArguments = false)
@Argument(name = "cardId", argumentType = ArgumentType.INT)
@CallbackArgument(name = "clickedShare", argumentType = ArgumentType.BOOLEAN)
internal object HomeDetails : Screen {
override val deepLinksList: List<DeepLinkContract>
get() = listOf(
DeepLinkContract(
action = Intent.ACTION_VIEW,
uriPattern = "custom:url/{cardId}"
)
)
}
@Content
internal object HomeContent : HomeDestination {
@Composable
override fun AnimatedContentScope.Content() {
val argumentsFromHomeDetailsDestination = HomeDetailsCallbackArguments.rememberHomeDetailsCallbackArguments()
argumentsFromHomeDetailsDestination.OnSingleBooleanCallbackArgument(
key = HomeDetailsCallbackArguments.CLICKED_SHARE,
onValue = {
if (it == true){
//user has clicked share
}
}
)
HomeScreen(onClick = {
Navigator.navigateSingleTop(HomeDetailsDestination.openHomeDetails(cardId = 42))
})
}
}
@Content
internal object HomeDetailsContent : HomeDetailsDestination {
@Composable
override fun AnimatedContentScope.Content() {
}
}
A GraphFactory
is generated for you which you can use with an extension function addGraphs
to have ease of use like
addGraphs(
navigationGraphs = GraphFactory.graphs
)
you can checkout this line.
A Navigator
is there for you to collect the navigation events and also to send navigation events, for a single module setup, you can check out the sample.
Multi module
You would need to create one umbrella module, usually named "navigator" or however you see fit where you would write the code for the Graphs
//:navigator module
//UserAccountGraph.kt
@Graph(
startingDestination = UserAccountDetails::class,
destinations = [
EditAccountDetails::class,
DeleteAccount::class,
ChangePassword::class,
]
)
internal object UserAccountGraph
@Destination
internal object UserAccountDetails : Screen
@Destination
internal object ChangePassword : BottomSheet
@Destination
internal object DeleteAccount : Dialog
@Destination(generateScreenEntryArguments = true)
@Argument(name = "email", argumentType = ArgumentType.STRING)
@CallbackArgument(name = "isAccountUpdated", argumentType = ArgumentType.BOOLEAN)
internal object EditAccountDetails : Screen
//HomeGraph.kt
@Graph(startingDestination = Home::class, rootGraph = true)
internal object HomeGraph
@Destination
@Argument(name = "hideBottomNav", argumentType = ArgumentType.BOOLEAN, defaultValue = DefaultBooleanValueFalse::class)
internal object Home : Screen
Your :navigator
module acts as the only point where you have the navigation codegen code and nothing else, here you can control the arg
whether to generate injectable view model arguments through
ksp {
arg("foSho.injectViewModelArguments", "true")
}
Then inside your feature module
// :feature:user_details
@Content
internal object UserAccountDetailsContent : UserAccountDetailsDestination {
@Composable
override fun AnimatedContentScope.Content() {
}
}
and also make sure to add the :feature:user_details
in your :app
module so that it can be aggregated into the GraphFactory
.
Screen
has AnimatedContentScope as a receiverDialog
doesn't have any receiverBottomSheet
has a ColumnScope as a receiver
When using Kotlin version older than 1.8.0, you need to make sure the IDE looks at the generated folder. See KSP related issue.
How to do it depends on the AGP version you are using in this case:
Warning: In both cases, add this inside
android
block and replacingapplicationVariants
withlibraryVariants
if the module is not an application one (i.e, it uses'com.android.library'
plugin).
Since AGP (Android Gradle Plugin) version 7.4.0
- groovy - build.gradle(:module-name)
applicationVariants.all { variant ->
variant.addJavaSourceFoldersToModel(
new File(buildDir, "generated/ksp/${variant.name}/kotlin")
)
}
- kotlin - build.gradle.kts(:module-name)
applicationVariants.all {
addJavaSourceFoldersToModel(
File(buildDir, "generated/ksp/$name/kotlin")
)
}
For AGP (Android Gradle Plugin) version older than 7.4.0
- groovy - build.gradle(:module-name)
applicationVariants.all { variant ->
kotlin.sourceSets {
getByName(variant.name) {
kotlin.srcDir("build/generated/ksp/${variant.name}/kotlin")
}
}
}
- kotlin - build.gradle.kts(:module-name)
applicationVariants.all {
kotlin.sourceSets {
getByName(name) {
kotlin.srcDir("build/generated/ksp/$name/kotlin")
}
}
}
Support it by joining stargazers for this repository. 🌠
And follow me or check out my blog for my next creations! ⭐
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.