Moskala M. Advanced Kotlin 2023
Moskala M. Advanced Kotlin 2023
Marcin Moskała
This book is for sale at http://leanpub.com/advanced_kotlin
Introduction 1
Who is this book for? 1
That will be covered? 1
The structure of the book 3
The Kotlin for Developers series 3
Conventions 4
Code conventions 4
Acknowledgments 6
Generic variance modifiers 8
List variance 10
Consumer variance 12
Function types 15
The Covariant Nothing Object 17
The Covariant Nothing Class 23
Variance modifier limitations 25
UnsafeVariance annotation 31
Variance modifier positions 33
Star projection 35
Summary 35
Interface delegation 38
The delegation pattern 38
Delegation and inheritance 39
Kotlin interface delegation support 41
Wrapper classes 45
The decorator pattern 46
Intersection types 49
Limitations 51
Conflicting elements from parents 52
Summary 53
CONTENTS
Property delegation 54
How property delegation works 55
Other getValue and setValue parameters 59
Implementing a custom property delegate 62
Provide a delegate 65
Property delegates in Kotlin stdlib 68
The notNull delegate 68
The lazy delegate 70
The observable delegate 82
The vetoable delegate 86
A map as a delegate 88
Review of how variables work 91
Summary 98
Kotlin Contracts 99
The meaning of a contract 100
How many times do we invoke a function from an
argument? 101
Implications of the fact that a function has re-
turned a value 105
Using contracts in practice 107
Summary 108
Java interoperability 109
Nullable types 109
Kotlin type mapping 112
JVM primitives 113
Collection types 115
Annotation targets 119
Static elements 125
JvmField 127
Using Java accessors in Kotlin 129
JvmName 130
JvmMultifileClass 134
JvmOverloads 135
Unit 138
Function types and function interfaces 139
Tricky names 142
Throws 143
JvmRecord 146
Summary 147
CONTENTS
Introduction
The chapter titles explain what will be covered quite well, but
here is a more detailed list:
• Kotlin Contracts
• Kotlin and Java type mapping
• Annotations for Kotlin and Java interoperability
• Multiplatform development structure, concepts and
possibilities
• Implementing multiplatform libraries
• Implementing Android and iOS applications with
shared modules
• Essentials of Kotlin/JS
• Reflecting Kotlin elements
• Reflecting Kotlin types
• Implementing custom Annotation Processors
• Implementing custom Kotlin Symbol Processors
• KSP incremental compilation and multiple-round pro-
cessing
• Defining Compiler Plugins
• Core Static Analysis concepts
• Overview of Kotlin static analyzers
• Defining custom Detekt rules
*
Introduction 4
Conventions
Code conventions
import kotlin.reflect.KType
import kotlin.reflect.typeOf
fun main() {
val t1: KType = typeOf<Int?>()
println(t1) // kotlin.Int?
val t2: KType = typeOf<List<Int?>>()
println(t2) // kotlin.collections.List<kotlin.Int?>
val t3: KType = typeOf<() -> Map<Int, Char?>>()
println(t3)
// () -> kotlin.collections.Map<kotlin.Int, kotlin.Char?>
}
class A {
val b by lazy { B() }
val c by lazy { C() }
val d by lazy { D() }
// ...
}
Introduction 6
Acknowledgments
Let’s say that Puppy is a subtype of Dog, and you have a generic
Box class to enclose them both. The question is: what is the
relation between the Box<Puppy> and Box<Dog> types? In other
words, can we use Box<Puppy> where Box<Dog> is expected, or
vice versa? To answer these questions, we need to know what
the variance modifier of this class type parameter is¹.
When a type parameter has no variance modifier (no out or in
modifier), we say it is invariant and thus expects an exact type.
So, if we have class Box<T>, then there is no relation between
Box<Puppy> and Box<Dog>.
class Box<T>
open class Dog
class Puppy : Dog()
fun main() {
val d: Dog = Puppy() // Puppy is a subtype of Dog
fun main() {
val d: Dog = Puppy() // Puppy is a subtype of Dog
fun main() {
val d: Dog = Puppy() // Puppy is a subtype of Dog
List variance
Let’s consider that you have the type Animal and its subclass
Cat. You also have the standalone function petAnimals, which
you use to pet all your animals when you get back home. You
also have a list of cats that is of type List<Cat>. The question
is: can you use your list of cats as an argument to the function
petAnimals, which expects a list of animals?
interface Animal {
fun pet()
}
}
}
}
}
fun main() {
val cats: List<Cat> =
listOf(Cat("Mruczek"), Cat("Puszek"))
petAnimals(cats) // Can I do that?
}
interface Animal
class Cat(val name: String) : Animal
class Dog(val name: String) : Animal
fun main() {
val cats: MutableList<Cat> =
mutableListOf(Cat("Mruczek"), Cat("Puszek"))
addAnimal(cats) // COMPILATION ERROR
val cat: Cat = cats.last()
// If code would compile, it would break here
}
Consumer variance
Let’s say that you have a class that can be used to send mes-
sages of a certain type.
Generic variance modifiers 13
interface Message
class GeneralSender(
serviceUrl: String
) : Sender<Message> {
private val connection = makeConnection(serviceUrl)
fun main() {
val numberConsumer: Consumer<Number> = Consumer()
numberConsumer.consume(2.71) // Consuming 2.71
val intConsumer: Consumer<Int> = numberConsumer
intConsumer.consume(42) // Consuming 42
val floatConsumer: Consumer<Float> = numberConsumer
floatConsumer.consume(3.14F) // Consuming 3.14
Function types
fun main() {
val strs = Node("A", Node("B", Empty()))
val ints = Node(1, Node(2, Empty()))
val empty: LinkedList<Char> = Empty()
}
fun main() {
val strs = Node("A", Node("B", Empty))
val ints = Node(1, Node(2, Empty))
val empty: LinkedList<Char> = Empty
}
Every empty list created with the listOf or emptyList functions from Kotlin stdlib
is actually the same object.
fun main() {
val empty: List<Nothing> = emptyList()
val strs: List<String> = empty
val ints: List<Int> = empty
class Schedule<T>(
val task: Task<T>
):TaskSchedulerMessage<T>
Generic variance modifiers 21
class Update<T>(
val taskUpdate: TaskUpdate<T>
) : TaskSchedulerMessage<T>
class Delete(
val taskId: String
) : TaskSchedulerMessage<Nothing>
class Task<T>(
val id: String,
val scheduleAt: Instant,
val data: T,
val priority: Int,
val maxRetries: Int? = null
)
class TaskUpdate<T>(
val id: String? = null,
val scheduleAt: Instant? = null,
val data: T? = null,
val priority: Int? = null,
val maxRetries: Int? = null
)
class TaskUpdate<T>(
val id: TaskPropertyUpdate<String> = Keep,
val scheduleAt: TaskPropertyUpdate<Instant> = Keep,
val data: TaskPropertyUpdate<T> = Keep,
val priority: TaskPropertyUpdate<Int> = Keep,
val maxRetries: TaskPropertyUpdate<Int?> = Keep
)
class TaskUpdate<T>(
val id: TaskPropertyUpdate<String> = Keep,
val scheduleAt: TaskPropertyUpdate<Instant> = Keep,
val data: TaskPropertyUpdate<T> = Keep,
val priority: TaskPropertyUpdate<Int> = Keep,
val maxRetries: TaskPropertyUpdate<Int?> = Keep
)
maxRetries = RestorePrevious,
priority = RestoreDefault,
)
class, which can be either Left or Right and must have two
type parameters that specify what data types it expects on the
Left and on the Right. However, both Left and Right should
each have only one type parameter to specify what type they
expect. To make this work, we need to fill the missing type
argument with Nothing.
Both Left and Right can be up-casted to Left and Right with
supertypes of the types of values they hold.
// Java
Integer[] numbers= {1, 4, 2, 3};
Object[] objects = numbers;
objects[2] = "B"; // Runtime error: ArrayStoreException
takeDog(Dog())
takeDog(Puppy())
takeDog(Hound())
fun put(value: T) {
this.value = value
Generic variance modifiers 27
}
}
fun main() {
val dogBox = Box<Dog>()
dogBox.put(Dog())
dogBox.put(Puppy())
dogBox.put(Hound())
This is actually the problem with Java arrays. They should not
be covariant because they have methods, like set, that allow
their modification.
Covariant type parameters can be safely used in private in-
positions.
fun main() {
val producer: Producer<Amphibious> =
Producer { Amphibious() }
val amphibious: Amphibious = producer.produce()
val boat: Boat = producer.produce()
val car: Car = producer.produce()
fun main() {
val carProducer = Producer<Amphibious> { Car() }
val amphibiousProducer: Producer<Amphibious> = carProducer
val amphibious = amphibiousProducer.produce()
// If not compilation error, we would have runtime error
UnsafeVariance annotation
interface Dog
interface Pet
data class Puppy(val name: String) : Dog, Pet
data class Wolf(val name: String) : Dog
data class Cat(val name: String) : Pet
fun main() {
val dogs = mutableListOf<Dog>(Wolf("Pluto"))
fillWithPuppies(dogs)
println(dogs)
// [Wolf(name=Pluto), Puppy(name=Jim), Puppy(name=Beam)]
Star projection
if (value is List<*>) {
...
}
out T Nothing T
in T T Any?
* Nothing Any?
Summary
Interface delegation
interface Creature {
val attackPower: Int
val defensePower: Int
fun attack()
}
class GenericCreature(
override val attackPower: Int,
override val defensePower: Int,
) : Creature {
override fun attack() {
println("Attacking with $attackPower")
}
}
// ...
}
fun main() {
val goblin = Goblin()
println(goblin.defensePower) // 1
goblin.attack() // Attacking with 2
}
interface Creature {
val attackPower: Int
val defensePower: Int
fun attack()
}
fun main() {
val goblin = Goblin()
println(goblin.defensePower) // 1
goblin.attack() // Attacking with 2
}
interface Creature {
val attackPower: Int
val defensePower: Int
fun attack()
}
class GenericCreature(
override val attackPower: Int,
override val defensePower: Int,
) : Creature {
override fun attack() {
println("Attacking with $attackPower")
}
}
fun main() {
val goblin = Goblin()
println(goblin.defensePower) // 1
goblin.attack() // Attacking with 2
}
Interface delegation 42
// or
class Goblin(
att: Int,
def: Int
) : Creature by GenericCreature(att, def)
// or
class Goblin(
creature: Creature
) : Creature by creature
// or
class Goblin(
val creature: Creature = GenericCreature(2, 1)
) : Creature by creature
// or
val creature = GenericCreature(2, 1)
Interface delegation 43
interface Creature {
val attackPower: Int
val defensePower: Int
fun attack()
}
class GenericCreature(
override val attackPower: Int,
override val defensePower: Int,
) : Creature {
override fun attack() {
println("Attacking with $attackPower")
}
}
fun main() {
val goblin = Goblin()
Interface delegation 44
println(goblin.defensePower) // 1
goblin.attack() // Special Goblin attack 2
}
interface Creature {
val attackPower: Int
val defensePower: Int
fun attack()
}
class GenericCreature(
override val attackPower: Int,
override val defensePower: Int,
) : Creature {
override fun attack() {
println("Attacking with $attackPower")
}
}
class Goblin(
private val creature: Creature = GenericCreature(2, 1)
) : Creature by creature {
override fun attack() {
println("It will be special Goblin attack!")
creature.attack()
}
}
fun main() {
val goblin = Goblin()
goblin.attack()
// It will be a special Goblin attack!
Interface delegation 45
// Attacking with 2
}
Wrapper classes
@Keep
@Immutable
data class ComposeImmutableList<T>(
val innerList: List<T>
) : List<T> by innerList
class StateFlow<T>(
source: StateFlow<T>,
private val scope: CoroutineScope
) : StateFlow<T> by source {
fun collect(onEach: (T) -> Unit) {
scope.launch {
collect { onEach(it) }
}
}
}
interface AdFilter {
fun showToPerson(user: User): Boolean
fun showOnPage(page: Page): Boolean
fun showOnArticle(article: Article): Boolean
}
class ShowOnPerson(
val authorKey: String,
val prevFilter: AdFilter = ShowAds
) : AdFilter {
override fun showToPerson(user: User): Boolean =
prevFilter.showToPerson(user)
class ShowToLoggedIn(
val prevFilter: AdFilter = ShowAds
) : AdFilter {
override fun showToPerson(user: User): Boolean =
user.isLoggedIn
Interface delegation 48
class Page
class Article(val authorKey: String)
class User(val isLoggedIn: Boolean)
interface AdFilter {
fun showToPerson(user: User): Boolean
fun showOnPage(page: Page): Boolean
fun showOnArticle(article: Article): Boolean
}
class ShowOnPerson(
val authorKey: String,
val prevFilter: AdFilter = ShowAds
) : AdFilter by prevFilter {
override fun showOnPage(page: Page) =
page is ProfilePage &&
Interface delegation 49
class ShowToLoggedIn(
val prevFilter: AdFilter = ShowAds
) : AdFilter by prevFilter {
override fun showToPerson(user: User): Boolean =
user.isLoggedIn
}
Intersection types
class IntegrationTestScope(
applicationTestBuilder: ApplicationTestBuilder,
val application: Application,
val backgroundScope: CoroutineScope,
) : TestApplicationBuilder(),
ClientProvider by applicationTestBuilder
Limitations
interface Creature {
fun attack()
}
fun walk() {}
}
Interface delegation 52
fun main() {
GenericCreature().attack() // GenericCreature attacks
Goblin().attack() // GenericCreature attacks
WildAnimal().attack() // WildAnimal attacks
Wolf().attack() // Wolf attacks
interface Attack {
val attack: Int
val defense: Int
}
interface Defense {
val defense: Int
}
class Dagger : Attack {
override val attack: Int = 1
override val defense: Int = -1
}
class LeatherArmour : Defense {
override val defense: Int = 2
}
class Goblin(
private val attackDelegate: Attack = Dagger(),
private val defenseDelegate: Defense = LeatherArmour(),
) : Attack by attackDelegate, Defense by defenseDelegate {
// We must override this property
override val defense: Int =
defenseDelegate.defense + attackDelegate.defense
}
Summary
Property delegation
// Data binding
private val port by bindConfiguration("port")
private val token: String by preferences.bind(TOKEN_KEY)
return field
}
set(value) {
println("attempts changed from $field to $value")
field = value
}
fun main() {
token = "AAA" // token changed from null to AAA
val res = token // token getter returned AAA
println(res) // AAA
attempts++
// attempts getter returned 0
// attempts changed from 0 to 1
}
import kotlin.reflect.KProperty
fun main() {
token = "AAA" // token changed from null to AAA
val res = token // token getter returned AAA
println(res) // AAA
attempts++
// attempts getter returned 0
// attempts changed from 0 to 1
}
// Code in Kotlin:
var token: String? by LoggingProperty(null)
// Kotlin code:
var token: String? by LoggingProperty(null)
fun main() {
token = "AAA" // token changed from null to AAA
val res = token // token getter returned AAA
println(res) // AAA
}
@Nullable
public static final String getToken() {
return (String)token$delegate
.getValue((Object)null, $$delegatedProperties[0]);
}
Let’s analyze this step by step. When you get a property value,
you call this property’s getter; property delegation delegates
this getter to the getValue function. When you set a property
value, you are calling this property’s setter; property delega-
tion delegates this setter to the setValue function. This way,
each delegate fully controls this property’s behavior.
You might also have noticed that the getValue and setValue
methods not only receive the value that was set to the prop-
erty and decide what its getter returns, but they also receive a
bounded reference to the property as well as a context (this).
The reference to the property is most often used to get its
name and sometimes to get information about annotations.
The parameter referencing the receiver gives us information
about where the function is used and who can use it.
import kotlin.reflect.KProperty
object AttemptsCounter {
var attempts: Int by LoggingProperty(0)
}
fun main() {
token = "AAA" // token in null changed from null to AAA
val res = token // token in null getter returned AAA
Property delegation 61
println(res) // AAA
AttemptsCounter.attempts = 1
// attempts in AttemptsCounter@XYZ changed from 0 to 1
val res2 = AttemptsCounter.attempts
// attempts in AttemptsCounter@XYZ getter returned 1
println(res2) // 1
}
class EmptyPropertyDelegate {
operator fun getValue(
thisRef: Any?,
property: KProperty<*>
): String {
return ""
}
operator fun setValue(
thisRef: Any?,
property: KProperty<*>,
value: String
) {
// no-op
}
}
fun main() {
val map: Map<String, Any> = mapOf(
"name" to "Marcin",
"kotlinProgrammer" to true
)
val name: String by map
val kotlinProgrammer: Boolean by map
print(name) // Marcin
print(kotlinProgrammer) // true
thisRef: T,
property: KProperty<*>,
value: V
)
}
Provide a delegate
import kotlin.reflect.KProperty
class LoggingPropertyProvider<T>(
private val value: T
) {
Property delegation 66
fun main() {
token = "AAA" // token changed from null to AAA
val res = token // token getter returned AAA
println(res) // AAA
}
⁵Before you do this, consider the fact that there are already
many similar libraries, such as PreferenceHolder, which I
published years ago.
Property delegation 67
class LoggingPropertyProvider<T>(
private val value: T
) : PropertyDelegateProvider<Any?, LoggingProperty<T>> {
• Delegates.notNull
• lazy
• Delegates.observable
• Delegates.vetoable
• Map<String, T> and MutableMap<String, T>
import kotlin.properties.Delegates
fun main() {
a = 10
println(a) // 10
a = 20
println(a) // 20
println(b) // IllegalStateException:
// Property b should be initialized before getting.
}
@Value("${server.port}")
var serverPort: Int by Delegates.notNull()
// ...
}
// DSL builder
fun person(block: PersonBuilder.() -> Unit): Person =
PersonBuilder().apply(block).build()
class PersonBuilder() {
lateinit var name: String
var age: Int by Delegates.notNull()
fun build(): Person = Person(name, age)
}
// DSL use
val person = person {
name = "Marc"
age = 30
}
class A {
val b = B()
val c = C()
val d = D()
// ...
}
class A {
val b by lazy { B() }
val c by lazy { C() }
val d by lazy { D() }
// ...
}
class OurLanguageParser {
val cardRegex by lazy { Regex("...") }
val questionRegex by lazy { Regex("...") }
val answerRegex by lazy { Regex("...") }
// ...
}
// Usage
print("5.173.80.254".isValidIpAddress()) // true
Property delegation 73
The problem with this function is that the Regex object needs
to be created every time we use it. This is a serious disadvan-
tage since regex pattern compilation is complex, therefore
this function is unsuitable for repeated use in performance-
constrained parts of our code. However, we can improve it by
lifting the regex up to the top level:
fun test() {
val user = User(...) // Calculating...
val copy = user.copy() // Calculating...
println(copy.fullDisplay) // XYZ
println(copy.fullDisplay) // XYZ
}
fun test() {
val user = User(...)
val copy = user.copy()
println(copy.fullDisplay) // Calculating... XYZ
println(copy.fullDisplay) // Calculating... XYZ
}
fun produceFullDisplay() {
println("Calculating...")
// ...
}
}
fun test() {
val user = User(...)
val copy = user.copy()
println(copy.fullDisplay) // Calculating... XYZ
println(copy.fullDisplay) // XYZ
}
questionLabelView =
findViewById(R.id.main_question_label)
answerLabelView =
findViewById(R.id.main_answer_label)
confirmButtonView =
findViewById(R.id.main_button_confirm)
}
}
setContentView(R.layout.main_activity)
}
}
// ActivityExt.kt
fun <T : View> Activity.bindView(viewId: Int) =
lazy { this.findViewById<T>(viewId) }
Property delegation 80
// ActivityExt.kt
fun <T> Activity.bindString(@IdRes id: Int): Lazy<T> =
lazy { this.getString(id) }
fun <T> Activity.bindColor(@IdRes id: Int): Lazy<T> =
lazy { this.getColour(id) }
fun <T : Parcelable> Activity.extra(key: String) =
lazy { this.intent.extras.getParcelable(key) }
fun Activity.extraString(key: String) =
lazy { this.intent.extras.getString(key) }
class Lazy {
var x = 0
val y by lazy { 1 / x }
fun hello() {
try {
print(y)
} catch (e: Exception) {
x = 1
print(y)
}
}
}
fun main() {
name = "Martin" // Empty -> Martin
name = "Igor" // Martin -> Igor
name = "Igor" // Igor -> Igor
}
// Alternative to
var prop: SomeType = initial
set(newValue) {
field = newValue
operation(::prop, field, newValue)
}
import kotlin.properties.Delegates.observable
import kotlin.properties.Delegates.observable
class ObservableProperty<T>(initial: T) {
private var observers: List<(T) -> Unit> = listOf()
fun main() {
val name = ObservableProperty("")
name.addObserver { println("Changed to $it") }
name.value = "A"
// Changed to A
name.addObserver { println("Now it is $it") }
name.value = "B"
// Changed to B
// Now it is B
}
fun bindToDrawerOpen(
initial: Boolean,
lazyView: () -> DrawerLayout
) = observable(initial) { _, _, open ->
if (open) drawerLayout.openDrawer(GravityCompat.START)
else drawerLayout.closeDrawer(GravityCompat.START)
}
import kotlin.properties.Delegates.observable
fun main() {
book = "TheWitcher"
repeat(69) { page++ }
println(book) // TheWitcher
println(page) // 69
book = "Ice"
println(book) // Ice
Property delegation 86
println(page) // 0
}
// Alternative to
var prop: SomeType = initial
set(newValue) {
if (operation(::prop, field, newValue)) {
field = newValue
}
}
import kotlin.properties.Delegates.vetoable
fun main() {
smallList = listOf("A", "B", "C") // [A, B, C]
println(smallList) // [A, B, C]
smallList = listOf("D", "E", "F", "G") // [D, E, F, G]
println(smallList) // [A, B, C]
smallList = listOf("H") // [H]
println(smallList) // [H]
}
import kotlin.properties.Delegates.vetoable
A map as a delegate
fun main() {
val map: Map<String, Any> = mapOf(
"name" to "Marcin",
"kotlinProgrammer" to true
)
val name: String by map
val kotlinProgrammer: Boolean by map
println(name) // Marcin
println(kotlinProgrammer) // true
}
So what are some use cases for using Map as a delegate? In most
applications, you should not need it. However, you might be
forced by an API to treat objects as maps that have some ex-
pected keys and some that might be added dynamically in the
future. I mean situations like “This endpoint will return an
object representing a user, with properties id, displayName,
etc., and on the profile page you need to iterate over all these
Property delegation 90
fun main() {
val user = User(
mapOf<String, Any>(
"id" to 1234L,
"name" to "Marcin"
)
)
println(user.name) // Marcin
println(user.id) // 1234
println(user.map) // {id=1234, name=Marcin}
}
fun main() {
val user = User(
mutableMapOf(
"id" to 123L,
"name" to "Alek",
)
)
println(user.name) // Alek
println(user.id) // 123
user.name = "Bolek"
println(user.name) // Bolek
println(user.map) // {id=123, name=Bolek}
user.map["id"] = 456
println(user.id) // 456
println(user.map) // {id=456, name=Bolek}
}
fun main() {
var a = 10
var b = a
a = 20
println(b)
}
fun main() {
val user1 = object {
var name: String = "Rafał"
}
val user2 = user1
user1.name = "Bartek"
println(user2.name)
}
interface Nameable {
val name: String
}
fun main() {
var user1: Namable = object : Nameable {
override var name: String = "Rafał"
}
val user2 = user1
user1 = object : Nameable {
override var name: String = "Bartek"
}
println(user2.name)
}
fun main() {
var list1 = listOf(1, 2, 3)
var list2 = list1
list1 += 4
println(list2)
}
fun main() {
val list1 = mutableListOf(1, 2, 3)
val list2 = list1
list1 += 4
println(list2)
}
fun main() {
var map = mapOf("a" to 10)
val a by map
map = mapOf("a" to 20)
println(a)
}
Can you see that the answer should be 10? On the other hand,
if the map were mutable, the answer would be different:
fun main() {
val mmap = mutableMapOf("a" to 10)
val a by mmap
mmap["a"] = 20
println(a)
}
Can you see that the answer should be 20? This is consistent
with the behavior of the other variables and with what prop-
erties are compiled to.
Property delegation 97
fun main() {
map = mapOf("a" to 20)
println(a) // 10
mmap["b"] = 20
println(b) // 20
}
Finally, let’s get back to our puzzle again. I hope you can see
now that changing the cities property should not influence
the value of sanFrancisco, tallinn, or kotlin. In Kotlin, we
delegate to a delegate, not to a property, just like we assign a
property to a value, not another property.
)
)
Summary
Kotlin Contracts
@kotlin.internal.InlineOnly
public inline fun CharSequence?.isNullOrBlank(): Boolean {
contract {
returns(false) implies (this@isNullOrBlank != null)
}
@ContractsDsl
@ExperimentalContracts
@InlineOnly
@SinceKotlin("1.3")
@Suppress("UNUSED_PARAMETER")
inline fun contract(builder: ContractBuilder.() -> Unit) {
}
Inline function calls are replaced with the body of these func-
tions¹³. If this body is empty, it means that such a function
call is literally replaced with nothing, so it is gone. So, why
might we want to call a function if its call is gone during code
compilation? Kotlin Contracts are a way of communicating
with the compiler; therefore, it’s good that they are replaced
with nothing, otherwise they would only disturb and slow
down our code after compilation. Inside Kotlin Contracts,
we specify extra information that the compiler can utilize to
improve the Kotlin programming experience. In the above
example, the isNullOrBlank contract specifies when the func-
tion returns false, thus the Kotlin compiler can assume that
the receiver is not null. This information is used for smart-
casting. The contract of measureTimeMillis specifies that the
block function will be called in place exactly once. Let’s see
what this means and how exactly we can specify a contract.
// C++
int mul(int x, int y)
[[expects: x > 0]]
[[expects: y > 0]]
[[ensures audit res: res > 0]]{
return x * y;
}
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
fun main() {
val i: Int
i = 42
println(i) // 42
}
fun main() {
val i: Int
run {
i = 42
}
println(i) // 42
}
@OptIn(ExperimentalContracts::class)
inline fun measureTimeMillis(block: () -> Unit): Long {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
val start = System.currentTimeMillis()
block()
return System.currentTimeMillis() - start
}
@OptIn(ExperimentalContracts::class)
fun checkTextEverySecond(callback: (String) -> Unit) {
contract {
callsInPlace(callback, InvocationKind.AT_LEAST_ONCE)
}
val task = object : TimerTask() {
override fun run() {
callback(getCurrentText())
}
}
task.run()
Timer().schedule(task, 1000, 1000)
}
fun main() {
var text: String
checkTextEverySecond {
text = it
}
println(text)
}
fun main() {
run {
println("A")
return
println("B") // unreachable
}
println("C") // unreachable
}
@OptIn(ExperimentalContracts::class)
fun VideoState.startedLoading(): Boolean {
contract {
returns(true) implies (this@startedLoading is Loading)
}
return this is Loading && this.progress > 0
}
Currently, the returns function can only use true, false, and
null as arguments. The implication must be either a parame-
ter (or receiver) that is of some type or is not null.
Kotlin Contracts 107
@OptIn(ExperimentalContracts::class)
suspend fun measureCoroutineDuration(
body: suspend () -> Unit
): Duration {
contract {
callsInPlace(body, InvocationKind.EXACTLY_ONCE)
}
val dispatcher = coroutineContext[ContinuationInterceptor]
return if (dispatcher is TestDispatcher) {
val before = dispatcher.scheduler.currentTime
Kotlin Contracts 108
body()
val after = dispatcher.scheduler.currentTime
after - before
} else {
measureTimeMillis {
body()
}
}.milliseconds
}
@OptIn(ExperimentalContracts::class)
suspend fun <T> measureCoroutineTimedValue(
body: suspend () -> T
): TimedValue<T> {
contract {
callsInPlace(body, InvocationKind.EXACTLY_ONCE)
}
var value: T
val duration = measureCoroutineDuration {
value = body()
}
return TimedValue(value, duration)
}
Summary
Java interoperability
Nullable types
Java cannot mark that a type is not nullable as all its types are
considered nullable (except for primitive types). In trying to
correct this flaw, Java developers started using Nullable and
NotNull annotations from a number of libraries that define
such annotations. These annotations are helpful but do not
offer the safety that Kotlin offers. Nevertheless, in order to
respect this convention, Kotlin also marks its types using
Nullable and NotNull annotations when compiled to JVM¹⁶.
class MessageSender {
fun sendMessage(title: String, content: String?) {}
}
On the other hand, when Kotlin sees Java types with Nullable
and NotNull annotations, it treats these types accordingly as
nullable and non-nullable types¹⁷.
Observable<List<User?>?>?.
There would be so many types to
unpack, even though we know none of them should actually
be nullable.
// Java
public class UserRepo {
// Kotlin
val repo = UserRepo()
val user1 = repo.fetchUsers()
// The type of user1 is Observable<List<User!>!>!
val user2: Observable<List<User>> = repo.fetchUsers()
val user3: Observable<List<User?>?>? = repo.fetchUsers()
kotlin.Cloneable java.lang.Cloneable
kotlin.Comparable java.lang.Comparable
kotlin.Enum java.lang.Enum
kotlin.Annotation java.lang.Annotation
kotlin.Deprecated java.lang.Deprecated
kotlin.CharSequence java.lang.CharSequence
kotlin.String java.lang.String
kotlin.Number java.lang.Number
kotlin.Throwable java.lang.Throwable
JVM primitives
// KotlinFile.kt
fun multiply(a: Int, b: Int) = a * b
Short short
Int int
Long long
Float float
Double double
Char char
Boolean boolean
Short? Short
Int? Integer
Long? Long
Float? Float
Double? Double
Char? Char
Boolean? Boolean
Set<Long> Set<Long>
IntArray int[]
Similar array types are defined for all primitive Java types.
ShortArray short[]
IntArray int[]
LongArray long[]
FloatArray float[]
DoubleArray double[]
CharArray char[]
BooleanArray boolean[]
Array<Array<LongArray>> long[][][]
Array<Array<Int>> Integer[][]
Array<Array<Array<Long>>> Long[][][]
Collection types
// Java
public final class JavaClass {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3);
numbers.add(4); // UnsupportedOperationException
}
}
// KotlinFile.kt
fun readOnlyList(): List<Int> = listOf(1, 2, 3)
fun mutableList(): MutableList<Int> = mutableListOf(1, 2, 3)
@NotNull
public static final List mutableList() {
return CollectionsKt
.mutableListOf(new Integer[]{1, 2, 3});
}
}
// Java
public final class JavaClass {
public static void main(String[] args) {
List<Integer> integers = KotlinFileKt.readOnlyList();
integers.add(20); // UnsupportedOperationException
}
}
Annotation targets
import kotlin.properties.Delegates.notNull
class User {
var name = "ABC" // getter, setter, field
var surname: String by notNull()//getter, setter, delegate
val fullName: String // only getter
get() = "$name $surname"
}
@NotNull
private String name = "ABC";
@NotNull
private final ReadWriteProperty surname$delegate;
@NotNull
public final String getName() {
return this.name;
}
@NotNull
public final String getSurname() {
return (String) this.surname$delegate
Java interoperability 120
.getValue(this, $$delegatedProperties[0]);
}
@NotNull
public final String getFullName() {
return this.name + ' ' + this.getSurname();
}
public User() {
this.surname$delegate = Delegates.INSTANCE.notNull();
}
}
class User {
@SomeAnnotation
var name = "ABC"
}
class User {
@field:SomeAnnotation
var name = "ABC"
}
annotation class A
annotation class B
annotation class C
annotation class D
annotation class E
class User {
@property:A
@get:B
@set:C
@field:D
@setparam:E
var name = "ABC"
}
Java interoperability 122
@A
public static void getName$annotations() {
}
@B
@NotNull
public final String getName() {
return this.name;
}
@C
public final void setName(@E @NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.name = var1;
}
}
class User(
@param:A val name: String
)
• param
• property
• field
Java interoperability 123
annotation class A
class User {
@A
val name = "ABC"
}
@A
public static void getName$annotations() {
}
@NotNull
public final String getName() {
return this.name;
}
}
annotation class A
annotation class B
@A
class User @B constructor(
val name: String
)
@NotNull
public final String getName() {
return this.name;
}
@B
public User(@NotNull String name) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
}
}
We can also annotate a file using the file target and place an
annotation at the beginning of the file (before the package).
An example will be shown in the JvmName section.
When you annotate an extension function or an extension
property, you can also use the receiver target to annotate the
receiver parameter.
Java interoperability 125
// Java alternative
public static final double log(@Positive double $this$log) {
return Math.log($this$log);
}
Static elements
import java.math.BigDecimal
object MoneyUtils {
fun parseMoney(text: String): Money = TODO()
}
fun main() {
val m1 = Money.usd(10.0)
val m2 = MoneyUtils.parseMoney("10 EUR")
}
// Java
public class JavaClass {
public static void main(String[] args) {
Money m1 = Money.Companion.usd(10.0);
Money m2 = MoneyUtils.INSTANCE.parseMoney("10 EUR");
}
}
// Kotlin
class Money(val amount: BigDecimal, val currency: String) {
companion object {
@JvmStatic
fun usd(amount: Double) =
Money(amount.toBigDecimal(), "PLN")
}
}
object MoneyUtils {
@JvmStatic
fun parseMoney(text: String): Money = TODO()
}
fun main() {
val money1 = Money.usd(10.0)
val money2 = MoneyUtils.parseMoney("10 EUR")
}
// Java
public class JavaClass {
public static void main(String[] args) {
Money m1 = Money.usd(10.0);
Money m2 = MoneyUtils.parseMoney("10 EUR");
}
}
JvmField
// Kotlin
class Box {
var name = ""
}
// Java
public class JavaClass {
public static void main(String[] args) {
Box box = new Box();
box.setName("ABC");
System.out.println(box.getName());
}
}
// Kotlin
class Box {
@JvmField
var name = ""
}
// Java
public class JavaClass {
public static void main(String[] args) {
Box box = new Box();
box.name = "ABC";
System.out.println(box.name);
}
}
// Kotlin
object Box {
@JvmField
var name = ""
}
// Java
public class JavaClass {
public static void main(String[] args) {
Box.name = "ABC";
System.out.println(Box.name);
}
}
// Kotlin
class MainWindow {
// ...
companion object {
const val SIZE = 10
}
}
// Java
public class JavaClass {
public static void main(String[] args) {
System.out.println(MainWindow.SIZE);
}
}
class User {
var name = "ABC"
var isAdult = true
}
Java interoperability 130
// Java alternative
public final class User {
@NotNull
private String name = "ABC";
private boolean isAdult = true;
@NotNull
public final String getName() {
return this.name;
}
JvmName
@JvmName("averageLongList")
fun List<Long>.average() = sum().toDouble() / size
@JvmName("averageIntList")
fun List<Int>.average() = sum().toDouble() / size
fun main() {
val ints: List<Int> = List(10) { it }
println(ints.average()) // 4.5
val longs: List<Long> = List(10) { it.toLong() }
println(longs.average()) // 4.5
}
Java interoperability 132
// Java
public class JavaClass {
public static void main(String[] args) {
List<Integer> ints = List.of(1, 2, 3);
double res1 = TestKt.averageIntList(ints);
System.out.println(res1); // 2.0
List<Long> longs = List.of(1L, 2L, 3L);
double res2 = TestKt.averageLongList(longs);
System.out.println(res2); // 2.0
}
}
package test
@file:JvmName("Math")
package test
JvmMultifileClass
// FooUtils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass
package demo
fun foo() {
// ...
}
Java interoperability 135
// BarUtils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass
package demo
fun bar() {
// ...
}
import demo.Utils;
JvmOverloads
class Pizza(
val tomatoSauce: Int = 1,
val cheese: Int = 0,
val ham: Int = 0,
val onion: Int = 0,
)
class EmailSender {
fun send(
Java interoperability 136
receiver: String,
title: String = "",
message: String = "",
) {
/*...*/
}
}
class EmailSender {
@JvmOverloads
fun send(
receiver: String,
title: String = "",
message: String = "",
) {
/*...*/
}
}
Unit
fun a() {}
fun main() {
println(a()) // kotlin.Unit
}
// Kotlin code
fun a(): Unit {
return Unit
}
fun main() {
println(a()) // kotlin.Unit
}
class ListAdapter {
fun setListItemListener(
listener: (
position: Int,
id: Int,
child: View,
parent: View
) -> Unit
) {
// ...
}
// ...
}
// Usage
fun usage() {
val a = ListAdapter()
a.setListItemListener { position, id, child, parent ->
// ...
}
}
class ListAdapter {
// ...
}
fun usage() {
val a = ListAdapter()
a.setListItemListener { position, id, child, parent ->
// ...
}
}
Java interoperability 142
Tricky names
class MarkdownToHtmlTest {
@Test
fun `Simple text should remain unchanged`() {
val text = "Lorem ipsum"
val result = markdownToHtml(text)
assertEquals(text, result)
}
}
Throws
void checkFirstLine() {
String line;
try {
line = readFirstLine("number.txt");
// We must catch checked exceptions,
// or declare them with throws
} catch (IOException e) {
throw new RuntimeException(e);
}
// parseInt throws NumberFormatException,
// which is an unchecked exception
int number = Integer.parseInt(line);
// Dividing two numbers might throw
// ArithmeticException of number is 0,
// which is an unchecked exception
System.out.println(10 / number);
}
}
// Kotlin
@file:JvmName("FileUtils")
package test
import java.io.*
// Kotlin
@file:JvmName("FileUtils")
package test
import java.io.*
@Throws(IOException::class)
fun readFirstLine(fileName: String): String =
File(fileName).useLines { it.first() }
Using the Throws annotation is not only useful for Kotlin and
Java interoperability; it is also often used as a form of doc-
umentation that specifies which exceptions should be ex-
pected.
JvmRecord
@JvmRecord
data class Person(val name: String, val age: Int)
Summary
plugins {
kotlin("multiplatform") version "1.8.21"
}
group = "com.marcinmoskala"
version = "0.0.1"
kotlin {
jvm {
withJava()
}
js(IR) {
browser()
binaries.library()
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx\
Using Kotlin Multiplatform 151
-coroutines-core:1.6.4")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
val jvmMain by getting
val jvmTest by getting
val jsMain by getting
val jsTest by getting
}
jvmToolchain(11)
}
We define source set files inside folders in src that have the
same name as the source set name. So, common files should
be inside the src/commonMain folder, and JVM test files should
be inside src/jvmTest.
This is how we configure common modules. Transforming
a project that uses no external libraries from Kotlin/JVM to
multiplatform is quite simple. The problem is that in the
common module, you cannot use platform-specific libraries,
so the Java stdlib and Java libraries cannot be used. You
can use only libraries that are multiplatform and support all
your targets, but it’s still an impressive list, including Kotlin
Coroutines, Kotlin Serialization, Ktor Client and many more.
To deal with other cases, it’s useful to define expected and
actual elements.
// commonMain
expect fun randomUUID(): String
// jvmMain
import java.util.*
// commonMain
expect object Platform {
val name: String
}
// jvmMain
actual object Platform {
actual val name: String = "JVM"
}
// jsMain
Using Kotlin Multiplatform 153
// commonMain
expect class DateTime {
fun getHour(): Int
fun getMinute(): Int
fun getSecond(): Int
// ...
}
// jvmMain
actual typealias DateTime = LocalDateTime
// jsMain
import kotlin . js . Date
class DateTime(
val date: Date = Date()
) {
actual fun getHour(): Int = date.getHours()
actual fun getMinute(): Int = date.getMinutes()
actual fun getSecond(): Int = date.getSeconds()
}
Possibilities
Multiplatform libraries
class YamlParser {
fun parse(text: String): YamlObject {
/*...*/
}
fun serialize(obj: YamlObject): String {
/*...*/
}
// ...
}
// ...
Since you only use Kotlin and the Kotlin Standard Library,
you can place this code in the common source set. For that,
we need to set up a multiplatform module in our project,
which entails defining the file where we will define our
common and platform modules. It needs to have its own
build.gradle(.kts) file with the Kotlin Multiplatform Gradle
plugin (kotlin("multiplatform") using kotlin dsl syntax), then
it needs to define the source sets configuration. This is where
we specify which platforms we want to compile this module
to, and we specify dependencies for each platform.
Using Kotlin Multiplatform 162
// build.gradle.kts
plugins {
kotlin("multiplatform") version "1.8.10"
// ...
java
}
kotlin {
jvm {
compilations.all {
kotlinOptions.jvmTarget = "1.8"
}
withJava()
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
}
js(IR) {
browser()
binaries.library()
}
sourceSets {
val commonMain by getting {
dependencies {
// ...
}
}
val commonTest by getting {
dependencies {
// ...
}
}
val jvmMain by getting {
dependencies {
// ...
}
}
val jvmTest by getting
val jsMain by getting
Using Kotlin Multiplatform 163
We should place our common source set files inside the “com-
monMain” folder. If we do not have any expected declara-
tions, we should now be able to generate a library in JVM
bytecode or JavaScript from our shared module.
This is how we could use it from Java:
// Java
YamlParser yaml = new YamlParser();
System.out.println(yaml.parse("someProp: ABC"));
// YamlObject(properties={someProp=YamlString(value=ABC)})
If you build this code using Kotlin/JS for the browser, this is
how you can use this class:
Using Kotlin Multiplatform 164
// JavaScript
const parser = new YamlParser();
console.log(parser.parse("someProp: ABC"))
// {properties: {someprop: "ABC"}}
@JsExport
class YamlParser {
fun parse(text: String): YamlObject {
/*...*/
}
fun serialize(obj: YamlObject): String {
/*...*/
}
// ...
}
@JsExport
sealed interface YamlElement
@JsExport
data class YamlObject(
val properties: Map<String, YamlElement>
) : YamlElement
@JsExport
data class YamlString(val value: String) : YamlElement
// ...
This code can be used not only from JavaScript but also from
TypeScript, which should see proper types for classes and
interfaces.
Using Kotlin Multiplatform 165
// TypeScript
const parser: YamlParser = new YamlParser();
const obj: YamlObject = parser.parse(text);
// jsMain module
@JsExport
@JsName("NetworkYamlReader")
class NetworkYamlReaderJs {
private val reader = NetworkYamlReader()
private val scope = CoroutineScope(SupervisorJob())
class WorkoutViewModel(
private val timer: TimerService,
private val speaker: SpeakerService,
private val loadTrainingUseCase: LoadTrainingUseCase
// ...
) : ViewModel() {
private var state: WorkoutState = ...
init {
loadTraining()
}
fun onNext() {
// ...
}
// ...
}
ViewModel class
// commonMain
expect abstract class ViewModel() {
open fun onCleared()
}
// androidMain
abstract class ViewModel : androidx.lifecycle.ViewModel() {
val scope = viewModelScope
// commonMain
expect abstract class ViewModel() {
val scope: CoroutineScope
open fun onCleared()
}
// androidMain
abstract class ViewModel : androidx.lifecycle.ViewModel() {
val scope = viewModelScope
Platform-specific classes
// commonMain
interface SpeakerService {
fun speak(text: String)
}
// Android application
class AndroidSpeaker(context: Context) : SpeakerService {
// Swift
class iOSSpeaker: Speaker {
private let synthesizer = AVSpeechSynthesizer()
Observing properties
// iOS Swift
struct LoginScreen: View {
@ObservedObject
var viewModel: WorkoutViewModel = WorkoutViewModel()
// ...
}
You can also turn StateFlow into an object that can be observed
with callback functions. There are also libraries for that, or
we can just define a simple wrapper class that will let you
collect StateFlow in Swift.
Using Kotlin Multiplatform 172
// Swift
viewModel.title.collect(
onNext: { value in
// ...
},
onCompletion: { error in
// ...
}
)
I guess there might be more options in the future, but for now
this seems to be the best approach to multiplatform Kotlin
projects.
Summary
JavaScript interoperability
Let’s say you have a Kotlin/JVM project and realize you need
to use some parts of it on a website. This is not a problem: you
can migrate these parts to a common module that can be used
to build a package to be used from JavaScript or TypeScript²⁷.
You can also distribute this package to npm and expose it to
other developers.
The first time I encountered such a situation was with Anki-
Markdown²⁸, a library I built that lets me keep my flashcards
in a special kind of Markdown and synchronize it with Anki (a
popular program for flashcards). I initially implemented this
synchronization as a JVM application I ran in the terminal,
but this was inconvenient. Since I use Obsidian to manage my
notes, I realized using AnkiMarkdown as an Obsidian plugin
would be great. For that, I needed my synchronization code in
JS. Not a problem! It took only a couple of hours to move it to
the multiplatform module and distribute it to npm, and now
I can use it in Obsidian.
The second time I had a similar situation was when I worked
for Scanz. We had a desktop client for a complex application
implemented in Kotlin/JVM. Since we wanted to create a
new web-based client, we extracted common parts (services,
view models, repositories) into a shared module and reused it.
There were some trade-offs we needed to make, as I will show
in this chapter, but we succeeded, and not only was it much
faster than rewriting all these parts, but we could also use the
common module for Android and iOS applications.
The last story I want to share is my Sudoku problem generator
and solver²⁹. It implements all the basic sudoku techniques
and uses them to generate and solve sudoku puzzles. I needed
²⁷The current implementation of Kotlin/JS compiles Kotlin
code to ES5 or ES6.
²⁸Link to AnkiMarkdown repository:
github.com/MarcinMoskala/AnkiMarkdown
²⁹Link to the repository of this project:
github.com/MarcinMoskala/sudoku-generator-solver
JavaScript interoperability 174
Setting up a project
plugins {
kotlin("multiplatform") version "1.8.21"
}
kotlin {
jvm {}
js(IR) {
browser() // use if you need to run code in a browser
nodejs() // use if you need to run code in a Node.js
useEsModules() // output .mjs ES6 modules
binaries.executable()
}
sourceSets {
val commonMain by getting {
dependencies {
// common dependencies
}
}
val jvmMain by getting
val jvmTest by getting
val jsMain by getting
val jsTest by getting
}
}
Using Kotlin/JS
fun printHello() {
console.log("Hello")
}
@Suppress("NOT_DOCUMENTED")
public external interface Console {
public fun dir(o: Any): Unit
public fun error(vararg o: Any?): Unit
public fun info(vararg o: Any?): Unit
public fun log(vararg o: Any?): Unit
public fun warn(vararg o: Any?): Unit
}
Both the console property and the Console interface are de-
clared as external, which means they are not implemented in
Kotlin, but JavaScript provides them. We can use them in our
Kotlin code but not implement them. If there is a JavaScript
element we want to use in Kotlin but don’t have a declaration
for, we can create it ourselves. For instance, if we want to use
the alert function, we can declare it as follows:
fun showAlert() {
alert("Hello")
}
@JsName("alert")
external fun alert(message: String)
fun main() {
val message = js("prompt('Enter your name')")
println(message)
}
fun main() {
val user = "John"
val surname =
js("prompt('What is your surname ${user}?')")
println(surname)
}
fun main() {
val o: dynamic = js("{name: 'John', surname: 'Foo'}")
println(o.name) // John
println(o.surname) // Foo
println(o.toLocaleString()) // [object Object]
println(o.unknown) // undefined
import kotlin.js.json
fun main() {
val o = json(
"name" to "John",
"age" to 42,
)
print(JSON.stringify(o)) // {"name":"John","age":42}
}
// AnkiMarkdown project
"dependencies": {
// ...
"AnkiMarkdown": "file:../build/productionLibrary"
}
npmPublish {
packages {
named("js") {
packageName.set("anki-markdown")
version.set(libVersion)
}
}
registries {
register("npmjs") {
uri.set(uri("https://registry.npmjs.org"))
authToken.set(npmSecret)
}
}
}
Exposing objects
@JsExport
class A(
val b: Int
) {
fun c() { /*...*/ }
}
@JsExport
fun d() { /*...*/ }
class E(
val h: String
) {
fun g() { /*...*/ }
}
@JsExport
@JsName("SudokuGenerator")
class SudokuGeneratorJs {
private val sudokuGenerator = SudokuGenerator()
@JsExport
@JsName("Sudoku")
class SudokuJs internal constructor(
private val sudoku: Sudoku
) {
fun valueAt(position: PositionJs): Int {
return sudoku.valueAt(position.toPosition())
}
fun possibilitiesAt(position: PositionJs): Array<Int> {
return sudoku.possibilitiesAt(position.toPosition())
.toTypedArray()
}
@JsExport
@JsName("Position")
class PositionJs(
val row: Int,
val column: Int
)
)
fun Position.toPositionJs() = PositionJs(
row = row,
column = column
)
class UserListViewModel(
private val userRepository: UserRepository
) : ViewModel() {
private val _userList: MutableStateFlow<List<User>> =
MutableStateFlow(emptyList())
val userList: StateFlow<List<User>> = _userList
fun loadUsers() {
viewModelScope.launch {
userRepository.fetchUsers()
.onSuccess { _usersList.value = it }
.onFailure { _error.emit(it) }
}
}
}
@JsExport
interface FlowObserver<T> {
fun stopObserving()
fun startObserving(
onEach: (T) -> Unit,
onError: (Throwable) -> Unit = {},
onComplete: () -> Unit = {},
)
}
class FlowObserverImpl<T>(
private val delegate: Flow<T>,
private val coroutineScope: CoroutineScope
) : FlowObserver<T> {
private var observeJobs: List<Job> = emptyList()
@JsExport
interface StateFlowObserver<T> : FlowObserver<T> {
val value: T
}
class StateFlowObserverImpl<T>(
private val delegate: StateFlow<T>,
private val coroutineScope: CoroutineScope
) : StateFlowObserver<T> {
private var jobs = mutableListOf<Job>()
override val value: T
get() = delegate.value
@JsExport("UserListViewModel")
class UserListViewModelJs internal constructor(
userRepository: UserRepository
) : ViewModelJs() {
val delegate = UserListViewModel(userRepository)
fun loadUsers() {
delegate.loadUsers()
}
}
// Usage
const SomeView = ({app}: { app: App }) => {
const viewModel = useMemo(() => {
app.createUserListViewModel()
JavaScript interoperability 191
}, [])
const userList = useStateFlowState(viewModel.userList)
const error = useFlowState(viewModel.error)
// ...
}
// build.gradle.kts
kotlin {
// ...
sourceSets {
// ...
val jsMain by getting {
dependencies {
implementation(npm("@js-joda/timezone", "2.18.0"))
implementation(npm("@oneidentity/zstd-js", "1.0.3"))
implementation(npm("base-x", "4.0.0"))
}
}
JavaScript interoperability 192
// ...
}
}
@JsModule("@oneidentity/zstd-js")
external object zstd {
fun ZstdInit(): Promise<ZstdCodec>
object ZstdCodec {
val ZstdSimple: ZstdSimple
val ZstdStream: ZstdStream
}
class ZstdSimple {
fun decompress(input: Uint8Array): Uint8Array
}
class ZstdStream {
fun decompress(input: Uint8Array): Uint8Array
}
}
@JsModule("base-x")
external fun base(alphabet: String): BaseConverter
Summary
Reflection
import kotlin.reflect.full.memberProperties
class Person(
val name: String,
val surname: String,
val children: Int,
val female: Boolean,
)
class Dog(
val name: String,
val age: Int,
)
fun main() {
val granny = Person("Esmeralda", "Weatherwax", 0, true)
displayPropertiesAsList(granny)
// * children: 0
// * female: true
// * name: Esmeralda
Reflection 197
// * surname: Weatherwax
displayPropertiesAsList(DogBreed.BORDER_COLLIE)
// * name: BORDER_COLLIE
// * ordinal: 3
}
fun main() {
val json = "{\"brand\":\"Jeep\", \"doors\": 3}"
val gson = Gson()
val car: Car = gson.fromJson(json, Car::class.java)
println(car) // Car(brand=Jeep, doors=3)
val newJson = gson.toJson(car)
println(newJson) // {"brand":"Jeep", "doors": 3}
}
Hierarchy of classes
Before we get into the details, let’s first review the general
type hierarchy of element references.
Reflection 199
Notice that all the types in this hierarchy start with the K
prefix. This indicates that this type is part of Kotlin Reflection
and differentiates these classes from Java Reflection. The
type Class is part of Java Reflection, so Kotlin called its equiv-
alent KClass.
At the top of this hierarchy, you can find KAnnotatedElement.
Element is a term that includes classes, functions, and proper-
ties, so it includes everything we can reference. All elements
can be annotated, which is why this interface includes the
annotations property, which we can use to get element anno-
tations.
interface KAnnotatedElement {
val annotations: List<Annotation>
}
The next confusing thing you might have noticed is that there
is no type to represent interfaces. This is because interfaces in
reflection API nomenclature are also considered classes, so
their references are of type KClass. This might be confusing,
but it is really convenient.
Now we can get into the details, which is not easy because
everything is connected to nearly everything else. At the same
time, using the reflection API is really intuitive and easy to
learn. Nevertheless, I decided that to help you understand
this API better I’ll do something I generally avoid doing: we
will go through the essential classes and discuss their meth-
ods and properties. In between, I will show you some practi-
cal examples and explain some essential reflection concepts.
Function references
import kotlin.reflect.*
fun printABC() {
println("ABC")
}
fun main() {
val f1 = ::printABC
val f2 = ::double
val f3 = Complex::plus
val f4 = Complex::double
val f5 = Complex?::isNullOrZero
val f6 = Box<Int>::get
val f7 = Box<String>::set
}
Reflection 201
// ...
fun main() {
val f1: KFunction0<Unit> =
::printABC
val f2: KFunction1<Int, Int> =
::double
val f3: KFunction2<Complex, Number, Complex> =
Complex::plus
val f4: KFunction1<Complex, Complex> =
Complex::double
val f5: KFunction1<Complex?, Boolean> =
Complex?::isNullOrZero
val f6: KFunction1<Box<Int>, Int> =
Box<Int>::get
val f7: KFunction2<Box<String>, String, Unit> =
Box<String>::set
}
// ...
fun main() {
val c = Complex(1.0, 2.0)
val f3: KFunction1<Number, Complex> = c::plus
val f4: KFunction0<Complex> = c::double
val f5: KFunction0<Boolean> = c::isNullOrZero
val b = Box(123)
val f6: KFunction0<Int> = b::get
val f7: KFunction1<Int, Unit> = b::set
}
// ...
fun main() {
val f1: KFunction<Unit> = ::printABC
val f2: KFunction<Int> = ::double
val f3: KFunction<Complex> = Complex::plus
val f4: KFunction<Complex> = Complex::double
val f5: KFunction<Boolean> = Complex?::isNullOrZero
val f6: KFunction<Int> = Box<Int>::get
val f7: KFunction<Unit> = Box<String>::set
val c = Complex(1.0, 2.0)
val f8: KFunction<Complex> = c::plus
val f9: KFunction<Complex> = c::double
val f10: KFunction<Boolean> = c::isNullOrZero
val b = Box(123)
val f11: KFunction<Int> = b::get
val f12: KFunction<Unit> = b::set
}
// ...
fun main() {
val f: KFunction2<Int, Int, Int> = ::add
println(f(1, 2)) // 3
println(f.invoke(1, 2)) // 3
}
import kotlin.reflect.KFunction
fun main() {
val f: KFunction<String> = String::times
println(f.isInline) // true
println(f.isExternal) // false
println(f.isOperator) // true
println(f.isInfix) // true
println(f.isSuspend) // false
}
import kotlin.reflect.KCallable
fun main() {
val f: KCallable<String> = String::times
println(f.name) // times
println(f.parameters.map { it.name }) // [null, times]
println(f.returnType) // kotlin.String
println(f.typeParameters) // []
println(f.visibility) // PUBLIC
println(f.isFinal) // true
println(f.isOpen) // false
println(f.isAbstract) // false
println(f.isSuspend) // false
}
KCallable also has two methods that can be used to call it. The
first one, call, accepts a vararg number of parameters of type
Any? and the result type R, which is the only KCallable type
parameter. When we call the call method, we need to provide
a proper number of values with appropriate types, otherwise,
it throws IllegalArgumentException. Optional arguments must
also have a value specified when we use the call function.
Reflection 206
import kotlin.reflect.KCallable
fun main() {
val f: KCallable<Int> = ::add
println(f.call(1, 2)) // 3
println(f.call("A", "B")) // IllegalArgumentException
}
import kotlin.reflect.KCallable
fun sendEmail(
email: String,
title: String = "",
message: String = ""
) {
println(
"""
Sending to $email
Title: $title
Message: $message
""".trimIndent()
)
}
fun main() {
val f: KCallable<Unit> = ::sendEmail
f.callBy(mapOf(f.parameters[0] to "ABC"))
// Sending to ABC
// Title:
// Message:
Reflection 207
Parameter references
import kotlin.reflect.KCallable
import kotlin.reflect.KParameter
import kotlin.reflect.typeOf
fun sendEmail(
email: String,
title: String,
message: String = ""
) {
println(
Reflection 209
"""
Sending to $email
Title: $title
Message: $message
""".trimIndent()
)
}
fun printSum(a: Int, b: Int) {
println(a + b)
}
fun Int.printProduct(b: Int) {
println(this * b)
}
fun main() {
callWithFakeArgs(::sendEmail)
// Sending to Fake email
// Title: Fake title
// Message:
callWithFakeArgs(::printSum) // 246
callWithFakeArgs(Int::printProduct) // 15129
}
Property references
import kotlin.reflect.*
import kotlin.reflect.full.memberExtensionProperties
class Box(
var value: Int = 0
) {
val Int.addedToBox
get() = Box(value + this)
}
fun main() {
val p1: KProperty0<Any> = ::lock
println(p1) // val lock: kotlin.Any
val p2: KMutableProperty0<String> = ::str
println(p2) // var str: kotlin.String
val p3: KMutableProperty1<Box, Int> = Box::value
println(p3) // var Box.value: kotlin.Int
val p4: KProperty2<Box, *, *> = Box::class
.memberExtensionProperties
.first()
println(p4) // val Box.(kotlin.Int.)addedToBox: Box
}
import kotlin.reflect.*
class Box(
var value: Int = 0
)
fun main() {
val box = Box()
val p: KMutableProperty1<Box, Int> = Box::value
println(p.get(box)) // 0
p.set(box, 999)
println(p.get(box)) // 999
}
Class reference
import kotlin.reflect.KClass
class A
fun main() {
val class1: KClass<A> = A::class
println(class1) // class A
val a: A = A()
val class2: KClass<out A> = a::class
println(class2) // class A
}
import kotlin.reflect.KClass
open class A
class B : A()
fun main() {
val a: A = B()
val clazz: KClass<out A> = a::class
println(clazz) // class B
}
• Simple name is just the name used after the class key-
word. We can read it using the simpleName property.
Reflection 214
package a.b.c
class D {
class E
}
fun main() {
val clazz = D.E::class
println(clazz.simpleName) // E
println(clazz.qualifiedName) // a.b.c.D.E
}
fun main() {
val o = object {}
val clazz = o::class
println(clazz.simpleName) // null
println(clazz.qualifiedName) // null
}
KClass has only a few properties that let us check some class-
specific characteristics:
fun main() {
println(UserMessages::class.visibility) // PUBLIC
println(UserMessages::class.isSealed) // true
println(UserMessages::class.isOpen) // false
println(UserMessages::class.isFinal) // false
println(UserMessages::class.isAbstract) // false
println(UserId::class.visibility) // PRIVATE
println(UserId::class.isData) // true
println(UserId::class.isFinal) // true
println(UserId.Companion::class.isCompanion) // true
Reflection 216
println(UserId.Companion::class.isInner) // false
println(Filter::class.visibility) // INTERNAL
println(Filter::class.isFun) // true
}
import kotlin.reflect.full.*
fun Child.e() {}
fun main() {
println(Child::class.members.map { it.name })
// [c, d, a, b, equals, hashCode, toString]
println(Child::class.functions.map { it.name })
// [d, b, equals, hashCode, toString]
println(Child::class.memberProperties.map { it.name })
// [c, a]
println(Child::class.declaredMembers.map { it.name })
// [c, d]
println(Child::class.declaredFunctions.map { it.name })
// [d]
println(
Child::class.declaredMemberProperties.map { it.name }
) // [c]
}
package playground
import kotlin.reflect.KFunction
fun main() {
val constructors: Collection<KFunction<User>> =
User::class.constructors
println(constructors.size) // 3
constructors.forEach(::println)
// fun <init>(playground.User): playground.User
// fun <init>(playground.UserJson): playground.User
// fun <init>(kotlin.String): playground.User
}
import kotlin.reflect.KClass
import kotlin.reflect.full.superclasses
interface I1
interface I2
open class A : I1
class B : A(), I2
fun main() {
val a = A::class
val b = B::class
println(a.superclasses) // [class I1, class kotlin.Any]
println(b.superclasses) // [class A, class I2]
println(a.supertypes) // [I1, kotlin.Any]
println(b.supertypes) // [A, I2]
}
interface I1
interface I2
open class A : I1
class B : A(), I2
fun main() {
val a = A()
val b = B()
println(A::class.isInstance(a)) // true
println(B::class.isInstance(a)) // false
println(I1::class.isInstance(a)) // true
println(I2::class.isInstance(a)) // false
println(A::class.isInstance(b)) // true
println(B::class.isInstance(b)) // true
println(I1::class.isInstance(b)) // true
println(I2::class.isInstance(b)) // true
}
Reflection 220
fun main() {
println(List::class.typeParameters) // [out E]
println(Map::class.typeParameters) // [K, out V]
}
class A {
class B
inner class C
}
fun main() {
println(A::class.nestedClasses) // [class A$B, class A$C]
}
fun main() {
println(LinkedList::class.sealedSubclasses)
// [class Node, class Empty]
}
Reflection 221
An object declaration has only one instance, and we can get its
reference using the objectInstance property of type T?, where T
is the KClass type parameter. This property returns null when
a class does not represent an object declaration.
import kotlin.reflect.KClass
fun main() {
printInstance(Node::class) // null
printInstance(Empty::class) // Empty@XYZ
}
Serialization example
class Creature(
val name: String,
val attack: Int,
val defence: Int,
)
fun main() {
val creature = Creature(
name = "Cockatrice",
attack = 2,
defence = 4
)
println(creature.toJson())
// {"attack": 2, "defence": 4, "name": "Cockatrice"}
}
import kotlin.reflect.full.memberProperties
// Example use
class Creature(
val name: String,
val attack: Int,
val defence: Int,
val traits: List<Trait>,
val cost: Map<Element, Int>
)
enum class Element {
FOREST, ANY,
}
enum class Trait {
FLYING
}
fun main() {
val creature = Creature(
name = "Cockatrice",
attack = 2,
defence = 4,
traits = listOf(Trait.FLYING),
cost = mapOf(
Element.ANY to 3,
Element.FOREST to 2
)
)
println(creature.toJson())
// {"attack": 2, "cost": {"ANY": 3, "FOREST": 2},
// "defence": 4, "name": "Cockatrice",
// "traits": ["FLYING"]}
}
// Annotations
@Target(AnnotationTarget.PROPERTY)
annotation class JsonName(val name: String)
@Target(AnnotationTarget.PROPERTY)
annotation class JsonIgnore
// Example use
class Creature(
@JsonIgnore val name: String,
@JsonName("att") val attack: Int,
@JsonName("def") val defence: Int,
val traits: List<Trait>,
val cost: Map<Element, Int>
)
enum class Element {
FOREST, ANY,
}
enum class Trait {
FLYING
}
fun main() {
val creature = Creature(
name = "Cockatrice",
attack = 2,
defence = 4,
traits = listOf(Trait.FLYING),
cost = mapOf(
Element.ANY to 3,
Element.FOREST to 2
)
)
println(creature.toJson())
// {"att": 2, "cost": {"ANY": 3, "FOREST": 2},
// "def": 4, "traits": ["FLYING"]}
}
Referencing types
Examples of types and classes. This image was first published in my book Kotlin
Essentials.
Relations between classes, types and objects. This image was first published in my
book Kotlin Essentials.
import kotlin.reflect.KType
import kotlin.reflect.typeOf
fun main() {
val t1: KType = typeOf<Int?>()
println(t1) // kotlin.Int?
val t2: KType = typeOf<List<Int?>>()
println(t2) // kotlin.collections.List<kotlin.Int?>
val t3: KType = typeOf<() -> Map<Int, Char?>>()
println(t3)
// () -> kotlin.collections.Map<kotlin.Int, kotlin.Char?>
}
import kotlin.reflect.typeOf
fun main() {
println(typeOf<Int>().isMarkedNullable) // false
println(typeOf<Int?>().isMarkedNullable) // true
}
import kotlin.reflect.typeOf
class Box<T>
fun main() {
val t1 = typeOf<List<Int>>()
println(t1.arguments) // [kotlin.Int]
val t2 = typeOf<Map<Long, Char>>()
println(t2.arguments) // [kotlin.Long, kotlin.Char]
val t3 = typeOf<Box<out String>>()
println(t3.arguments) // [out kotlin.String]
}
import kotlin.reflect.*
fun main() {
val t1 = typeOf<List<Int>>()
println(t1.classifier) // class kotlin.collections.List
println(t1 is KType) // true
Reflection 231
val t3 = Box<Int>::get.returnType.classifier
println(t3) // T
println(t3 is KTypeParameter) // true
}
// KTypeParameter definition
interface KTypeParameter : KClassifier {
val name: String
val upperBounds: List<KType>
val variance: KVariance
val isReified: Boolean
}
class ValueGenerator(
private val random: Random = Random,
) {
inline fun <reified T> randomValue(): T =
randomValue(typeOf<T>()) as T
import kotlin.random.Random
import kotlin.reflect.KType
import kotlin.reflect.typeOf
class RandomValueConfig(
val nullProbability: Double = 0.1,
)
class ValueGenerator(
private val random: Random = Random,
val config: RandomValueConfig = RandomValueConfig(),
) {
Now we can add support for some other basic types. Boolean
is simplest because it can be generated using nextBoolean from
Random. The same can be said about Int, but 0 is a special value,
Reflection 233
import kotlin.math.ln
import kotlin.random.Random
import kotlin.reflect.KType
import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.typeOf
class RandomValueConfig(
val nullProbability: Double = 0.1,
val zeroProbability: Double = 0.1,
)
class ValueGenerator(
private val random: Random = Random,
val config: RandomValueConfig = RandomValueConfig(),
) {
import kotlin.math.ln
import kotlin.random.Random
import kotlin.reflect.KType
import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.full.withNullability
import kotlin.reflect.typeOf
class RandomValueConfig(
val nullProbability: Double = 0.1,
val zeroProbability: Double = 0.1,
val stringSizeParam: Double = 0.1,
val listSizeParam: Double = 0.3,
)
Reflection 235
class ValueGenerator(
private val random: Random = Random,
val config: RandomValueConfig = RandomValueConfig(),
) {
companion object {
private val CHARACTERS =
('A'..'Z') + ('a'..'z') + ('0'..'9') + " "
}
}
fun main() {
val r = Random(1)
val g = ValueGenerator(random = r)
println(g.randomValue<Int>()) // -527218591
println(g.randomValue<Int?>()) // -2022884062
println(g.randomValue<Int?>()) // null
println(g.randomValue<List<Int>>())
// [-1171478239]
println(g.randomValue<List<List<Boolean>>>())
// [[true, true, false], [], [], [false, false], [],
// [true, true, true, true, true, true, true, false]]
println(g.randomValue<List<Int?>?>())
// [-416634648, null, 382227801]
println(g.randomValue<String>()) // WjMNxTwDPrQ
println(g.randomValue<List<String?>>())
// [VAg, , null, AIKeGp9Q7, 1dqARHjUjee3i6XZzhQ02l, DlG, , ]
}
import java.lang.reflect.*
import kotlin.reflect.*
import kotlin.reflect.jvm.*
class A {
val a = 123
fun b() {}
}
fun main() {
val c1: Class<A> = A::class.java
val c2: Class<A> = A().javaClass
Breaking encapsulation
import kotlin.reflect.*
import kotlin.reflect.full.*
import kotlin.reflect.jvm.isAccessible
class A {
private var value = 0
private fun printValue() {
println(value)
}
override fun toString(): String =
"A(value=$value)"
}
fun main() {
val a = A()
val c = A::class
prop?.set(a, 999)
println(a) // A(value=999)
println(prop?.get(a)) // 999
Summary
Annotation processing
interface UserRepository {
fun findUser(userId: String): User?
fun findUsers(): List<User>
fun updateUser(user: User)
fun insertUser(user: User)
}
@GenerateInterface("UserRepository")
class MongoUserRepository : UserRepository {
override fun findUser(userId: String): User? = TODO()
override fun findUsers(): List<User> = TODO()
override fun updateUser(user: User) {
TODO()
}
override fun insertUser(user: User) {
TODO()
}
}
// build.gradle.kts
plugins {
kotlin("kapt") version "<your_kotlin_version>"
}
dependencies {
implementation(project(":generateinterface-annotations"))
kapt(project(":generateinterface-processor"))
// ...
}
package academy.kt
import kotlin.annotation.AnnotationTarget.CLASS
@Target(CLASS)
annotation class GenerateInterface(val name: String)
package academy.kt
academy.kt.GenerateInterfaceProcessor
buildInterfaceFile(
interfacePackage,
interfaceName,
publicMethods
).writeTo(processingEnv.filer)
}
Note that you can also use a library like KotlinPoet and gener-
ate a Kotlin file instead of a Java file. I decided to generate a
Java file for two reasons:
That is all we need. If you build your main module again, the
code using the GenerateInterface annotation should compile.
class MockitoInjectMocksExamples {
@Mock
lateinit var emailService: EmailService
@Mock
lateinit var smsService: SMSService
@InjectMocks
lateinit var notificationSender: NotificationSender
@BeforeEach
fun setup() {
MockitoAnnotations.initMocks(this)
}
// ...
}
@RestController
class WelcomeResource {
@Value("\${welcome.message}")
private lateinit var welcomeMessage: String
@Autowired
private lateinit var configuration: BasicConfiguration
@GetMapping("/welcome")
fun retrieveWelcomeMessage(): String = welcomeMessage
@RequestMapping("/dynamic-configuration")
fun dynamicConfiguration(): Map<String, Any?> = mapOf(
"message" to configuration.message,
"number" to configuration.number,
"key" to configuration.isValue,
)
}
@SpringBootApplication
open class MyApp {
companion object {
@JvmStatic
fun main(args: Array<String>) {
SpringApplication.run(MyApp::class.java, *args)
}
}
}
Summary
To bring our discussion about KSP into the real world, let’s
implement the same library as in the previous chapter, but
using KSP instead of Java Annotation Processing. So, we will
generate an interface for a class that includes all the public
methods of this class. This is the code we will use to test our
solution:
@GenerateInterface("UserRepository")
class MongoUserRepository<T> : UserRepository {
@Throws(DuplicatedUserId::class)
override suspend fun insertUser(user: User) {
TODO()
}
}
interface UserRepository {
suspend fun findUser(userId: String): User?
@Throws(DuplicatedUserId::class)
suspend fun insertUser(user: User)
}
// build.gradle.kts
plugins {
id("com.google.devtools.ksp")
// …
}
dependencies {
implementation(project(":annotations"))
ksp(project(":processor"))
// ...
}
// build.gradle.kts
plugins {
id("com.google.devtools.ksp")
}
dependencies {
implementation(project(":annotations"))
ksp(project(":processor"))
kspTest(project(":processor"))
// ...
}
package academy.kt
import kotlin.annotation.AnnotationTarget.CLASS
@Target(CLASS)
annotation class GenerateInterface(val name: String)
class GenerateInterfaceProcessorProvider
: SymbolProcessorProvider {
academy.kt.GenerateInterfaceProcessorProvider
class GenerateInterfaceProcessor(
private val codeGenerator: CodeGenerator,
) : SymbolProcessor {
return emptyList()
}
) {
// ...
}
)
.build()
companion object {
val IGNORED_MODIFIERS =
listOf(Modifier.OPEN, Modifier.OVERRIDE)
}
Kotlin Symbol Processing 267
Testing KSP
sources = listOf(
SourceFile.kotlin(sourceFileName, source)
)
symbolProcessorProviders = listOf(
GenerateInterfaceProcessorProvider()
)
}
val result = compilation.compile()
assertEquals(OK, result.exitCode)
With such a function, I can easily verify that the code I expect
to be generated for a specific annotated class is correct:
Kotlin Symbol Processing 269
class GenerateInterfaceProcessorTest {
@Test
fun `should generate interface for simple class`() {
assertGeneratedFile(
sourceFileName = "RealTestRepository.kt",
source = """
import academy.kt.GenerateInterface
@GenerateInterface("TestRepository")
class RealTestRepository {
fun a(i: Int): String = TODO()
private fun b() {}
}
""",
generatedResultFileName = "TestRepository.kt",
generatedSource = """
import kotlin.Int
import kotlin.String
// ...
}
class GenerateInterfaceProcessorTest {
// ...
@Test
fun `should fail when incorrect name`() {
assertFailsWithMessage(
sourceFileName = "RealTestRepository.kt",
source = """
import academy.kt.GenerateInterface
@GenerateInterface("")
class RealTestRepository {
fun a(i: Int): String = TODO()
private fun b() {}
}
""",
message = "Interface name cannot be empty"
)
}
// ...
}
// A.kt
@GenerateInterface("IA")
class A {
fun a()
}
// B.kt
@GenerateInterface("IB")
class B {
fun b()
}
So now, how does a file become dirty, and how does it become
clean? The situation is simple in our project: for each input
file, we generate one output file. So, when the input file is
changed, it becomes dirty. When the corresponding output
file is generated for it, the input file becomes clean. However,
things can get much more complex. We can generate multiple
output files from one input file, or multiple input files might
be used to generate one output file, or one output file might
depend on other output files.
Consider a situation where a generated file is based not only
on the annotated element but also on its parent. So, if this
parent changes, the file should be reprocessed.
// A.kt
@GenerateInterface
open class A {
// ...
}
// B.kt
class B : A() {
// ...
}
fun classWithParents(
classDeclaration: KSClassDeclaration
): List<KSClassDeclaration> =
classDeclaration.superTypes
.map { it.resolve().declaration }
.filterIsInstance<KSClassDeclaration>()
.flatMap { classWithParents(it) }
.toList()
.plus(classDeclaration)
By rule, if any input file of an output file becomes dirty, all the
other dependencies of this output file become dirty too. This
relationship is transitive. Consider the following scenario:
If the output file OA.kt depends on A.kt and B.kt, then:
@Single
class UserRepository {
// ...
}
@Provide
class UserService(
val userRepository: UserRepository
) {
Kotlin Symbol Processing 277
// ...
}
class UserRepositoryProvider :
SingleProvider<UserRepository>() {
class ProviderGenerator(
private val codeGenerator: CodeGenerator,
) : SymbolProcessor {
return notProcessed.toList()
}
// ...
}
plugins {
kotlin("multiplatform")
id("com.google.devtools.ksp")
}
kotlin {
jvm {
withJava()
}
linuxX64() {
binaries {
executable()
}
}
sourceSets {
val commonMain by getting
val linuxX64Main by getting
val linuxX64Test by getting
}
}
dependencies {
add("kspCommonMainMetadata", project(":test-processor"))
add("kspJvm", project(":test-processor"))
add("kspJvmTest", project(":test-processor"))
// Doing nothing, because there's no such test source set
add("kspLinuxX64Test", project(":test-processor"))
// kspLinuxX64 source set will not be processed
}
Kotlin Symbol Processing 280
Summary
Compiler frontend is responsible for parsing and analyzing Kotlin code and
transforming it into a representation that is sent to the backend, on the basis
of which the backend generates platform-specific files. The frontend is target-
independent, but there are two frontends: older K1, and newer K2. The backend
is target-specific.
When you use Kotlin in an IDE like IntelliJ, the IDE shows you
warnings, errors, component usages, code completions, etc.,
but IntelliJ itself doesn’t analyze Kotlin: all these features are
based on communication with the Kotlin Compiler, which
has a special API for IDEs, and the frontend is responsible for
this communication.
Each backend variant shares a part that generates Kotlin
intermediate representation from the representation provided
by the frontend (in the case of K2, it is FIR, which means
frontend intermediate representation). Platform-specific files
are generated based on this representation.
Kotlin Compiler Plugins 283
Each backend shares a part that transforms the representation provided by the
frontend into Kotlin intermediate representation, which is used to generate target-
specific files.
Compiler extensions
Kotlin Compiler extensions are also divided into those for the
frontend or the backend. All the frontend extensions start
with the Fir prefix and end with the Extension suffix. Here is
the complete list of the currently supported K2 extensions⁴⁷:
⁴⁷K1 extensions are deprecated, so I will just skip them.
Kotlin Compiler Plugins 284
plugins {
id("kotlin-parcelize")
}
We’ll start our journey with a simple task: make all classes
open. This behavior is inspired by the AllOpen plugin, which
opens all classes annotated with one of the specified annota-
tions. However, our example will be simpler as we will just
open all classes.
As a dependency, we only need kotlin-compiler-embeddable
that offers us the classes we can use for defining plugins.
Just like in KSP or Annotation Processing, we need
to add a file to resources/META-INF/services with the
Kotlin Compiler Plugins 289
// org.jetbrains.kotlin.compiler.plugin.
// CompilerPluginRegistrar
com.marcinmoskala.AllOpenComponentRegistrar
@file:OptIn(ExperimentalCompilerApi::class)
class FirAllOpenStatusTransformer(
session: FirSession
) : FirStatusTransformerExtension(session) {
override fun needTransformStatus(
declaration: FirDeclaration
): Boolean = declaration is FirRegularClass
Changing a type
class FirScriptSamWithReceiverConventionTransformer(
session: FirSession
) : FirSamConversionTransformerExtension(session) {
override fun getCustomFunctionTypeForSamConversion(
function: FirSimpleFunction
): ConeLookupTagBasedType? {
val containingClassSymbol = function
.containingClassLookupTag()
?.toFirRegularClassSymbol(session)
?: return null
return if (shouldTransform(it)) {
val parameterTypes = function.valueParameters
.map { it.returnTypeRef.coneType }
if (parameterTypes.isEmpty()) return null
createFunctionType(
getFunctionType(it),
parameters = parameterTypes.drop(1),
receiverType = parameterTypes[0],
rawReturnType = function.returnTypeRef
.coneType
)
} else null
}
// ...
}
This folder includes not only K2 plugins but also K1 and KSP-
based plugins. We are only interested in K2 plugins, so you can
Kotlin Compiler Plugins 294
Summary
A real world linter. Similarly to it, static analyser inspect your codebase and cap-
ture potential bugs and problems before they hit production - Photo is licensed un-
der Creative Commons - Source https://www.pexels.com/photo/woman-in-black-
long-sleeve-shirt-holding-a-lint-roller-6865186/
Static Code Analysers 299
Type of analysers
Formatters
AST Analysers
Once you have an Abstract Syntax Tree, you can visit this tree
and perform inspection on specific nodes of it. Static analy-
sers generally use the visitor pattern to register inspection on
specific nodes of the AST.
Say for example that you want to prevent the usage of the
println function in your codebase, as you have a dedicated
logger and want to make sure all the logging goes through it.
Your inspector can ask to visit all the nodes of the AST which
are of type “function call”. On those nodes, it will report a
finding whenever the caller function is the println⁵¹.
An example of an Abstract Syntax Tree - On
the left, a simple Kotlin file. On the right, PSI-
Viewer shows the PSI ([Program Structure Inter-
face](https://plugins.jetbrains.com/docs/intellij/psi.html))
representation of such code. The caret is on the println
function invocation, which corresponds to a CALL_EXPRESSION
node in the tree.
The amount of information available in the AST might vary
based on different tool and the type of inspection that you’re
interested in doing. Sometimes is sufficient to have only the
syntactical information in the AST (e.g. the name of the
parameters). For more advanced inspections, you might need
an AST which has type information (e.g. the return type of a
function invoked from a 3rd party library).
Popular AST analysers in the industry are tools like FindBugs
for Java or ESlint for JavaScript.
Data-Flow Analysers
Code Manipulation
Embedded vs Standalone
On the other hand, tools like Detekt, Ktlint, and others are
standalone. You can typically execute them from the com-
mand line by specifying your source file. Those standalone
static analysers offer plugin and integrations for the most
popular developer environments (IntelliJ IDEA, Gradle, Vi-
sual Studio Code and so on). On top of this, standalone static
analysers are generally better suited to be integrated as part
of pre-commit hooks.
Ktlint
KtFmt
Android Lint
Detekt
Setting-up Detekt
plugins {
kotlin("jvm") version "..."
// Add this line
id("io.gitlab.arturbosch.detekt") version "..."
}
This line adds the Detekt Gradle Plugin to your project and
is sufficient to set up Detekt in your project with the default
configuration.
You can see it in action by invoking the build Gradle task from
the command-line:
$ ./gradlew build
fun main() {
println(42)
}
$ ./gradlew build
[...]
BUILD FAILED in 470ms
Configuring Detekt
$ ./gradlew detektGenerateConfig
This will create a config file at the path you can find in the
console, by copying the Detekt default one. The configuration
file looks like the following:
...
comments:
active: true
AbsentOrWrongFileLicense:
active: false
licenseTemplateFile: 'license.template'
licenseTemplateIsRegex: false
CommentOverPrivateFunction:
active: false
...
Incremental Adoption
<SmellBaseline>
<ManuallySuppressedIssues>
<ID>CatchRuntimeException:Junk.kt$e: RuntimeException\
</ID>
</ManuallySuppressedIssues>
<CurrentIssues>
<ID>NestedBlockDepth:Indentation.kt$Indentation$overr\
ide fun procedure(node: ASTNode)</ID>
<ID>TooManyFunctions:LargeClass.kt$io.gitlab.arturbos\
ch.detekt.rules.complexity.LargeClass.kt</ID>
<ID>ComplexMethod:DetektExtension.kt$DetektExtension$\
fun convertToArguments(): MutableList<String></ID>
</CurrentIssues>
</SmellBaseline>
Static Code Analysers 311
Now that you know the basics of how to use Detekt, it’s time to
learn how to write a custom rule to run your own inspection.
fun main() {
// Non compliant
System.out.print("Hello")
// Compliant
println("World!")
}
@KotlinCoreEnvironmentTest
internal class MyRuleTest(private val env: KotlinCoreEnvironm\
ent) {
@Test
fun `reports usages of System_out_println`() {
val code = """
fun main() {
System.out.println("Hello")
}
"""
@Test
fun `does not report usages Kotlin's println`() {
val code = """
fun main() {
println("Hello")
}
Static Code Analysers 313
"""
override
fun visitDotQualifiedExpression(expression: KtDotQualifie\
dExpression) {
super.visitDotQualifiedExpression(expression)
if (expression.text.startsWith("System.out.println"))\
{
report(
CodeSmell(
issue,
Entity.from(expression),
"You should not use System.out.println, u\
se Kotlin's println instead",
)
)
}
}
}
Now that you coded your rule, the last part is how you can
distribute your rule and let others use it.
Your rule should be published to a Maven Repository and con-
sumed as any other dependency in a Gradle project. You won’t
be using an implementation dependency but a detektPlugin de-
pendency though.
plugins {
kotlin("jvm") version "..."
id("io.gitlab.arturbosch.detekt") version "..."
}
dependencies {
detektPlugin("org.example:detekt-custom-rule:...")
}
The user will then have to turn on your rule in their config
file:
Static Code Analysers 316
...
MyRuleSet:
MyRule:
active: true
...
And they will start seing inspections as they run Detekt nor-
mally:
$ ./gradlew build
[...]
BUILD FAILED in 470ms
Conclusion
Ending
Notes
Ending 321
Notes
Ending 322
Notes
Ending 323
Notes
Ending 324
Notes
Ending 325
Notes
Ending 326
Notes
Ending 327
Notes
Ending 328
Notes
Ending 329
Notes
Ending 330
Notes
Ending 331
Notes
Ending 332
Notes
Ending 333
Notes
Ending 334
Notes
Ending 335
Notes
Ending 336
Notes
Ending 337
Notes
Ending 338
Notes
Ending 339
Notes
Ending 340
Notes
Ending 341
Notes
Ending 342
Notes
Ending 343
Notes
Ending 344
Notes
Ending 345
Notes