This project demonstrates how to use Kotlin Symbol Processing (KSP) with Amper build system to generate type-safe navigation boilerplate code for Compose Desktop applications. It showcases a modular architecture with reusable module templates and code generation techniques.
This project is not production ready or meant to be used in an actual project, it's more of a proof of concept.
- KSP Processors in Amper: Using Kotlin Symbol Processing to generate code in an Amper-based project
- Module Templates: Creating reusable module templates for consistent feature implementation
- Compose Navigation Boilerplate Generation: Automating the creation of type-safe navigation code
The project implements a type-safe navigation system using KSP to generate the boilerplate code. This provides:
- Compile-time safety for navigation routes
- Automatic parameter passing between screens
- Centralized navigation destination registration
- Parameters can be ignored in the declared keys
Two main KSP processors are used:
- DestinationProcessor: Processes
@Destination
annotations to generate navigation code for individual screens - NavBuilderProcessor: Aggregates all generated navigation destinations into a single function
The project uses Amper module templates to standardize feature implementation:
feature.module-template.yaml
: Defines dependencies, settings, and KSP processors for feature modules- Ensures consistent configuration across all feature modules
- Simplifies the creation of new features
- app: Main application module with the entry point
- navigation:
- core: Core navigation interfaces and annotations
- processor: KSP processor for generating navigation code
- aggregator: KSP processor for aggregating all navigation destinations
- feature: Feature modules (home, profile, settings)
- components: Reusable UI components
- template: Module templates for creating new features
- external: A feature module placed outside
/feature
to test KSP processors
- Feature screens are annotated with
@Destination
- KSP processes these annotations during compilation
- Type-safe navigation code is generated for each destination, for each module
- All destinations for all modules are aggregated into a single function (
allDestinations
) - The application uses this generated code for navigation
// Define a destination key
@Serializable
data object HomeKey : DestinationKey
// Annotate a composable as a destination
@Destination(HomeKey::class)
@Composable
fun HomeScreen() {
// Screen implementation
}
@Serializable
data class ProfileKey(
val id: String,
@Destination.Ignore
val optional: Boolean = false,
) : DestinationKey
// Annotate a composable as a destination
@Destination(ProfileKey::class)
@Composable
fun ProfileScreen(id: String) { // must match name in key
// Screen implementation
}
A generated allDestinations()
function will be generated which can then be used in the NavHost
Composable:
@Composable
fun App() {
val navController: NavHostController = rememberNavController()
NavHost(
navController = navController,
startDestination = HomeKey,
builder = { allDestinations() },
)
}
Alternatively, each module that has the KSP processor applied will also generate a ${moduleName}Destinations
@Composable
fun App() {
val navController: NavHostController = rememberNavController()
NavHost(
navController = navController,
startDestination = HomeKey,
builder = {
homeDestinations()
profileDestinations()
},
)
}
- Clone the repository
- Run the application using Amper:
./amper run
- Instead of feature modules depending on each other to navigate through module.yaml, screens should have a lambda param (For ex:
onNavigateToProfile: () -> Unit
), the processor would add the same parameter to the generated${moduleName}Destinations
so the linking can happen inapp
module, similar to recommended compose navigation usage.