Advanced Kotlin Kotlin for Developers
Advanced Kotlin Kotlin for Developers
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
Exercise: Generic types usage 37
Exercise: Generic Response 38
Exercise: Generic Consumer 39
Interface delegation 42
The delegation pattern 42
Delegation and inheritance 43
Kotlin interface delegation support 45
Wrapper classes 49
The decorator pattern 50
Intersection types 53
CONTENTS
Limitations 55
Conflicting elements from parents 56
Summary 57
Exercise: ApplicationScope 58
Property delegation 59
How property delegation works 60
Other getValue and setValue parameters 64
Implementing a custom property delegate 67
Provide a delegate 70
Property delegates in Kotlin stdlib 73
The notNull delegate 73
Exercise: Lateinit delegate 75
The lazy delegate 76
Exercise: Blog Post Properties 88
The observable delegate 89
The vetoable delegate 94
A map as a delegate 96
Review of how variables work 98
Summary 105
Exercise: Mutable lazy delegate 105
Kotlin Contracts 107
The meaning of a contract 108
How many times do we invoke a function from an
argument? 109
Implications of the fact that a function has re-
turned a value 113
Using contracts in practice 115
Summary 116
Exercise: Coroutine time measurement 117
Java interoperability 119
Nullable types 119
Kotlin type mapping 122
JVM primitives 123
Collection types 125
Annotation targets 129
Static elements 135
JvmField 137
Using Java accessors in Kotlin 139
JvmName 140
CONTENTS
JvmMultifileClass 144
JvmOverloads 145
Unit 148
Function types and function interfaces 149
Tricky names 152
Throws 153
JvmRecord 156
Summary 157
Exercise: Adjust Kotlin for Java usage 157
Using Kotlin Multiplatform 161
Multiplatform module configuration 161
Expect and actual elements 164
Possibilities 167
Multiplatform libraries 171
A multiplatform mobile application 179
Summary 185
Exercise: Multiplatform LocalDateTime 185
JavaScript interoperability 187
Setting up a project 188
Using libraries available for Kotlin/JS 190
Using Kotlin/JS 190
Building and linking a package 193
Distributing a package to npm 195
Exposing objects 195
Exposing Flow and StateFlow 200
Adding npm dependencies 205
Frameworks and libraries for Kotlin/JS 207
JavaScript and Kotlin/JS limitations 207
Summary 209
Exercise: Migrating a Kotlin/JVM project to KMP 209
Reflection 211
Hierarchy of classes 213
Function references 214
Parameter references 222
Property references 224
Class reference 227
Serialization example 236
Referencing types 242
Type reflection example: Random value 246
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
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() }
// ...
}
CONTENTS 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.
CONTENTS 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>
CONTENTS 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
CONTENTS 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
The below code will not compile due to the type mismatch.
Which lines will show compilation errors?
takeBoxOutInt(BoxOut<Nothing>())
Starting code:
Usage example:
val p1 = Printer<Number>()
val p2: Printer<Int> = p1
val p3: Printer<Double> = p1
val s1 = Sender<Any>()
val s2: Sender<Int> = s1
val s3: Sender<String> = s1
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
}
CONTENTS 46
// 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)
CONTENTS 47
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()
CONTENTS 48
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!
CONTENTS 49
// 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
CONTENTS 52
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 &&
CONTENTS 53
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() {}
}
CONTENTS 56
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
Exercise: ApplicationScope
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
CONTENTS 66
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
) {
CONTENTS 71
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.
CONTENTS 72
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
}
val a by Lateinit<Int>()
a = 1
println(a) // 1
val b by Lateinit<String>()
b = "ABC"
println(b) // ABC
val c by Lateinit<String>()
println(c) // IllegalStateException:
// Uninitialized lateinit property c
val a by Lateinit<Int?>()
a = 1
println(a) // 1
a = null
println(a) // null
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
CONTENTS 79
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) }
CONTENTS 86
// 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)
}
}
}
Starting code:
CONTENTS 89
fun main() {
name = "Martin" // Empty -> Martin
name = "Igor" // Martin -> Igor
name = "Igor" // Igor -> Igor
}
CONTENTS 90
// 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
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]
import kotlin.properties.Delegates.vetoable
emailRegex.matches(newEmail)
}
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
properties, including those that are not known in advance,
and display an appropriate view for each of them”. For such
a requirement, we need to represent an object using Map, then
we can use this map as a delegate in order to more easily use
properties we know we can expect.
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.
CONTENTS 104
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
Starting code:
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.
CONTENTS 115
@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
CONTENTS 116
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
runBlocking {
val result: String
CONTENTS 118
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
CONTENTS 130
.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"
}
CONTENTS 132
@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
CONTENTS 133
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.
CONTENTS 135
// 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
}
CONTENTS 140
// 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
}
CONTENTS 142
// 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() {
// ...
}
CONTENTS 145
// 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(
CONTENTS 146
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 ->
// ...
}
}
CONTENTS 152
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
package advanced.java
fun main() {
val money1 = Money.eur("10.00")
val money2 = Money.eur("29.99")
println(money1 + money2)
// Money(amount=39.99, currency=EUR)
package advanced.java;
import java.math.BigDecimal;
import java.util.List;
List<Money> moneyList =
List.of(money1, money2, money1);
System.out.println(MoneyUtils.plus(money1, money2));
// Money(amount=39.99, currency=EUR)
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:
CONTENTS 164
kotlinx-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
CONTENTS 166
// commonMain
expect class DateTime {
fun getHour(): Int
fun getMinute(): Int
fun getSecond(): Int
// ...
}
// jvmMain
actual typealias DateTime = LocalDateTime
// jsMain
import kotlin.js.Date
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.
CONTENTS 175
// 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
CONTENTS 176
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:
CONTENTS 177
// 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.
CONTENTS 178
// 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.
CONTENTS 185
// 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
Those are expected elements that you need to provide for each
platform:
Starting code and unit tests for this exercise can be found in
the MarcinMoskala/kmp-exercise project on GitHub. You can
clone this project and solve this exercise locally.
CONTENTS 187
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
CONTENTS 188
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 commonTest by getting
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()
CONTENTS 205
}, [])
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"))
}
}
CONTENTS 206
// ...
}
}
@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
CONTENTS 212
// * 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.
CONTENTS 214
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
}
CONTENTS 216
// ...
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.
CONTENTS 221
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:
CONTENTS 222
Parameter references
import kotlin.reflect.KCallable
import kotlin.reflect.KParameter
import kotlin.reflect.typeOf
fun sendEmail(
email: String,
title: String,
message: String = ""
) {
println(
CONTENTS 224
"""
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.
CONTENTS 229
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
CONTENTS 231
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
}
CONTENTS 235
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]
}
CONTENTS 236
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
CONTENTS 246
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,
CONTENTS 248
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,
)
CONTENTS 250
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
class FunctionCaller {
inline fun <reified T> setConstant(value: T) {
setConstant(typeOf<T>(), value)
}
Usage example:
fun main() {
val caller = FunctionCaller()
caller.setConstant("ABC")
caller.setConstant(123)
caller.setConstant(typeOf<Number>(), 3.14)
caller.call(::printStrIntNum)
// str: ABC, int: 123, num: 3.14
caller.call(::printWithOptionals)
// l: 999, s: ABC
}
@Target(AnnotationTarget.PROPERTY)
annotation class SerializationName(val name: String)
@Target(AnnotationTarget.PROPERTY)
annotation class SerializationIgnore
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
annotation class SerializationNameMapper(
val mapper: KClass<out NameMapper>
)
@Target(AnnotationTarget.CLASS)
CONTENTS 257
interface NameMapper {
fun map(name: String): String
}
Starting code:
Usage example:
@SerializationNameMapper(SnakeCaseName::class)
@SerializationIgnoreNulls
class Creature(
val name: String,
@SerializationName("att")
val attack: Int,
@SerializationName("def")
val defence: Int,
val traits: List<Trait>,
val elementCost: Map<Element, Int>,
@SerializationNameMapper(LowerCaseName::class)
val isSpecial: Boolean,
@SerializationIgnore
var used: Boolean = false,
val extraDetails: String? = null,
)
fun main() {
val creature = Creature(
name = "Cockatrice",
attack = 2,
defence = 4,
traits = listOf(Trait.FLYING),
elementCost = mapOf(
Element.ANY to 3,
Element.FOREST to 2
),
isSpecial = true,
)
println(serializeToJson(creature))
// {"att": 2, "def": 4,
// "element_cost": {"ANY": 3, "FOREST": 2},
// "isspecial": true, "name": "Cockatrice",
// "traits": ["FLYING"]}
}
@Target(AnnotationTarget.PROPERTY)
annotation class SerializationName(val name: String)
@Target(AnnotationTarget.PROPERTY)
annotation class SerializationIgnore
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
annotation class SerializationNameMapper(
val mapper: KClass<out NameMapper>
)
@Target(AnnotationTarget.CLASS)
annotation class SerializationIgnoreNulls
interface NameMapper {
fun map(name: String): String
}
Starting code:
CONTENTS 260
fun main() {
data class SampleDataClass(
val externalTxnId: String,
val merchantTxnId: String,
val reference: String
)
println(serializeToXml(data))
// <SampleDataClass>
// <externalTxnId>07026984141550752666<externalTxnId>
// <merchantTxnId>07026984141550752666<merchantTxnId>
// <reference>MERCHPAY<reference>
// </SampleDataClass>
@SerializationNameMapper(UpperSnakeCaseName::class)
@SerializationIgnoreNulls
class Book(
val title: String,
val author: String,
@SerializationName("YEAR")
val publicationYear: Int,
val isbn: String?,
@SerializationIgnore
val price: Double,
)
@SerializationNameMapper(UpperSnakeCaseName::class)
class Library(
CONTENTS 261
println(serializeToXml(library))
// <LIBRARY>
// <CATALOG>
// <BOOK>
// <AUTHOR>J. R. R. Tolkien<AUTHOR>
// <ISBN>978-0-261-10235-4<ISBN>
// <YEAR>1937<YEAR>
// <TITLE>The Hobbit<TITLE>
// </BOOK>
// <BOOK>
CONTENTS 262
// <AUTHOR>Andrzej Sapkowski<AUTHOR>
// <ISBN>978-0-575-09404-2<ISBN>
// <YEAR>1993<YEAR>
// <TITLE>The Witcher<TITLE>
// </BOOK>
// <BOOK>
// <AUTHOR>Nassim Nicholas Taleb<AUTHOR>
// <YEAR>2012<YEAR>
// <TITLE>Antifragile<TITLE>
// </BOOK>
// <CATALOG>
// </LIBRARY>
}
interface UserRepository {
fun get(): String
}
class RealUserRepository(
private val userConfiguration: UserConfiguration,
) : UserRepository {
override fun get(): String =
"User from ${userConfiguration.url}"
}
class UserService(
private val userRepository: UserRepository,
private val userConfiguration: UserConfiguration,
) {
CONTENTS 264
fun main() {
val registry: Registry = registry {
singleton<UserConfiguration> {
UserConfiguration("http://localhost:8080")
}
normal<UserService> {
UserService(
userRepository = get(),
userConfiguration = get(),
)
}
singleton<UserRepository> {
RealUserRepository(
userConfiguration = get(),
)
}
}
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)
}
CONTENTS 293
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:
CONTENTS 295
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
) {
CONTENTS 303
// ...
}
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
}
CONTENTS 306
Summary
• https://github.com/MarcinMoskala/generateinterface-
ksp - generates interfaces for classes
• https://github.com/MarcinMoskala/DependencyInjection-
KSP - generates class for simple dependency injection
CONTENTS 308
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.
CONTENTS 310
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.
CONTENTS 311
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
CONTENTS 316
// 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
CONTENTS 321
Summary
As the name suggests, static analysers are tools that spot bugs
and recurrent patterns by analysing your code statically, i.e.,
without running it⁴⁸.
Running your code is generally considered an expensive op-
eration. Think about Kotlin/JVM, where you need to start a
Java Virtual Machine, or Android, where you need an emu-
lator or a physical device to run your code. One technique
that can be used to protect your code from bugs is writing
tests! They’re a great tool to prevent bugs from reaching
production, but they require a runtime and are an expensive
operation that becomes more and more expensive as your
codebase grows.
On the other hand, static analysers look at your code without
executing it at all, so there is no need to start a runtime
environment at all.
For example, there is no need to execute your code to warn you
that you’ve declared a variable that you’re not using. A static
analyser can keep track of all the variables you’ve declared
and those you access. The ones you declare but never access
⁴⁸A common example of static analysers is spellcheckers.
You probably use one daily when you write documents or
emails. Similarly, a compiler also is a form of static analyser
as it exposes warnings while it compiles your code.
CONTENTS 324
in the 70s⁵¹. Lint itself takes its name from the tiny bits of
fabric that appear on your clothes as you wear them. A linter
should inspect your codebase and capture all these small
imperfections and potential problems.
A real-world linter. Similarly, a static analyser inspects your codebase and cap-
tures 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/
Types of analysers
Formatters
like Prettier for Web, the various *fmt tools like gofmt for Go,
and rustfmt for Rust.
Kotlin has various formatters to pick from:
IntelliJ’s built-in formatter
ktlint
ktfmt
diktat
Enforcing coding conventions and reducing bikeshedding is a
common challenge for large engineering teams.
You probably don’t want to manually read every line of code
change to evaluate whether it conforms to the common pat-
terns. You may also not want to waste a significant amount of
time with your colleagues arguing over whether curly braces
should be put on a newline or not.
Formatters increase productivity by allowing you to have
such discussions once during the initial configuration and
objectively apply your preferred style to every code change.
Data-Flow Analysers
Reading this code, we know that the type of answer inside the
if block is smart-casted to String and is not a String? anymore.
The != null check is effectively a type restriction on the set
CONTENTS 329
Code Manipulation
All the previously mentioned tools inspect your code and raise
warnings whenever they discover something untoward.
However, some tools can go one step further and manipulate
your code if they find a violation.
Formatters manipulate your code as they usually provide a
mechanism to “reformat” your codebase so that you don’t
have to edit the code manually.
Code quality analysers can also manipulate your code, but
this isn’t always the case with formatters. For example, if
an analyser discovers an unused variable, the manipulation
would be to entirely remove the line where this variable is
declared.
However, this is often considered an invasive operation as
analysers generally need to perform substantial modification
⁵⁴An interesting read is the Control- and data-flow analysis
chapter of the Kotlin language spec.
CONTENTS 330
Embedded vs Standalone
Kotlin Compiler
IntelliJ IDEA
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
The 200 inspections that detekt offers out of the box are called
rules. Remembering all of them is hard, which is why they’re
organized in rulesets, collections of rules that serve the same
purposes.
Let’s take a brief look at them:
Configuring detekt
$ ./gradlew detektGenerateConfig
This will create a config file at the path shown in the console
by copying the default detekt config file. The configuration
file looks as follows:
CONTENTS 337
...
comments:
active: true
AbsentOrWrongFileLicense:
active: false
licenseTemplateFile: 'license.template'
licenseTemplateIsRegex: false
style:
active: true
MagicNumber:
ignorePropertyDeclaration: true
ignoreAnnotation: true
ignoreEnums: true
ignoreNumbers:
- '-1'
- '0'
- '1'
...
Rules are grouped by ruleset, and you can toggle each rule via
the active key. For instance, the AbsentOrWrongFileLicense
rule is disabled by default as you need to provide a
licenseTemplateFile to enable it.
Incremental Adoption
When running detekt on a big codebase for the first time, you
could be overwhelmed by the number of findings that detekt
reports. Fixing them all at once could be unfeasible, so you
should probably take an incremental approach to adopting
detekt.
You could use the config file to disable some rules. This has the
side effect of also turning off the inspection entirely for newer
code added to your codebase.
A smarter approach is to use a baseline, which is a snapshot
of a detekt run that can be used to suppress a group of inspec-
tions for future runs of detekt. Using a baseline is a two-step
process:
CONTENTS 338
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ImplicitUnitReturnType:HelloWorld.kt$Hello$fun
aFunctionWithImplicitUnitReturnType()</ID>
</CurrentIssues>
</SmellBaseline>
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.
src/main/kotlin/org/example/detekt/MyRuleSetProvider.kt - the
code of your ruleset. In order to be used, your rule needs to
live inside a ruleset, which allows you to add multiple custom
rules and distribute all of them together.
src/main/resources/config/config.yml - the default config file
for your rule. This is used to offer the default configuration
for your rules.
Please note that detekt uses the Java Service Provider API,
so the file inside src/main/resources/META-INF/services is also
needed to properly discover your ruleset. The template also
comes with two tests that can help you write your rule.
fun main() {
// Non compliant
System.out.print("Hello")
// Compliant
println("World!")
}
@KotlinCoreEnvironmentTest
internal class MyRuleTest(
private val env: KotlinCoreEnvironment
) {
@Test
fun `reports usages of System_out_println`() {
val code = """
fun main() {
System.out.println("Hello")
}
""".trimIndent()
@Test
fun `does not report usages Kotlin's println`() {
val code = """
fun main() {
println("Hello")
}
""".trimIndent()
Now that you’ve written and tested your rule, the last part is
distributing it and letting others use it.
Your rule should be published to a Maven Repository⁵⁸ and
consumed like any other dependency in a Gradle project,
but you’ll be using a detektPlugin dependency instead of an
implementation dependency.
plugins {
kotlin("jvm") version "..."
id("io.gitlab.arturbosch.detekt") version "..."
}
dependencies {
detektPlugin("org.example:detekt-custom-rule:...")
}
Users will then have to activate your rule in their config file:
...
MyRuleSet:
MyRule:
active: true
...
And they’ll start seeing findings when they run detekt nor-
mally:
$ ./gradlew build
[...]
BUILD FAILED in 470ms
Conclusion
Ending
Exercise solutions
Solution: ApplicationScope
class ApplicationScope(
private val scope: CoroutineScope,
private val applicationScope: ApplicationControlScope,
private val loggingScope: LoggingScope,
) : CoroutineScope by scope,
ApplicationControlScope by applicationScope,
LoggingScope by loggingScope
companion object {
val NOT_INITIALIZED = Any()
}
}
thisRef: Any?,
property: KProperty<*>,
value: T
) {
this.value = value
this.initializer = null
}
}
@OptIn(ExperimentalContracts::class)
suspend fun measureCoroutine(
body: suspend () -> Unit
): Duration {
contract {
callsInPlace(body, InvocationKind.EXACTLY_ONCE)
}
val dispatcher = coroutineContext[ContinuationInterceptor]
return if (dispatcher is TestDispatcher) {
val before = dispatcher.scheduler.currentTime
body()
val after = dispatcher.scheduler.currentTime
after - before
} else {
measureTimeMillis {
body()
}
}.milliseconds
}
CONTENTS 355
@file:JvmName("MoneyUtils")
package advanced.java
import java.math.BigDecimal
@JvmStatic
fun usd(amount: String) =
Money(BigDecimal(amount), Currency.USD)
@JvmField
val ZERO_EUR = eur("0.00")
}
}
@JvmName("sumMoney")
fun List<Money>.sum(): Money? {
if (isEmpty()) return null
val currency = this.map { it.currency }.toSet().single()
return Money(
amount = sumOf { it.amount },
currency = currency
)
}
Kotlin/JVM code:
Kotlin/JS code:
import kotlin.js.Date
plugins {
kotlin("multiplatform") version "1.8.0"
application
}
group = "org.example"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
kotlin {
jvm {
withJava()
}
js(IR) {
moduleName = "sudoku-generator"
browser()
binaries.library()
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(
"org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
CONTENTS 358
}
}
val jvmMain by getting
val jvmTest by getting
val jsMain by getting
val jsTest by getting
}
}
@file:OptIn(ExperimentalJsExport::class)
import generator.SudokuGenerator
import solver.SudokuSolver
@JsExport
@JsName("SudokuSolver")
class SudokuSolverJs {
private val generator = SudokuGenerator()
private val solver = SudokuSolver()
@JsExport
class Sudoku(
val solved: Array<Array<Int?>>,
CONTENTS 359
class FunctionCaller {
private var values: MutableMap<KType, Any?> =
mutableMapOf()
}
return function.callBy(args)
}
}
return reference
.memberProperties
.filterNot { it.hasAnnotation<SerializationIgnore>()}
.mapNotNull { prop ->
val annotationName = prop
.findAnnotation<SerializationName>()
val mapper = prop
.findAnnotation<SerializationNameMapper>()
?.let(::createMapper)
val name = annotationName?.name
?: mapper?.map(prop.name)
?: classNameMapper?.map(prop.name)
?: prop.name
val value = prop.call(any)
if (ignoreNulls && value == null) {
return@mapNotNull null
}
"\"${name}\": ${valueToJson(value)}"
}
.joinToString(
prefix = "{",
postfix = "}",
CONTENTS 361
)
}
.find { it.parameters.isEmpty() }
?.call()
return reference
.memberProperties
.filterNot { it.hasAnnotation<SerializationIgnore>()}
.mapNotNull { prop ->
val annotationName = prop
.findAnnotation<SerializationName>()
val mapper = prop
.findAnnotation<SerializationNameMapper>()
?.let(::createMapper)
val name = annotationName?.name
?: mapper?.map(prop.name)
?: classNameMapper?.map(prop.name)
?: prop.name
val value = prop.call(any)
if (ignoreNulls && value == null) {
return@mapNotNull null
}
"<$name>${valueToXml(value)}</$name>"
}
.joinToString(
separator = "",
CONTENTS 363
prefix = "<$className>",
postfix = "</$className>",
)
}
.find { it.parameters.isEmpty() }
?.call()
class Registry {
private val creatorsRegistry =
mutableMapOf<KType, () -> Any?>()
private val instances =
mutableMapOf<KType, Any?>()