1. Introduction

In this article, we’re going to have a look at KotlinPoet. We’ll see what it is, what we can do with it, and how to use it.

2. What is KotlinPoet?

KotlinPoet is an open-source library that generates Kotlin source code. Square maintains it, which is the Kotlin equivalent of their JavaPoet library.

Unlike tools such as Asm, KotlinPoet generates raw, uncompiled source code. This would need to be compiled before it can be directly executed. However, depending on our needs, this can be more useful and powerful—for example, for code generation during a build process or within an IDE.

KotlinPoet generates most of the major structures in Kotlin programs—classes, fields, methods, annotations, etc. As such, we can programmatically generate anything from single lines of code to entire source files.

3. Dependencies

Before we can use KotlinPoet, we need to include the latest version in our build, which is 1.16.0 at the time of writing.

If we’re using Maven, we can include this dependency:

<dependency>
    <groupId>com.squareup</groupId>
    <artifactId>kotlinpoet-jvm</artifactId>
    <version>1.16.0</version>
</dependency>

Or if we’re using Gradle, we can include it like this:

implementation("com.squareup:kotlinpoet:1.16.0")

At this point, we’re ready to start using it in our application.

4. Generating Code

Now that we’ve got KotlinPoet set up, we can use it to generate code.

All code is generated using one of a set of Spec classes. One of these is used for each major category of code that we can generate—for example, TypeSpec is used for generating type definitions, FunSpec is used for generating function definitions, and many others.

Each of these Spec classes has a companion object that exposes functions to start generation, and then we chain calls from that point on.

For example, we can generate an empty class as follows:

val code = TypeSpec.classBuilder("Test").build()

The output of this is then a TypeSpec instance that represents our class, and we can use the toString() method on it to generate the resulting code:

public class Test

5. Generating Functions

The first unit of code we’ll generate with KotlinPoet is the function. This includes everything we’d expect to see in a function—the function itself, parameters, return type, and the actual body of the function.

We start out with the FunSpec class. We need to decide the type of function that we wish to build – regular, constructor, getter, setter, or whether we’re overriding another function – and then call the appropriate builder function:

FunSpec.builder("simple") // public fun simple() {}
FunSpec.constructorBuilder() // public constructor()
FunSpec.getterBuilder() // public get() {}
FunSpec.setterBuilder() // public set() {}

5.1. Specifying the Function Body

Once we have a function definition, we must give it a body. KotlinPoet doesn’t model function bodies but instead lets us specify them freely.

The simplest way to achieve this is by adding individual statements – using the addStatement() call:

val code = FunSpec.builder("test")
  .addStatement("println(\"Testing\")")
  .build()

Doing this will then generate the code:

public fun test() {
    println("Testing")
}

When doing this, we can see that we’ve had to escape the quotes around our string. KotlinPoet gives us tools to make this easier by allowing the code to be specified as format strings that take arguments. For example, we could have written the above as:

val code = FunSpec.builder("test")
  .addStatement("println(%S)", "Testing")
  .build()

Which will produce the same code as before. The advantage here is that KotlinPoet will correctly format our input; however, it needs to – in this case, surround it with quotes. We have a few options for format specifiers that we can use here:

  • %S – format the argument as a string, escaping quotes and dollar signs and surrounding everything in quotes.
  • %P – format the argument as a string template, escaping quotes but not dollar signs and surrounding the whole thing in quotes.
  • %L – treat the argument as a literal value without escaping or quoting.
  • %N – treat the argument as another code element and insert its name.
  • %T – treat the argument as a type, inserting the type name and ensuring that imports are added as appropriate.
  • %M – treat the argument as a member of a package or class, inserting the properly qualified member name and ensuring imports are added as appropriate.

5.2. Control Flow

Certain aspects of the function body are special and need to be treated differently. In particular, this is true of any of the standard control flow mechanisms that themselves enclose more statements.

KotlinPoet gives us the beginControlFlow() and endControlFlow() calls to manage this. beginControlFlow() outputs some statement and then an opening brace, whereas endControlFlow() outputs the closing brace.

KotlinPoet will also indent any statements included between these:

val code = FunSpec.builder("test")
  .beginControlFlow("if (showOutput)")
  .addStatement("println(%S)", "Testing")
  .endControlFlow()
  .build()

This will produce the following:

public fun test() {
    if (showOutput) {
        println("Testing")
    }
}

We also have nextControlFlow() to allow for structures such as else and else if. This is a combination of both endControlFlow() and beginControlFlow() on the same line.

5.3. Parameters

Functions with no parameters are of limited use, so we need a way to specify them. We can do this with the addParameter() call, to which we need to provide a ParameterSpec.

At the very least, a ParameterSpec needs to have a name and a type:

val code = FunSpec.builder("test")
  .addParameter(ParameterSpec.builder("param", String::class).build())
  .build()

This will produce:

public fun test(param: kotlin.String) {
}

We can also provide a default value for our parameters using the defaultValue() call on our ParameterSpec:

val code = FunSpec.builder("test")
  .addParameter(ParameterSpec.builder("param", Int::class).defaultValue("%L", 42).build())
  .build()

As we saw earlier, the default value is provided as a format specifier.

KotlinPoet also provides a shorthand for adding parameters by just passing the parameter name and type directly to the addParameter() call:

val code = FunSpec.builder("test")
  .addParameter("param", String::class)
  .build()

However, doing this means we can’t customize the parameter extra.

5.4. Return Type

Similar to function parameters, we can also specify the return type. Unlike other settings, a function can only have a single return type that defaults to Unit. This is done using the returns() call:

val code = FunSpec.builder("test")
  .returns(String::class)
  .build()

Which will produce:

public fun test(): kotlin.String {
}

In addition to this, KotlinPoet has a unique understanding of single-expression functions. If our function specification has a single statement that starts with a return, then we’ll generate this differently:

val code = FunSpec.builder("test")
  .returns(Int::class)
  .addStatement("return 5")
  .build()

will generate:

public fun test(): kotlin.Int = 5

The return type is provided in this case because our function specification included it. Kotlin allows us to omit it according to our specifications in these cases while still producing valid code.

6. Generating Types

Now that we can generate functions, we need some types to work with them. This can include types that are used within our functions or that the functions exist within.

KotlinPoet allows us to create all of the expected type definitions that Kotlin supports—classes, interfaces, objects, and so on.

We can create types using the TypeSpec specification class:

TypeSpec.classBuilder("Test") // public class Test
TypeSpec.interfaceBuilder("Test") // public interface Test
TypeSpec.objectBuilder("Test") // public object Test
TypeSpec.enumBuilder("Test") // public enum class Test

6.1. Adding Methods

Once we can create our types, it’s helpful to be able to give them some functionality. The first way to do this is to be able to add methods.

We’ve already seen how to create functions using FunSpec. We can add these to classes by using the addFunction() call on the TypeSpec instance:

val code = TypeSpec.classBuilder("Test")
  .addFunction(FunSpec.builder("doSomething")
    .returns(Int::class)
    .addParameter("input", String::class)
    .build())
  .build()

Doing this will then produce the following:

public class Test {
    public fun doSomething(input: kotlin.String): kotlin.Int {
    }
}

We can generate our functions using FunSpec’s full capabilities, and they’ll be correctly added to the class.

6.2. Specifying Modifiers

Once we can add things to our types, we’ll need to be able to specify modifiers on them. This includes access modifiers as well as static, final, and more.

These modifiers are specified on the entry added to the type – such as the new method – using the addModifiers() call. If we don’t add any, there will be a default set – the entry will be public, and no other modifiers will be specified.

For example, to add a protected abstract method to our class, we’d do:

.addFunction(FunSpec.builder("doSomething")
  .addModifiers(KModifier.PROTECTED, KModifier.ABSTRACT)
  .build())

Note that there are certain nonsense combinations that KotlinPoet will still generate code for – for example, we can specify multiple access modifiers simultaneously, or we can specify both abstract and final simultaneously.

However, KotlinPoet does guard against other invalid combinations. For example, we can’t add an abstract method to a class that isn’t also abstract.

6.3. Adding Properties

Now that we can add methods to our types, we also want to be able to add properties. We add these by using the addProperty() call and providing a PropertySpec instance:

val code = TypeSpec.classBuilder("Test")
  .addProperty(PropertySpec.builder("test", String::class)
    .build())
  .build()

This will then produce the following code:

public class Test {
    public val test: kotlin.String
}

We can then use calls on the PropertySpec instance to further customise the field, exactly as we’ve seen with methods. This includes setting modifiers on the property, exactly as we saw on functions.

We can also mark the property as mutable – which will cause it to generate using var instead of val – and give it an initial value:

.addProperty(PropertySpec.builder("test", String::class)
  .addModifiers(KModifier.PRIVATE)
    .mutable()
    .initializer("%S", "Hello")
    .build())

This will produce:

private var test: kotlin.String = "Hello"

As with function parameters, we also have a simplified version that we can use to add properties to types by specifying only the name and the type:

.addProperty("test", String::class)

7. Generating Files

The final thing we’re going to explore is generating entire source files. These can contain anything that we’d expect to see in a Kotlin source file—types, properties, and top-level functions.

We can generate files using the FileSpec builder. This starts out needing a package name and the filename that the file would represent:

val code = FileSpec.builder("com.baeldung.kotlin.kotlinpoet", "Testing.kt").build()

This starts by just giving us a file with a package statement:

package com.baeldung.kotlin.kotlinpoet

We can then use addType(), addProperty(), and addFunction() to add content to this file:

val code = FileSpec.builder("com.baeldung.kotlin.kotlinpoet", "Testing.kt")
  .addType(TypeSpec.classBuilder("Testing")
    .addFunction(FunSpec.builder("count")
      .returns(Int::class)
      .addParameter(ParameterSpec.builder("items", List::class).build())
      .addStatement("return items.size()")
       .build())
    .build())
  .build()

Will give us:

package com.baeldung.kotlin.kotlinpoet

import kotlin.Int
import kotlin.collections.List

public class Testing {
    public fun count(items: List): Int = items.size()
}

We’ll notice that KotlinPoet has automatically added import statements and no longer fully qualifies those types in the generated code.

8. Conclusion

Here’s a quick introduction to KotlinPoet. This library can do much more, so why not try it out and see?

All of the examples are available over on GitHub.

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments