KSP and Compose Navigation

Ali Shobeyri
Daresay
Published in
6 min readJun 17, 2024

One day I was sitting behind my desk and I was developing a system for compose navigation that I wanted to use inside my client project, I never liked the system we were using for compose navigation and always compared it to what we had in fragment navigation, an easy to use system with generated code for arguments and optional arguments and etc. then a lamp lighted up above my head that why can’t I generate code for it by myself ???

Purpose

what we’ll achieve in this paper is how we can have a system in Android that can generate code for later usage

Notice: the Github Link for this article => https://github.com/sasssass/ksp_compose_navigation/tree/master

KSP

For this matter we have different tools, as an Android developer you probably have heard about kapt, kapt stands for Kotlin Annotation Processing Tool, with kapt you’re enabled to use the power of Java Annotation Processor in your Kotlin code, and via that you can generate code in the compile time, many different plugin such as Hilt, Room and etc use kapt to generated these code for us so we can use them inside our code.

So what is KSP here? KSP stands for Kotlin Symbol Processor, KSP is a newer tool over kapt that is designed to work without Java!

KSP has advantages over kapt, it has better performance on build time since it doesn’t require to interact with Java, so if you have a pure Kotlin project, you can choose KSP over kapt

But what is annotation processing? Annotation processing in Java and Kotlin is a feature that allows you to do some compile-time tasks (such as generating code)

Annotation is a marker that you can apply in your class, functions and etc :

When you have your annotation you can process it via the processor, then you can generate codes, XML files, and so on, also you can do some other tasks like validation of the code that has annotation and etc.

schema of what will happen in your gradle

Combine it with Compose Navigation

Now it’s the time for actual code, firstly you need these plugins and dependencies :

Also, you need to create two Kotlin based modules (or Android depending on what you’re going to do)

You must add kotlinpoet and symbol-processing and annotation in the processor module as dependencies and in your app you need to add annotation and processor :

processor gradle :

plugins {
alias(libs.plugins.ksp)
}

dependencies {
implementation(project(":code_generator:annotation"))
implementation(libs.symbol.processing)
implementation(libs.kotlinpoet)
}

app gradle :

plugins {
alias(libs.plugins.ksp)
}

implementation(project(path = ":code_generator:annotation"))
ksp(project(path = ":code_generator:processor"))

also don’t forget to apply KSP plugin in your project gradle :

plugins {
alias(libs.plugins.ksp) apply false
}

I want to create an annotation that I can add over my composable function to create a graph, for this matter, I need three parameters :

I. Route II. Arguments III. Optional arguments

You’ll have this file in your annotation module, let’s deep into it, here I defined an annotation with three parameters as I said, I also defined a Target for my annotation, as you can see this annotation is used to apply functions (you can also have annotation for classes, properties and …)

Also, I defined retention, which is used for controlling the lifecycle of annotations and how they can be stored :

Now we need to define our processor in our processor module, we’ll have two classes, NodeProcessorProvider and NodeProcessor, when KSP runs, the provider class will look for creating an instance of processor class when it’s needed.

In this example NodeProcessor has two inputs, a logger and a code generator, both of these inputs can be extracted from the environment, environment has the necessary contexts and utils that can fill the requirements of the processer.

If we go step by step on this code you can see first I searched for my symbols with specific annotation (NavigationNode), then I iterated all of the symbols I found and checked if they were declared in function (because NavigationNode is based on function as I defined it) and if they are valid or not, if these conditions are true we can extract annotation from symbols and then we can extract route, args, and optionalArgs from the annotation.

In the end, we’ll return all the symbols that can’t be processed.

You can also see I used logger, logger has other mods like warn and … that you can use depending on your needs.

Now we have all the parameters that we need and it’s time for generating code!

We need to create a new File in our code, you can add this function to NodeProcessor and call it from process (replace it with the commented part)

First, we need a proper name for our generated code which is the concatenation of functionName (the name of the function that you add an annotation above it) and “GraphNode”, we also need packageName to save generated code, now it’s time to user kotlinpoet library which helps us to generate .kt files, FileSpec helps you to create the file you need to generate and TypeSpec helps you to add properties, functions and … to your generated file (also we have dependencies but for this example, this file doesn’t depend on other files), now you can use codeGenetrator (that you got it from environment already) and combine it with FileSpec to generate your code.

Let’s talk more about TypeSepc because, in this new file, we need some properties and functions for our compose navigation.

Here’s an example of how you can add a new property to your type, for this matter you need PropertySpec, you can add the name (“rawRoute”), the type of the property (String), and a modifier (in this case it’s Public).

Now let’s see an example for functions :

Here is a dummy function that has an input as String and output as String and some code, the comment below the function is our final output, you can also add comment (addComment) or other things in your FunSepc, I’ll suggest you to play around with it to see how powerful this library (kotlinpoet) is and how much flexibility it has for generting code .

When your FunSepc is ready you can simply add it to your typeBuilder :

typeBuilder.addFunction(myFunction) // myFunction as FunSepc

If you check out this link, you can see the full implementation of processer to see how I created my own navigation class inside it, now it’s time for Rebuild the Project and you’ll have this beautiful generated file :

In my example I used the node like this :

You can see generated objects like ScreenWithArgumentGraphNode and etc here, if you add NavigationNode annotation to any function it would generate this helper class for you to help you through navigation :

ez pz lemon squeezy, now you have a powerful tools for compose navigation like the way you had it with fragment navigation !

Once again, check the Github link to see explore the way I generated my own helper classes, and then you can modify.

--

--