Kotlin Reference
Kotlin Reference
0
Table of Contents
Kotlin Docs 68
Install Kotlin 68
Is anything missing? 69
Hello world 70
Variables 70
String templates 71
Practice 71
Next step 71
Basic types 71
Practice 73
Next step 73
Collections 73
List 74
Set 75
Map 76
Practice 78
Next step 79
Control flow 79
Conditional expressions 79
Ranges 82
Loops 82
2
Loops practice 84
Next step 85
Functions 85
Named arguments 86
Single-expression functions 87
Functions practice 88
Lambda expressions 89
Next step 93
Classes 93
Properties 93
Create instance 94
Access properties 94
Member functions 95
Data classes 95
Practice 97
Next step 98
Null safety 98
Nullable types 99
Practice 100
3
Extension functions 101
Practice 103
Practice 109
Practice 112
Interfaces 117
Delegation 118
Practice 119
Practice 126
Practice 133
4
Intermediate: Properties 135
Practice 141
Practice 148
Practice 154
Kotlin/Wasm 158
5
Browser API support 160
Kotlin/Native 161
Interoperability 161
Notebooks 164
Kandy 166
6
What's new in Kotlin 2.2.0 172
Language 172
Kotlin/JVM 178
Kotlin/Native 181
Kotlin/Wasm 182
Kotlin/JS 184
Gradle 185
Language 193
Kotlin/JVM 200
Kotlin/Native 205
Kotlin/Wasm 205
Kotlin/JS 211
7
Install Kotlin 2.1.0 218
Kotlin/Native 219
Kotlin/Wasm 220
Gradle 223
Language 228
Kotlin/Native 232
Kotlin/Wasm 233
Kotlin/JS 234
Gradle 235
Kotlin/JVM 251
8
Kotlin/Native 251
Kotlin/Wasm 252
Kotlin/JS 254
Kotlin/JVM 269
Kotlin/Native 269
Kotlin/Wasm 280
Gradle 282
Language 289
Kotlin/JVM 290
Kotlin/Native 290
Kotlin/Wasm 294
Kotlin/JS 295
Gradle 297
9
Install Kotlin 1.9.0 304
Language 307
Kotlin/JVM 312
Kotlin/Native 313
Kotlin/JavaScript 318
Gradle 319
Kotlin/JVM 326
Kotlin/Native 327
Kotlin/JS 330
Gradle 332
10
Language 339
Kotlin/JVM 344
Kotlin/Native 346
Kotlin/JS 347
Gradle 348
Language 353
Kotlin/JVM 354
Kotlin/Native 355
Kotlin/JS 357
Gradle 361
Language 367
Kotlin/JVM 369
Kotlin/Native 370
Kotlin/JS 375
Security 377
Gradle 378
Language 380
11
Kotlin/JVM 382
Kotlin/Native 383
Kotlin/JS 385
Tools 390
Kotlin/JVM 395
Kotlin/Native 396
Kotlin/JS 400
Gradle 400
Kotlin/JVM 406
Kotlin/Native 407
Kotlin/JS 408
Gradle 408
Kotlin/JVM 413
Kotlin/Native 415
Kotlin/JS 416
12
Standard library 416
Kotlin/JVM 427
Kotlin/Native 428
Kotlin/JS 428
Kotlin/JVM 431
Kotlin/JS 431
Kotlin/Native 433
Kotlin/JVM 445
Kotlin/JS 447
Kotlin/Native 448
13
Standard library 454
Kotlin/Native 461
Contracts 461
@JvmDefault 465
Tooling 467
Tools 473
14
What's new in Kotlin 1.1 473
JavaScript 474
Libraries 495
15
Update to a new Kotlin version 500
Functions 511
Variables 512
Comments 513
Ranges 515
Collections 515
Idioms 518
16
Instance checks 519
if expression 522
17
Coding conventions 524
Configure style in IDE 524
Formatting 527
Numbers 540
Booleans 546
Characters 547
Strings 548
Arrays 551
18
When to use arrays 551
If expression 560
Exceptions 566
19
Packages and imports 576
Imports 577
Classes 577
Constructors 577
Inheritance 580
Inheritance 580
Properties 583
Interfaces 586
20
Resolving overriding conflicts 587
Packages 590
Modules 591
Extensions 592
Copying 596
Inheritance 599
21
Use case scenarios 600
Variance 602
Members 612
Inheritance 612
Representation 612
Delegation 621
22
Delegated properties 623
Functions 631
noinline 641
23
Infix calls for named functions 649
Restrictions 658
Equality 664
24
Qualified this 666
Threading 667
Callbacks 668
Coroutines 669
Coroutines 669
Annotations 671
Usage 671
Constructors 672
Instantiation 672
Lambdas 672
Reflection 679
25
Get started with Kotlin Notebook 683
Add Kotlin DataFrame and Kandy libraries to your Kotlin Notebook 689
Texts 694
HTML 696
Images 696
Charts 699
26
Refine data 702
27
Run the application 720
Operators 733
Properties 736
28
Instance fields 737
Visibility 740
KClass 740
Null-safety 742
29
Update the MessageController class 756
Build a Kotlin app that uses Spring AI to answer questions based on documents stored in 767
Before
Qdrant you start
— tutorial 767
30
Using Java records from Kotlin code 784
Mutability 793
Covariance 794
Sequences 796
Get the first and the last items of a possibly empty collection 797
31
Group elements 798
Filter elements 799
In IDE 807
Properties 815
32
Setting up your project 819
Bindings 819
33
Receive C string bytes from Kotlin 838
Mappings 852
Subclassing 860
C features 861
Unsupported 861
34
Use code from Objective-C 866
Threads 876
35
What's next 881
Breakpoints 882
Stepping 883
Tier 1 886
Tier 2 886
Tier 3 887
36
How do I enable bitcode for my Kotlin framework? 893
37
Web-related browser APIs 924
Troubleshooting 924
Dependencies 928
CSS 932
Node.js 934
Yarn 935
38
If you run into any problems 947
Convert JS- and React-related classes and interfaces to external interfaces 955
Additional troubleshooting tips when working with the Kotlin/JS IR compiler 957
Equality 961
39
Use dependencies from npm 962
Example 970
40
Construct from elements 984
Copy 986
Iterators 987
Ranges 988
Progression 989
Sequences 990
Construct 990
Map 994
Zip 995
Associate 996
Flatten 996
41
Filtering collections 997
Partition 998
Grouping 999
Slice 1000
Chunked 1001
Windowed 1002
Ordering 1004
42
Updating elements 1010
Filter 1015
Distinctions 1025
Functions 1028
43
Coroutines guide 1037
Callbacks 1045
Coroutines 1050
Concurrency 1052
Channels 1061
44
Timeout
1070
Asynchronous timeout and resources 1071
Buffering 1092
45
Composing multiple flows 1095
Channels 1105
Pipelines 1107
Fan-out 1109
Fan-in 1110
CoroutineExceptionHandler 1113
Supervision 1116
46
Thread confinement fine-grained 1120
Serialization 1137
Libraries 1137
Formats 1137
47
Lincheck guide 1146
48
Keywords and operators 1161
Hard keywords 1161
Gradle 1165
Organize 1187
Optimize 1187
49
Target the JVM 1195
Troubleshooting 1212
Maven 1213
50
Set dependencies 1214
Ant 1220
Targeting JavaScript with single source folder and metaInfo option 1222
References 1222
Introduction 1223
Community 1224
Gradle 1225
51
Generate documentation 1226
Troubleshooting 1255
Maven 1256
CLI 1264
HTML 1276
Configuration 1276
Customization 1279
Markdown 1281
GFM 1281
Jekyll 1282
52
Javadoc 1283
Eclipse 1290
Differences between "Kotlin coding conventions" and "IntelliJ IDEA default code style" 1291
Prototyping 1295
53
What's next 1299
Maven 1315
Gradle 1315
FAQ 1316
54
Support in IDEs 1324
Changes 1325
Gradle 1348
Maven 1349
Gradle 1351
55
Maven 1351
Gradle 1353
Maven 1353
Gradle 1361
Maven 1362
56
Command-line compiler 1363
Overview 1370
Resources 1372
57
Comparison to kapt 1379
Limitations 1380
Find the actual class or interface declaration that the type alias points to 1380
Types 1382
Misc 1383
Details 1384
Example 1 1392
Example 2 1392
Advanced 1394
58
Running KSP from command line 1395
Can I use a newer KSP implementation with an older Kotlin compiler? 1397
Besides Kotlin, are there other version requirements for libraries? 1397
Building Reactive Spring Boot applications with Kotlin coroutines and RSocket 1399
59
Return and throw with the Elvis operator 1402
Strings 1406
Simplicity 1417
60
Readability 1418
Consistency 1421
Predictability 1422
Debuggability 1424
Testability 1427
61
Compatibility types 1427
How the EAP can help you be more productive with Kotlin 1436
62
How the EAP can help you be more productive with Kotlin 1436
FAQ 1438
What advantages does Kotlin give me over the Java programming language? 1439
63
Are there Kotlin events? 1441
Language 1442
Tools 1446
Language 1453
Tools 1460
Language 1463
Tools 1474
Language 1477
Tools 1485
Language 1487
64
Tools 1496
Language 1499
Language 1500
Tools 1506
Language 1509
Tools 1517
Tools 1527
Tools 1542
65
GSoC 2025: project ideas 1551
Security 1565
Contribution 1565
66
JetBrains support 1568
Moderators 1570
Copyright 1571
67
Kotlin Docs
Get started with Kotlin
Kotlin is a modern but already mature programming language designed to make developers happier. It's concise, safe, interoperable with Java and other
languages, and provides many ways to reuse code between multiple platforms for productive programming.
To start, why not take our tour of Kotlin? This tour covers the fundamentals of the Kotlin programming language and can be completed entirely within your browser.
Install Kotlin
Kotlin is included in each IntelliJ IDEA and Android Studio release. Download and install one of these IDEs to start using Kotlin.
Console
Here you'll learn how to develop a console application and create unit tests with Kotlin.
1. Create a basic JVM application with the IntelliJ IDEA project wizard.
Backend
Here you'll learn how to develop a backend application with Kotlin server-side.
Cross-platform
Here you'll learn how to develop a cross-platform application using Kotlin Multiplatform.
Android
To start using Kotlin for Android development, read Google's recommendation for getting started with Kotlin on Android.
Data
analysis
From building data pipelines to productionizing machine learning models, Kotlin is a great choice for working with data and getting the most out of it.
68
DataFrame – a library for data analysis and manipulation.
Join us on:
If you encounter any difficulties or problems, report an issue in our issue tracker.
Is anything missing?
If anything is missing or seems confusing on this page, please share your feedback.
These tours can be completed entirely within your browser. There is no installation required.
Quickly learn the essentials of the Kotlin programming language through our tours, which will take you from beginner to intermediate level. Each chapter contains:
Practice with exercises to test your understanding of what you have learned.
Basic types
Collections
Control flow
Functions
Classes
Null safety
If you're ready to take your understanding of Kotlin to the next level, take our intermediate tour:
69
Intermediate tour contents
Extension functions
Scope functions
Objects
Properties
Null safety
Hello world
Here is a simple program that prints "Hello, world!":
fun main() {
println("Hello, world!")
// Hello, world!
}
In Kotlin:
A function is a set of instructions that performs a specific task. Once you create a function, you can use it whenever you need to perform that task, without having
to write the instructions all over again. Functions are discussed in more detail in a couple of chapters. Until then, all examples use the main() function.
Variables
All programs need to be able to store data, and variables help you to do just that. In Kotlin, you can declare:
You can't change a read-only variable once you have given it a value.
For example:
fun main() {
val popcorn = 5 // There are 5 boxes of popcorn
val hotdog = 7 // There are 7 hotdogs
var customers = 10 // There are 10 customers in the queue
70
Variables can be declared outside the main() function at the beginning of your program. Variables declared in this way are said to be declared at top level.
We recommend that you declare all variables as read-only (val) by default. Declare mutable variables (var) only if necessary.
String templates
It's useful to know how to print the contents of variables to standard output. You can do this with string templates. You can use template expressions to access
data stored in variables and other objects, and convert them into strings. A string value is a sequence of characters in double quotes ". Template expressions
always start with a dollar sign $.
To evaluate a piece of code in a template expression, place the code within curly braces {} after the dollar sign $.
For example:
fun main() {
val customers = 10
println("There are $customers customers")
// There are 10 customers
You will notice that there aren't any types declared for variables. Kotlin has inferred the type itself: Int. This tour explains the different Kotlin basic types and how to
declare them in the next chapter.
Practice
Exercise
Complete the code to make the program print "Mary is 20 years old" to standard output:
fun main() {
val name = "Mary"
val age = 20
// Write your code here
}
fun main() {
val name = "Mary"
val age = 20
println("$name is $age years old")
}
Next step
Basic types
Basic types
Every variable and data structure in Kotlin has a type. Types are important because they tell the compiler what you are allowed to do with that variable or data
71
structure. In other words, what functions and properties it has.
In the last chapter, Kotlin was able to tell in the previous example that customers has type Int. Kotlin's ability to infer the type is called type inference. customers is
assigned an integer value. From this, Kotlin infers that customers has a numerical type Int. As a result, the compiler knows that you can perform arithmetic
operations with customers:
fun main() {
var customers = 10
println(customers) // 10
}
+=, -=, *=, /=, and %= are augmented assignment operators. For more information, see Augmented assignments.
Unsigned integers UByte, UShort, UInt, ULong val score: UInt = 100u
Floating-point numbers Float, Double val currentTemp: Float = 24.5f, val price: Double = 19.99
For more information on basic types and their properties, see Basic types.
With this knowledge, you can declare variables and initialize them later. Kotlin can manage this as long as variables are initialized before the first read.
To declare a variable without initializing it, specify its type with :. For example:
fun main() {
// Variable declared without initialization
val d: Int
// Variable initialized
d = 3
72
If you don't initialize a variable before it is read, you see an error:
fun main() {
// Variable declared without initialization
val d: Int
// Triggers an error
println(d)
// Variable 'd' must be initialized
}
Now that you know how to declare basic types, it's time to learn about collections.
Practice
Exercise
Explicitly declare the correct type for each variable:
fun main() {
val a: Int = 1000
val b = "log message"
val c = 3.14
val d = 100_000_000_000_000
val e = false
val f = '\n'
}
fun main() {
val a: Int = 1000
val b: String = "log message"
val c: Double = 3.14
val d: Long = 100_000_000_000_000
val e: Boolean = false
val f: Char = '\n'
}
Next step
Collections
Collections
When programming, it is useful to be able to group data into structures for later processing. Kotlin provides collections for exactly this purpose.
Maps Sets of key-value pairs where keys are unique and map to only one value
73
Each collection type can be mutable or read only.
List
Lists store items in the order that they are added, and allow for duplicate items.
When creating lists, Kotlin can infer the type of items stored. To declare the type explicitly, add the type within angled brackets <> after the list declaration:
fun main() {
// Read only list
val readOnlyShapes = listOf("triangle", "square", "circle")
println(readOnlyShapes)
// [triangle, square, circle]
To prevent unwanted modifications, you can create a read-only view of a mutable list by assigning it to a List:
Lists are ordered so to access an item in a list, use the indexed access operator []:
fun main() {
val readOnlyShapes = listOf("triangle", "square", "circle")
println("The first item in the list is: ${readOnlyShapes[0]}")
// The first item in the list is: triangle
}
To get the first or last item in a list, use .first() and .last() functions respectively:
fun main() {
val readOnlyShapes = listOf("triangle", "square", "circle")
println("The first item in the list is: ${readOnlyShapes.first()}")
// The first item in the list is: triangle
}
.first() and .last() functions are examples of extension functions. To call an extension function on an object, write the function name after the object
appended with a period .
Extension functions are covered in detail in the intermediate tour. For now, you only need to know how to call them.
fun main() {
val readOnlyShapes = listOf("triangle", "square", "circle")
println("This list has ${readOnlyShapes.count()} items")
// This list has 3 items
}
74
fun main() {
val readOnlyShapes = listOf("triangle", "square", "circle")
println("circle" in readOnlyShapes)
// true
}
To add or remove items from a mutable list, use .add() and .remove() functions respectively:
fun main() {
val shapes: MutableList<String> = mutableListOf("triangle", "square", "circle")
// Add "pentagon" to the list
shapes.add("pentagon")
println(shapes)
// [triangle, square, circle, pentagon]
Set
Whereas lists are ordered and allow duplicate items, sets are unordered and only store unique items.
When creating sets, Kotlin can infer the type of items stored. To declare the type explicitly, add the type within angled brackets <> after the set declaration:
fun main() {
// Read-only set
val readOnlyFruit = setOf("apple", "banana", "cherry", "cherry")
// Mutable set with explicit type declaration
val fruit: MutableSet<String> = mutableSetOf("apple", "banana", "cherry", "cherry")
println(readOnlyFruit)
// [apple, banana, cherry]
}
You can see in the previous example that because sets only contain unique elements, the duplicate "cherry" item is dropped.
To prevent unwanted modifications, you can create a read-only view of a mutable set by assigning it to a Set:
fun main() {
val readOnlyFruit = setOf("apple", "banana", "cherry", "cherry")
println("This set has ${readOnlyFruit.count()} items")
// This set has 3 items
}
fun main() {
val readOnlyFruit = setOf("apple", "banana", "cherry", "cherry")
println("banana" in readOnlyFruit)
75
// true
}
To add or remove items from a mutable set, use .add() and .remove() functions respectively:
fun main() {
val fruit: MutableSet<String> = mutableSetOf("apple", "banana", "cherry", "cherry")
fruit.add("dragonfruit") // Add "dragonfruit" to the set
println(fruit) // [apple, banana, cherry, dragonfruit]
Map
Maps store items as key-value pairs. You access the value by referencing the key. You can imagine a map like a food menu. You can find the price (value), by
finding the food (key) you want to eat. Maps are useful if you want to look up a value without using a numbered index, like in a list.
Every key in a map must be unique so that Kotlin can understand which value you want to get.
When creating maps, Kotlin can infer the type of items stored. To declare the type explicitly, add the types of the keys and values within angled brackets <> after
the map declaration. For example: MutableMap<String, Int>. The keys have type String and the values have type Int.
The easiest way to create maps is to use to between each key and its related value:
fun main() {
// Read-only map
val readOnlyJuiceMenu = mapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
println(readOnlyJuiceMenu)
// {apple=100, kiwi=190, orange=100}
To prevent unwanted modifications, you can create a read-only view of a mutable map by assigning it to a Map:
val juiceMenu: MutableMap<String, Int> = mutableMapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
val juiceMenuLocked: Map<String, Int> = juiceMenu
To access a value in a map, use the indexed access operator [] with its key:
fun main() {
// Read-only map
val readOnlyJuiceMenu = mapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
println("The value of apple juice is: ${readOnlyJuiceMenu["apple"]}")
// The value of apple juice is: 100
}
76
If you try to access a key-value pair with a key that doesn't exist in a map, you see a null value:
fun main() {
// Read-only map
val readOnlyJuiceMenu = mapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
println("The value of pineapple juice is: ${readOnlyJuiceMenu["pineapple"]}")
// The value of pineapple juice is: null
}
This tour explains null values later in the Null safety chapter.
You can also use the indexed access operator [] to add items to a mutable map:
fun main() {
val juiceMenu: MutableMap<String, Int> = mutableMapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
juiceMenu["coconut"] = 150 // Add key "coconut" with value 150 to the map
println(juiceMenu)
// {apple=100, kiwi=190, orange=100, coconut=150}
}
fun main() {
val juiceMenu: MutableMap<String, Int> = mutableMapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
juiceMenu.remove("orange") // Remove key "orange" from the map
println(juiceMenu)
// {apple=100, kiwi=190}
}
fun main() {
// Read-only map
val readOnlyJuiceMenu = mapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
println("This map has ${readOnlyJuiceMenu.count()} key-value pairs")
// This map has 3 key-value pairs
}
To check if a specific key is already included in a map, use the .containsKey() function:
fun main() {
val readOnlyJuiceMenu = mapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
println(readOnlyJuiceMenu.containsKey("kiwi"))
// true
}
To obtain a collection of the keys or values of a map, use the keys and values properties respectively:
fun main() {
val readOnlyJuiceMenu = mapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
println(readOnlyJuiceMenu.keys)
// [apple, kiwi, orange]
println(readOnlyJuiceMenu.values)
// [100, 190, 100]
}
keys and values are examples of properties of an object. To access the property of an object, write the property name after the object appended with a
period .
Properties are discussed in more detail in the Classes chapter. At this point in the tour, you only need to know how to access them.
fun main() {
77
val readOnlyJuiceMenu = mapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
println("orange" in readOnlyJuiceMenu.keys)
// true
println(200 in readOnlyJuiceMenu.values)
// false
}
For more information on what you can do with collections, see Collections.
Now that you know about basic types and how to manage collections, it's time to explore the control flow that you can use in your programs.
Practice
Exercise 1
You have a list of “green” numbers and a list of “red” numbers. Complete the code to print how many numbers there are in total.
fun main() {
val greenNumbers = listOf(1, 4, 23)
val redNumbers = listOf(17, 2)
// Write your code here
}
fun main() {
val greenNumbers = listOf(1, 4, 23)
val redNumbers = listOf(17, 2)
val totalCount = greenNumbers.count() + redNumbers.count()
println(totalCount)
}
Exercise 2
You have a set of protocols supported by your server. A user requests to use a particular protocol. Complete the program to check whether the requested protocol
is supported or not (isSupported must be a Boolean value).
fun main() {
val SUPPORTED = setOf("HTTP", "HTTPS", "FTP")
val requested = "smtp"
val isSupported = // Write your code here
println("Support for $requested: $isSupported")
}
Hint
Make sure that you check the requested protocol in upper case. You can use the .uppercase() function to help you with this.
fun main() {
val SUPPORTED = setOf("HTTP", "HTTPS", "FTP")
val requested = "smtp"
val isSupported = requested.uppercase() in SUPPORTED
println("Support for $requested: $isSupported")
}
Exercise 3
Define a map that relates integer numbers from 1 to 3 to their corresponding spelling. Use this map to spell the given number.
78
fun main() {
val number2word = // Write your code here
val n = 2
println("$n is spelt as '${<Write your code here >}'")
}
fun main() {
val number2word = mapOf(1 to "one", 2 to "two", 3 to "three")
val n = 2
println("$n is spelt as '${number2word[n]}'")
}
Next step
Control flow
Control flow
Like other programming languages, Kotlin is capable of making decisions based on whether a piece of code is evaluated to be true. Such pieces of code are called
conditional expressions. Kotlin is also able to create and iterate through loops.
Conditional expressions
Kotlin provides if and when for checking conditional expressions.
If you have to choose between if and when, we recommend using when because it:
If
To use if, add the conditional expression within parentheses () and the action to take if the result is true within curly braces {}:
fun main() {
val d: Int
val check = true
if (check) {
d = 1
} else {
d = 2
}
println(d)
// 1
}
There is no ternary operator condition ? then : else in Kotlin. Instead, if can be used as an expression. If there is only one line of code per action, the curly braces {}
are optional:
fun main() {
val a = 1
val b = 2
79
When
Use when when you have a conditional expression with multiple branches.
To use when:
Use -> in each branch to separate each check from the action to take if the check is successful.
when can be used either as a statement or as an expression. A statement doesn't return anything but performs actions instead.
fun main() {
val obj = "Hello"
when (obj) {
// Checks whether obj equals to "1"
"1" -> println("One")
// Checks whether obj equals to "Hello"
"Hello" -> println("Greeting")
// Default statement
else -> println("Unknown")
}
// Greeting
}
Note that all branch conditions are checked sequentially until one of them is satisfied. So only the first suitable branch is executed.
Here is an example of using when as an expression. The when expression is assigned immediately to a variable which is later used with the println() function:
fun main() {
//sampleStart
val obj = "Hello"
The examples of when that you've seen so far both had a subject: obj. But when can also be used without a subject.
This example uses a when expression without a subject to check a chain of Boolean expressions:
fun main() {
val trafficLightState = "Red" // This can be "Green", "Yellow", or "Red"
println(trafficAction)
// Stop
}
80
However, you can have the same code but with trafficLightState as the subject:
fun main() {
val trafficLightState = "Red" // This can be "Green", "Yellow", or "Red"
println(trafficAction)
// Stop
}
Using when with a subject makes your code easier to read and maintain. When you use a subject with a when expression, it also helps Kotlin check that all possible
cases are covered. Otherwise, if you don't use a subject with a when expression, you need to provide an else branch.
Exercise 1
Create a simple game where you win if throwing two dice results in the same number. Use if to print You win :) if the dice match or You lose :( otherwise.
In this exercise, you import a package so that you can use the Random.nextInt() function to give you a random Int. For more information about importing
packages, see Packages and imports.
Hint
Use the equality operator (==) to compare the dice results.
import kotlin.random.Random
fun main() {
val firstResult = Random.nextInt(6)
val secondResult = Random.nextInt(6)
// Write your code here
}
import kotlin.random.Random
fun main() {
val firstResult = Random.nextInt(6)
val secondResult = Random.nextInt(6)
if (firstResult == secondResult)
println("You win :)")
else
println("You lose :(")
}
Exercise 2
Using a when expression, update the following program so that it prints the corresponding actions when you input the names of game console buttons.
Button Action
A Yes
81
Button Action
B No
X Menu
Y Nothing
fun main() {
val button = "A"
println(
// Write your code here
)
}
fun main() {
val button = "A"
println(
when (button) {
"A" -> "Yes"
"B" -> "No"
"X" -> "Menu"
"Y" -> "Nothing"
else -> "There is no such button"
}
)
}
Ranges
Before talking about loops, it's useful to know how to construct ranges for loops to iterate over.
The most common way to create a range in Kotlin is to use the .. operator. For example, 1..4 is equivalent to 1, 2, 3, 4.
To declare a range that doesn't include the end value, use the ..< operator. For example, 1..<4 is equivalent to 1, 2, 3.
To declare a range in reverse order, use downTo. For example, 4 downTo 1 is equivalent to 4, 3, 2, 1.
To declare a range that increments in a step that isn't 1, use step and your desired increment value. For example, 1..5 step 2 is equivalent to 1, 3, 5.
Loops
The two most common loop structures in programming are for and while. Use for to iterate over a range of values and perform an action. Use while to continue an
action until a particular condition is satisfied.
For
Using your new knowledge of ranges, you can create a for loop that iterates over numbers 1 to 5 and prints the number each time.
82
Place the iterator and range within parentheses () with keyword in. Add the action you want to complete within curly braces {}:
fun main() {
for (number in 1..5) {
// number is the iterator and 1..5 is the range
print(number)
}
// 12345
}
fun main() {
val cakes = listOf("carrot", "cheese", "chocolate")
While
while can be used in two ways:
To execute the code block first and then check the conditional expression. (do-while)
Declare the conditional expression for your while loop to continue within parentheses ().
Add the action you want to complete within curly braces {}.
The following examples use the increment operator ++ to increment the value of the cakesEaten variable.
fun main() {
var cakesEaten = 0
while (cakesEaten < 3) {
println("Eat a cake")
cakesEaten++
}
// Eat a cake
// Eat a cake
// Eat a cake
}
Declare the conditional expression for your while loop to continue within parentheses ().
Define the action you want to complete within curly braces {} with the keyword do.
fun main() {
var cakesEaten = 0
var cakesBaked = 0
while (cakesEaten < 3) {
println("Eat a cake")
cakesEaten++
}
do {
println("Bake a cake")
cakesBaked++
} while (cakesBaked < cakesEaten)
// Eat a cake
83
// Eat a cake
// Eat a cake
// Bake a cake
// Bake a cake
// Bake a cake
}
For more information and examples of conditional expressions and loops, see Conditions and loops.
Now that you know the fundamentals of Kotlin control flow, it's time to learn how to write your own functions.
Loops practice
Exercise 1
You have a program that counts pizza slices until there’s a whole pizza with 8 slices. Refactor this program in two ways:
fun main() {
var pizzaSlices = 0
// Start refactoring here
pizzaSlices++
println("There's only $pizzaSlices slice/s of pizza :(")
pizzaSlices++
println("There's only $pizzaSlices slice/s of pizza :(")
pizzaSlices++
println("There's only $pizzaSlices slice/s of pizza :(")
pizzaSlices++
println("There's only $pizzaSlices slice/s of pizza :(")
pizzaSlices++
println("There's only $pizzaSlices slice/s of pizza :(")
pizzaSlices++
println("There's only $pizzaSlices slice/s of pizza :(")
pizzaSlices++
println("There's only $pizzaSlices slice/s of pizza :(")
pizzaSlices++
// End refactoring here
println("There are $pizzaSlices slices of pizza. Hooray! We have a whole pizza! :D")
}
fun main() {
var pizzaSlices = 0
while ( pizzaSlices < 7 ) {
pizzaSlices++
println("There's only $pizzaSlices slice/s of pizza :(")
}
pizzaSlices++
println("There are $pizzaSlices slices of pizza. Hooray! We have a whole pizza! :D")
}
fun main() {
var pizzaSlices = 0
pizzaSlices++
do {
println("There's only $pizzaSlices slice/s of pizza :(")
pizzaSlices++
} while ( pizzaSlices < 8 )
println("There are $pizzaSlices slices of pizza. Hooray! We have a whole pizza! :D")
}
Exercise 2
Write a program that simulates the Fizz buzz game. Your task is to print numbers from 1 to 100 incrementally, replacing any number divisible by three with the word
"fizz", and any number divisible by five with the word "buzz". Any number divisible by both 3 and 5 must be replaced with the word "fizzbuzz".
84
Hint 1
Use a for loop to count numbers and a when expression to decide what to print at each step.
Hint 2
Use the modulo operator (%) to return the remainder of a number being divided. Use the equality operator (==) to check if the remainder equals zero.
fun main() {
// Write your code here
}
fun main() {
for (number in 1..100) {
println(
when {
number % 15 == 0 -> "fizzbuzz"
number % 3 == 0 -> "fizz"
number % 5 == 0 -> "buzz"
else -> "$number"
}
)
}
}
Exercise 3
You have a list of words. Use for and if to print only the words that start with the letter l.
Hint
Use the .startsWith() function for String type.
fun main() {
val words = listOf("dinosaur", "limousine", "magazine", "language")
// Write your code here
}
fun main() {
val words = listOf("dinosaur", "limousine", "magazine", "language")
for (w in words) {
if (w.startsWith("l"))
println(w)
}
}
Next step
Functions
Functions
You can declare your own functions in Kotlin using the fun keyword.
fun hello() {
return println("Hello, world!")
}
fun main() {
hello()
// Hello, world!
}
In Kotlin:
85
Function parameters are written within parentheses ().
Each parameter must have a type, and multiple parameters must be separated by commas ,.
The return type is written after the function's parentheses (), separated by a colon :.
If a function doesn't return anything useful, the return type and return keyword can be omitted. Learn more about this in Functions without return.
fun main() {
println(sum(1, 2))
// 3
}
We recommend in our coding conventions that you name functions starting with a lowercase letter and use camel case with no underscores.
Named arguments
For concise code, when calling your function, you don't have to include parameter names. However, including parameter names does make your code easier to
read. This is called using named arguments. If you do include parameter names, then you can write the parameters in any order.
In the following example, string templates ($) are used to access the parameter values, convert them to String type, and then concatenate them into a
string for printing.
fun main() {
// Uses named arguments with swapped parameter order
printMessageWithPrefix(prefix = "Log", message = "Hello")
// [Log] Hello
}
86
fun main() {
// Function called with both parameters
printMessageWithPrefix("Hello", "Log")
// [Log] Hello
You can skip specific parameters with default values, rather than omitting them all. However, after the first skipped parameter, you must name all
subsequent parameters.
fun main() {
printMessage("Hello")
// Hello
}
Single-expression functions
To make your code more concise, you can use single-expression functions. For example, the sum() function can be shortened:
fun main() {
println(sum(1, 2))
// 3
}
You can remove the curly braces {} and declare the function body using the assignment operator =. When you use the assignment operator =, Kotlin uses type
inference, so you can also omit the return type. The sum() function then becomes one line:
fun main() {
println(sum(1, 2))
// 3
}
However, if you want your code to be quickly understood by other developers, it's a good idea to explicitly define the return type even when using the assignment
operator =.
If you use {} curly braces to declare your function body, you must declare the return type unless it is the Unit type.
87
the conditional expression is found to be true:
// Proceed with the registration if the username and email are not taken
registeredUsernames.add(username)
registeredEmails.add(email)
fun main() {
println(registerUser("john_doe", "[email protected]"))
// Username already taken. Please choose a different username.
println(registerUser("new_user", "[email protected]"))
// User registered successfully: new_user
}
Functions practice
Exercise 1
Write a function called circleArea that takes the radius of a circle in integer format as a parameter and outputs the area of that circle.
In this exercise, you import a package so that you can access the value of pi via PI. For more information about importing packages, see Packages and
imports.
import kotlin.math.PI
fun main() {
println(circleArea(2))
}
import kotlin.math.PI
fun main() {
println(circleArea(2)) // 12.566370614359172
}
Exercise 2
Rewrite the circleArea function from the previous exercise as a single-expression function.
88
import kotlin.math.PI
fun main() {
println(circleArea(2))
}
import kotlin.math.PI
fun main() {
println(circleArea(2)) // 12.566370614359172
}
Exercise 3
You have a function that translates a time interval given in hours, minutes, and seconds into seconds. In most cases, you need to pass only one or two function
parameters while the rest are equal to 0. Improve the function and the code that calls it by using default parameter values and named arguments so that the code is
easier to read.
fun main() {
println(intervalInSeconds(1, 20, 15))
println(intervalInSeconds(0, 1, 25))
println(intervalInSeconds(2, 0, 0))
println(intervalInSeconds(0, 10, 0))
println(intervalInSeconds(1, 0, 1))
}
fun main() {
println(intervalInSeconds(1, 20, 15))
println(intervalInSeconds(minutes = 1, seconds = 25))
println(intervalInSeconds(hours = 2))
println(intervalInSeconds(minutes = 10))
println(intervalInSeconds(hours = 1, seconds = 1))
}
Lambda expressions
Kotlin allows you to write even more concise code for functions by using lambda expressions.
fun main() {
val upperCaseString = { text: String -> text.uppercase() }
println(upperCaseString("hello"))
// HELLO
}
89
Lambda expressions can be hard to understand at first glance so let's break it down. Lambda expressions are written within curly braces {}.
The function returns the result of the .uppercase() function called on text.
The entire lambda expression is assigned to the upperCaseString variable with the assignment operator =.
The lambda expression is called by using the variable upperCaseString like a function and the string "hello" as a parameter.
If you declare a lambda without parameters, then there is no need to use ->. For example:
{ println("Log message") }
fun main() {
val numbers = listOf(1, -2, 3, -4, 5, -6)
println(positives)
// [1, 3, 5]
println(negatives)
// [-2, -4, -6]
//sampleEnd
}
{ x -> x > 0 } takes each element of the list and returns only those that are positive.
{ x -> x < 0 } takes each element of the list and returns only those that are negative.
For positive numbers, the example adds the lambda expression directly in the .filter() function.
For negative numbers, the example assigns the lambda expression to the isNegative variable. Then the isNegative variable is used as a function parameter in the
.filter() function. In this case, you have to specify the type of function parameters (x) in the lambda expression.
90
If a lambda expression is the only function parameter, you can drop the function parentheses ():
This is an example of a trailing lambda, which is discussed in more detail at the end of this chapter.
Another good example, is using the .map() function to transform items in a collection:
fun main() {
val numbers = listOf(1, -2, 3, -4, 5, -6)
val doubled = numbers.map { x -> x * 2 }
println(doubled)
// [2, -4, 6, -8, 10, -12]
println(tripled)
// [3, -6, 9, -12, 15, -18]
//sampleEnd
}
{ x -> x * 2 } takes each element of the list and returns that element multiplied by 2.
{ x -> x * 3 } takes each element of the list and returns that element multiplied by 3.
Function types
Before you can return a lambda expression from a function, you first need to understand function types.
You have already learned about basic types but functions themselves also have a type. Kotlin's type inference can infer a function's type from the parameter type.
But there may be times when you need to explicitly specify the function type. The compiler needs the function type so that it knows what is and isn't allowed for
that function.
This is what a lambda expression looks like if a function type for upperCaseString() is defined:
fun main() {
println(upperCaseString("hello"))
// HELLO
}
If your lambda expression has no parameters then the parentheses () are left empty. For example: () -> Unit
You must declare parameter and return types either in the lambda expression or as a function type. Otherwise, the compiler won't be able to know what
type your lambda expression is.
91
type.
In the following example, the toSeconds() function has function type (Int) -> Int because it always returns a lambda expression that takes a parameter of type Int and
returns an Int value.
This example uses a when expression to determine which lambda expression is returned when toSeconds() is called:
fun main() {
val timesInMinutes = listOf(2, 10, 15, 1)
val min2sec = toSeconds("minute")
val totalTimeInSeconds = timesInMinutes.map(min2sec).sum()
println("Total time is $totalTimeInSeconds secs")
// Total time is 1680 secs
}
Invoke separately
Lambda expressions can be invoked on their own by adding parentheses () after the curly braces {} and including any parameters within the parentheses:
fun main() {
println({ text: String -> text.uppercase() }("hello"))
// HELLO
//sampleEnd
}
Trailing lambdas
As you have already seen, if a lambda expression is the only function parameter, you can drop the function parentheses (). If a lambda expression is passed as the
last parameter of a function, then the expression can be written outside the function parentheses (). In both cases, this syntax is called a trailing lambda.
For example, the .fold() function accepts an initial value and an operation:
fun main() {
// The initial value is zero.
// The operation sums the initial value with every item in the list cumulatively.
println(listOf(1, 2, 3).fold(0, { x, item -> x + item })) // 6
For more information on lambda expressions, see Lambda expressions and anonymous functions.
Exercise 1
You have a list of actions supported by a web service, a common prefix for all requests, and an ID of a particular resource. To request an action title over the
resource with ID: 5, you need to create the following URL: https://example.com/book-info/5/title. Use a lambda expression to create a list of URLs from the list of
actions.
fun main() {
val actions = listOf("title", "year", "author")
val prefix = "https://example.com/book-info"
val id = 5
92
val urls = // Write your code here
println(urls)
}
fun main() {
val actions = listOf("title", "year", "author")
val prefix = "https://example.com/book-info"
val id = 5
val urls = actions.map { action -> "$prefix/$id/$action" }
println(urls)
}
Exercise 2
Write a function that takes an Int value and an action (a function with type () -> Unit) which then repeats the action the given number of times. Then use this function
to print “Hello” 5 times.
fun main() {
// Write your code here
}
fun main() {
repeatN(5) {
println("Hello")
}
}
Next step
Classes
Classes
Kotlin supports object-oriented programming with classes and objects. Objects are useful for storing data in your program. Classes allow you to declare a set of
characteristics for an object. When you create objects from a class, you can save time and effort because you don't have to declare these characteristics every
time.
class Customer
Properties
Characteristics of a class's object can be declared in properties. You can declare properties for a class:
93
Within the class body defined by curly braces {}.
We recommend that you declare properties as read-only (val) unless they need to be changed after an instance of the class is created.
You can declare properties without val or var within parentheses but these properties are not accessible after an instance has been created.
Just like with function parameters, class properties can have default values:
Create instance
To create an object from a class, you declare a class instance using a constructor.
By default, Kotlin automatically creates a constructor with the parameters declared in the class header.
For example:
fun main() {
val contact = Contact(1, "[email protected]")
}
In the example:
Contact is a class.
id and email are used with the default constructor to create contact.
Kotlin classes can have many constructors, including ones that you define yourself. To learn more about how to declare multiple constructors, see Constructors.
Access properties
To access a property of an instance, write the name of the property after the instance name appended with a period .:
fun main() {
val contact = Contact(1, "[email protected]")
94
// [email protected]
}
To concatenate the value of a property as part of a string, you can use string templates ( $). For example:
Member functions
In addition to declaring properties as part of an object's characteristics, you can also define an object's behavior with member functions.
In Kotlin, member functions must be declared within the class body. To call a member function on an instance, write the function name after the instance name
appended with a period .. For example:
fun main() {
val contact = Contact(1, "[email protected]")
// Calls member function printId()
contact.printId()
// 1
}
Data classes
Kotlin has data classes which are particularly useful for storing data. Data classes have the same functionality as classes, but they come automatically with
additional member functions. These member functions allow you to easily print the instance to readable output, compare instances of a class, copy instances, and
more. As these functions are automatically available, you don't have to spend time writing the same boilerplate code for each of your classes.
Function Description
toString() Prints a readable string of the class instance and its properties.
copy() Creates a class instance by copying another, potentially with some different properties.
See the following sections for examples of how to use each function:
Print as string
Compare instances
Copy instance
95
Print as string
To print a readable string of a class instance, you can explicitly call the toString() function, or use print functions (println() and print()) which automatically call
toString() for you:
fun main() {
val user = User("Alex", 1)
Compare instances
To compare data class instances, use the equality operator ==:
fun main() {
val user = User("Alex", 1)
val secondUser = User("Alex", 1)
val thirdUser = User("Max", 2)
Copy instance
To create an exact copy of a data class instance, call the copy() function on the instance.
To create a copy of a data class instance and change some properties, call the copy() function on the instance and add replacement values for properties as
function parameters.
For example:
fun main() {
val user = User("Alex", 1)
Creating a copy of an instance is safer than modifying the original instance because any code that relies on the original instance isn't affected by the copy and what
you do with it.
96
The last chapter of this tour is about Kotlin's null safety.
Practice
Exercise 1
Define a data class Employee with two properties: one for a name, and another for a salary. Make sure that the property for salary is mutable, otherwise you won’t
get a salary boost at the end of the year! The main function demonstrates how you can use this data class.
fun main() {
val emp = Employee("Mary", 20)
println(emp)
emp.salary += 10
println(emp)
}
fun main() {
val emp = Employee("Mary", 20)
println(emp)
emp.salary += 10
println(emp)
}
Exercise 2
Declare the additional data classes that are needed for this code to compile.
data class Person(val name: Name, val address: Address, val ownsAPet: Boolean = true)
// Write your code here
// data class Name(...)
fun main() {
val person = Person(
Name("John", "Smith"),
Address("123 Fake Street", City("Springfield", "US")),
ownsAPet = false
)
}
data class Person(val name: Name, val address: Address, val ownsAPet: Boolean = true)
data class Name(val first: String, val last: String)
data class Address(val street: String, val city: City)
data class City(val name: String, val countryCode: String)
fun main() {
val person = Person(
Name("John", "Smith"),
Address("123 Fake Street", City("Springfield", "US")),
ownsAPet = false
)
}
Exercise 3
To test your code, you need a generator that can create random employees. Define a RandomEmployeeGenerator class with a fixed list of potential names (inside
the class body). Configure the class with a minimum and maximum salary (inside the class header). In the class body, define the generateEmployee() function. Once
again, the main function demonstrates how you can use this class.
97
In this exercise, you import a package so that you can use the Random.nextInt() function. For more information about importing packages, see Packages
and imports.
Hint 1
Lists have an extension function called .random() that returns a random item within a list.
Hint 2
Random.nextInt(from = ..., until = ...) gives you a random Int number within specified limits.
import kotlin.random.Random
fun main() {
val empGen = RandomEmployeeGenerator(10, 30)
println(empGen.generateEmployee())
println(empGen.generateEmployee())
println(empGen.generateEmployee())
empGen.minSalary = 50
empGen.maxSalary = 100
println(empGen.generateEmployee())
}
import kotlin.random.Random
fun main() {
val empGen = RandomEmployeeGenerator(10, 30)
println(empGen.generateEmployee())
println(empGen.generateEmployee())
println(empGen.generateEmployee())
empGen.minSalary = 50
empGen.maxSalary = 100
println(empGen.generateEmployee())
}
Next step
Null safety
Null safety
In Kotlin, it's possible to have a null value. Kotlin uses null values when something is missing or not yet set. You've already seen an example of Kotlin returning a
null value in the Collections chapter when you tried to access a key-value pair with a key that doesn't exist in the map. Although it's useful to use null values in this
way, you might run into problems if your code isn't prepared to handle them.
To help prevent issues with null values in your programs, Kotlin has null safety in place. Null safety detects potential problems with null values at compile time,
rather than at run time.
Use safe calls to properties or functions that may contain null values.
98
Declare actions to take if null values are detected.
Nullable types
Kotlin supports nullable types which allows the possibility for the declared type to have null values. By default, a type is not allowed to accept null values. Nullable
types are declared by explicitly adding ? after the type declaration.
For example:
fun main() {
// neverNull has String type
var neverNull: String = "This can't be null"
// This is OK
nullable = null
println(strLength(neverNull)) // 18
println(strLength(nullable)) // Throws a compiler error
}
length is a property of the String class that contains the number of characters within a string.
fun main() {
val nullString: String? = null
println(describeString(nullString))
// Empty or null string
}
In the following example, the lengthString() function uses a safe call to return either the length of the string or null:
99
fun main() {
val nullString: String? = null
println(lengthString(nullString))
// null
}
Safe calls can be chained so that if any property of an object contains a null value, then null is returned without an error being thrown. For example:
person.company?.address?.country
The safe call operator can also be used to safely call an extension or member function. In this case, a null check is performed before the function is called. If the
check detects a null value, then the call is skipped and null is returned.
In the following example, nullString is null so the invocation of .uppercase() is skipped and null is returned:
fun main() {
val nullString: String? = null
println(nullString?.uppercase())
// null
}
Write on the left-hand side of the Elvis operator what should be checked for a null value. Write on the right-hand side of the Elvis operator what should be returned if
a null value is detected.
In the following example, nullString is null so the safe call to access the length property returns a null value. As a result, the Elvis operator returns 0:
fun main() {
val nullString: String? = null
println(nullString?.length ?: 0)
// 0
}
For more information about null safety in Kotlin, see Null safety.
Practice
Exercise
You have the employeeById function that gives you access to a database of employees of a company. Unfortunately, this function returns a value of the Employee?
type, so the result can be null. Your goal is to write a function that returns the salary of an employee when their id is provided, or 0 if the employee is missing from
the database.
fun main() {
println((1..5).sumOf { id -> salaryById(id) })
100
}
fun main() {
println((1..5).sumOf { id -> salaryById(id) })
}
What's next?
Congratulations! Now that you have completed the beginner tour, take your understanding of Kotlin to the next level with our intermediate tour:
Extension functions
In software development, you often need to modify the behavior of a program without altering the original source code. For example, in your project, you might want
to add extra functionality to a class from a third-party library.
Extension functions allow you to extend a class with additional functionality. You call extension functions the same way you call member functions of a class.
Before introducing the syntax for extension functions, you need to understand the terms receiver type and receiver object.
The receiver object is what the function is called on. In other words, the receiver is where or with whom the information is shared.
In this example, the main() function calls the .first() function. The .first() function is called on the readOnlyShapes variable, so the readOnlyShapes variable is the
receiver.
The receiver object has a type so that the compiler understands when the function can be used.
This example uses the .first() function from the standard library to return the first element in a list. To create your own extension function, write the name of the class
that you want to extend followed by a . and the name of your function. Continue with the rest of the function declaration, including its arguments and return type.
101
For example:
fun main() {
// "hello" is the receiver object
println("hello".bold())
// <b>hello</b>
}
In this example:
The receiver object is accessed inside the body by the keyword: this.
The .bold() extension function takes a string and returns it in a <b> HTML element for bold text.
Extension-oriented design
You can define extension functions anywhere, which enables you to create extension-oriented designs. These designs separate core functionality from useful but
non-essential features, making your code easier to read and maintain.
A good example is the HttpClient class from the Ktor library, which helps perform network requests. The core of its functionality is a single function request(), which
takes all the information needed for an HTTP request:
class HttpClient {
fun request(method: String, url: String, headers: Map<String, String>): HttpResponse {
// Network code
}
}
In practice, the most popular HTTP requests are GET or POST requests. It makes sense for the library to provide shorter names for these common use cases.
However, these don't require writing new network code, only a specific request call. In other words, they are perfect candidates to be defined as separate .get() and
.post() extension functions:
These .get() and .post() functions call the request() function with the correct HTTP method, so you don't have to. They streamline your code and make it easier to
understand:
class HttpClient {
fun request(method: String, url: String, headers: Map<String, String>): HttpResponse {
println("Requesting $method to $url with headers: $headers")
return HttpResponse("Response from $url")
}
}
fun main() {
val client = HttpClient()
102
This extension-oriented approach is widely used in Kotlin's standard library and other libraries. For example, the String class has many extension functions to help
you work with strings.
Practice
Exercise 1
Write an extension function called isPositive that takes an integer and checks whether it is positive.
fun main() {
println(1.isPositive())
// true
}
fun main() {
println(1.isPositive())
// true
}
Exercise 2
Write an extension function called toLowercaseString that takes a string and returns a lowercase version.
Hint
Use the .lowercase() function for the String type.
fun main() {
println("Hello World!".toLowercaseString())
// hello world!
}
fun main() {
println("Hello World!".toLowercaseString())
// hello world!
}
Next step
Intermediate: Scope functions
Scope functions
103
In programming, a scope is the area in which your variable or object is recognized. The most commonly referred to scopes are the global scope and the local scope:
Global scope – a variable or object that is accessible from anywhere in the program.
Local scope – a variable or object that is only accessible within the block or function where it is defined.
In Kotlin, there are also scope functions that allow you to create a temporary scope around an object and execute some code.
Scope functions make your code more concise because you don't have to refer to the name of your object within the temporary scope. Depending on the scope
function, you can access the object either by referencing it via the keyword this or using it as an argument via the keyword it.
Kotlin has five scope functions in total: let, apply, run, also, and with.
Each scope function takes a lambda expression and returns either the object or the result of the lambda expression. In this tour, we explain each scope function
and how to use it.
You can also watch the Back to the Stdlib: Making the Most of Kotlin's Standard Library talk on scope functions by Sebastian Aigner, Kotlin developer
advocate.
Let
Use the let scope function when you want to perform null checks in your code and later perform further actions with the returned object.
fun main() {
val address: String? = getNextAddress()
sendNotification(address)
}
The example creates a variable address that has a nullable String type. But this becomes a problem when you call the sendNotification() function because this
function doesn't expect that address could be a null value. The compiler reports an error as a result:
From the beginner tour, you already know that you can perform a null check with an if condition or use the Elvis operator ?:. But what if you want to use the returned
object later in your code? You could achieve this with an if condition and an else branch:
fun main() {
val address: String? = getNextAddress()
val confirm = if(address != null) {
sendNotification(address)
} else { null }
//sampleEnd
}
104
However, a more concise approach is to use the let scope function:
fun main() {
val address: String? = getNextAddress()
val confirm = address?.let {
sendNotification(it)
}
//sampleEnd
}
The example:
Uses a safe call for the let scope function on the address variable.
Passes the sendNotification() function as a lambda expression into the let scope function.
Refers to the address variable via it, using the temporary scope.
With this approach, your code can handle the address variable potentially being a null value, and you can use the confirm variable later in your code.
Apply
Use the apply scope function to initialize objects, like a class instance, at the time of creation rather than later on in your code. This approach makes your code
easier to read and manage.
class Client() {
var token: String? = null
fun connect() = println("connected!")
fun authenticate() = println("authenticated!")
fun getData(): String = "Mock data"
}
fun main() {
client.token = "asdf"
client.connect()
// connected!
client.authenticate()
// authenticated!
client.getData()
}
The example has a Client class that contains one property called token and three member functions: connect(), authenticate(), and getData().
The example creates client as an instance of the Client class before initializing its token property and calling its member functions in the main() function.
Although this example is compact, in the real world, it can be a while before you can configure and use the class instance (and its member functions) after you've
created it. However, if you use the apply scope function you can create, configure and use member functions on your class instance all in the same place in your
code:
class Client() {
var token: String? = null
fun connect() = println("connected!")
105
fun authenticate() = println("authenticated!")
fun getData(): String = "Mock data"
}
val client = Client().apply {
token = "asdf"
connect()
authenticate()
}
fun main() {
client.getData()
// connected!
// authenticated!
}
The example:
Creates a temporary scope within the apply scope function so that you don't have to explicitly refer to the client instance when accessing its properties or
functions.
Passes a lambda expression to the apply scope function that updates the token property and calls the connect() and authenticate() functions.
Calls the getData() member function on the client instance in the main() function.
As you can see, this strategy is convenient when you are working with large pieces of code.
Run
Similar to apply, you can use the run scope function to initialize an object, but it's better to use run to initialize an object at a specific moment in your code and
immediately compute a result.
Let's continue the previous example for the apply function, but this time, you want the connect() and authenticate() functions to be grouped so that they are called
on every request.
For example:
class Client() {
var token: String? = null
fun connect() = println("connected!")
fun authenticate() = println("authenticated!")
fun getData(): String = "Mock data"
}
fun main() {
val result: String = client.run {
connect()
// connected!
authenticate()
// authenticated!
getData()
}
}
The example:
Creates a temporary scope within the apply scope function so that you don't have to explicitly refer to the client instance when accessing its properties or
functions.
Passes a lambda expression to the apply scope function that updates the token property.
106
Creates a result variable with type String.
Creates a temporary scope within the run scope function so that you don't have to explicitly refer to the client instance when accessing its properties or
functions.
Passes a lambda expression to the run scope function that calls the connect(), authenticate(), and getData() functions.
Now you can use the returned result further in your code.
Also
Use the also scope function to complete an additional action with an object and then return the object to continue using it in your code, like writing a log.
fun main() {
val medals: List<String> = listOf("Gold", "Silver", "Bronze")
val reversedLongUppercaseMedals: List<String> =
medals
.map { it.uppercase() }
.filter { it.length > 4 }
.reversed()
println(reversedLongUppercaseMedals)
// [BRONZE, SILVER]
}
The example:
Passes a lambda expression to the .map() function that refers to medals via the it keyword and calls the .uppercase() extension function on it.
Passes a lambda expression as a predicate to the .filter() function that refers to medals via the it keyword and checks if the length of the list contained in the
medals variable is longer than 4 items.
It would be useful to add some logging in between the function calls to see what is happening to the medals variable. The also function helps with that:
fun main() {
val medals: List<String> = listOf("Gold", "Silver", "Bronze")
val reversedLongUppercaseMedals: List<String> =
medals
.map { it.uppercase() }
.also { println(it) }
// [GOLD, SILVER, BRONZE]
.filter { it.length > 4 }
.also { println(it) }
// [SILVER, BRONZE]
.reversed()
println(reversedLongUppercaseMedals)
// [BRONZE, SILVER]
}
107
Creates a temporary scope within the also scope function so that you don't have to explicitly refer to the medals variable when using it as a function parameter.
Passes a lambda expression to the also scope function that calls the println() function using the medals variable as a function parameter via the it keyword.
Since the also function returns the object, it is useful for not only logging but debugging, chaining multiple operations, and performing other side-effect operations
that don't affect the main flow of your code.
With
Unlike the other scope functions, with is not an extension function, so the syntax is different. You pass the receiver object to with as an argument.
Use the with scope function when you want to call multiple functions on an object.
class Canvas {
fun rect(x: Int, y: Int, w: Int, h: Int): Unit = println("$x, $y, $w, $h")
fun circ(x: Int, y: Int, rad: Int): Unit = println("$x, $y, $rad")
fun text(x: Int, y: Int, str: String): Unit = println("$x, $y, $str")
}
fun main() {
val mainMonitorPrimaryBufferBackedCanvas = Canvas()
The example creates a Canvas class that has three member functions: rect(), circ(), and text(). Each of these member functions prints a statement constructed from
the function parameters that you provide.
The example creates mainMonitorPrimaryBufferBackedCanvas as an instance of the Canvas class before calling a sequence of member functions on the instance
with different function parameters.
You can see that this code is hard to read. If you use the with function, the code is streamlined:
class Canvas {
fun rect(x: Int, y: Int, w: Int, h: Int): Unit = println("$x, $y, $w, $h")
fun circ(x: Int, y: Int, rad: Int): Unit = println("$x, $y, $rad")
fun text(x: Int, y: Int, str: String): Unit = println("$x, $y, $str")
}
fun main() {
val mainMonitorSecondaryBufferBackedCanvas = Canvas()
with(mainMonitorSecondaryBufferBackedCanvas) {
text(10, 10, "Foo")
rect(20, 30, 100, 50)
circ(40, 60, 25)
text(15, 45, "Hello")
rect(70, 80, 150, 100)
circ(90, 110, 40)
text(35, 55, "World")
rect(120, 140, 200, 75)
circ(160, 180, 55)
text(50, 70, "Kotlin")
}
//sampleEnd
}
This example:
Uses the with scope function with the mainMonitorSecondaryBufferBackedCanvas instance as the receiver object.
Creates a temporary scope within the with scope function so that you don't have to explicitly refer to the mainMonitorSecondaryBufferBackedCanvas instance
when calling its member functions.
108
Passes a lambda expression to the with scope function that calls a sequence of member functions with different function parameters.
Now that this code is much easier to read, you are less likely to make mistakes.
let it Lambda result Perform null checks in your code and later perform further actions with the returned object.
run this Lambda result Initialize objects at the time of creation AND compute a result.
Practice
Exercise 1
Rewrite the .getPriceInEuros() function as a single-expression function that uses safe call operators ?. and the let scope function.
Hint
Use safe call operators ?. to safely access the priceInDollars property from the getProductInfo() function. Then, use the let scope function to convert the value of
priceInDollars into euros.
class Product {
fun getProductInfo(): ProductInfo? {
return ProductInfo(100.0)
}
}
fun main() {
val product = Product()
val priceInEuros = product.getPriceInEuros()
if (priceInEuros != null) {
109
println("Price in Euros: €$priceInEuros")
// Price in Euros: €85.0
} else {
println("Price information is not available.")
}
}
class Product {
fun getProductInfo(): ProductInfo? {
return ProductInfo(100.0)
}
}
fun main() {
val product = Product()
val priceInEuros = product.getPriceInEuros()
if (priceInEuros != null) {
println("Price in Euros: €$priceInEuros")
// Price in Euros: €85.0
} else {
println("Price information is not available.")
}
}
Exercise 2
You have an updateEmail() function that updates the email address of a user. Use the apply scope function to update the email address and then the also scope
function to print a log message: Updating email for user with ID: ${it.id}.
fun updateEmail(user: User, newEmail: String): User = // Write your code here
fun main() {
val user = User(1, "[email protected]")
val updatedUser = updateEmail(user, "[email protected]")
// Updating email for user with ID: 1
fun main() {
val user = User(1, "[email protected]")
val updatedUser = updateEmail(user, "[email protected]")
// Updating email for user with ID: 1
Next step
Intermediate: Lambda expressions with receiver
110
Intermediate: Lambda expressions with receiver
In this chapter, you'll learn how to use receiver objects with another type of function, lambda expressions, and how they can help you create a domain-specific
language.
Lambda expressions with receiver are also known as function literals with receiver.
The syntax for a lambda expression with receiver is different when you define the function type. First, write the receiver object that you want to extend. Next, put a .
and then complete the rest of your function type definition. For example:
fun main() {
// Lambda expression with receiver definition
fun StringBuilder.appendText() { append("Hello!") }
In this example:
The function type of the lambda expression has no function parameters () and has no return value Unit.
The lambda expression calls the append() member function from the StringBuilder class and uses the string "Hello!" as the function parameter.
The stringBuilder instance is converted to string with the toString() function and printed via the println() function.
Lambda expressions with receiver are helpful when you want to create a domain-specific language (DSL). Since you have access to the receiver object's member
functions and properties without explicitly referencing the receiver, your code becomes leaner.
To demonstrate this, consider an example that configures items in a menu. Let's begin with a MenuItem class and a Menu class that contains a function to add
items to the menu called item(), as well as a list of all menu items items:
111
items.add(MenuItem(name))
}
}
Let's use a lambda expression with receiver passed as a function parameter ( init) to the menu() function that builds a menu as a starting point. You'll notice that the
code follows a similar approach to the previous example with the StringBuilder class:
Now you can use the DSL to configure a menu and create a printMenu() function to print the menu structure to the console:
As you can see, using a lambda expression with receiver greatly simplifies the code needed to create your menu. Lambda expressions are not only useful for setup
and creation but also for configuration. They are commonly used in building DSLs for APIs, UI frameworks, and configuration builders to produce streamlined code,
allowing you to focus more easily on the underlying code structure and logic.
Kotlin's ecosystem has many examples of this design pattern, such as in the buildList() and buildString() functions from the standard library.
Lambda expressions with receivers can be combined with type-safe builders in Kotlin to make DSLs that detect any problems with types at compile time
rather than at runtime. To learn more, see Type-safe builders.
Practice
Exercise 1
112
You have a fetchData() function that accepts a lambda expression with receiver. Update the lambda expression to use the append() function so that the output of
your code is: Data received - Processed.
fun main() {
fetchData {
// Write your code here
// Data received - Processed
}
}
fun main() {
fetchData {
append(" - Processed")
println(this.toString())
// Data received - Processed
}
}
Exercise 2
You have a Button class and ButtonEvent and Position data classes. Write some code that triggers the onEvent() member function of the Button class to trigger a
double-click event. Your code should print "Double click!".
class Button {
fun onEvent(action: ButtonEvent.() -> Unit) {
// Simulate a double-click event (not a right-click)
val event = ButtonEvent(isRightClick = false, amount = 2, position = Position(100, 200))
event.action() // Trigger the event callback
}
}
fun main() {
val button = Button()
button.onEvent {
// Write your code here
// Double click!
}
}
class Button {
fun onEvent(action: ButtonEvent.() -> Unit) {
// Simulate a double-click event (not a right-click)
val event = ButtonEvent(isRightClick = false, amount = 2, position = Position(100, 200))
event.action() // Trigger the event callback
}
}
113
val amount: Int,
val position: Position
)
fun main() {
val button = Button()
button.onEvent {
if (!isRightClick && amount == 2) {
println("Double click!")
// Double click!
}
}
}
Exercise 3
Write a function that creates a copy of a list of integers where every element is incremented by 1. Use the provided function skeleton that extends List<Int> with an
incremented function.
fun main() {
val originalList = listOf(1, 2, 3)
val newList = originalList.incremented()
println(newList)
// [2, 3, 4]
}
fun main() {
val originalList = listOf(1, 2, 3)
val newList = originalList.incremented()
println(newList)
// [2, 3, 4]
}
Next step
Intermediate: Classes and interfaces
Class inheritance
In a previous chapter, we covered how you can use extension functions to extend classes without modifying the original source code. But what if you are working
114
on something complex where sharing code between classes would be useful? In such cases, you can use class inheritance.
By default, classes in Kotlin can't be inherited. Kotlin is designed this way to prevent unintended inheritance and make your classes easier to maintain.
Kotlin classes only support single inheritance, meaning it is only possible to inherit from one class at a time. This class is called the parent.
The parent of a class inherits from another class (the grandparent), forming a hierarchy. At the top of Kotlin's class hierarchy is the common parent class: Any. All
classes ultimately inherit from the Any class:
The Any class provides the toString() function as a member function automatically. Therefore, you can use this inherited function in any of your classes. For
example:
class Car(val make: String, val model: String, val numberOfDoors: Int)
fun main() {
val car1 = Car("Toyota", "Corolla", 4)
// Uses the .toString() function via string templates to print class properties
println("Car1: make=${car1.make}, model=${car1.model}, numberOfDoors=${car1.numberOfDoors}")
// Car1: make=Toyota, model=Corolla, numberOfDoors=4
//sampleEnd
}
If you want to use inheritance to share some code between classes, first consider using abstract classes.
Abstract classes
115
Abstract classes can be inherited by default. The purpose of abstract classes is to provide members that other classes inherit or implement. As a result, they have a
constructor, but you can't create instances from them. Within the child class, you define the behavior of the parent's properties and functions with the override
keyword. In this way, you can say that the child class "overrides" the members of the parent class.
When you define the behavior of an inherited function or property, we call that an implementation.
Abstract classes can contain both functions and properties with implementation as well as functions and properties without implementation, known as abstract
functions and properties.
To declare a function or a property without an implementation, you also use the abstract keyword:
For example, let's say that you want to create an abstract class called Product that you can create child classes from to define different product categories:
The constructor has two parameters for the product's name and price.
Let's create a child class for electronics. Before you define an implementation for the category property in the child class, you must use the override keyword:
class Electronic(name: String, price: Double, val warranty: Int) : Product(name, price) {
override val category = "Electronic"
}
class Electronic(name: String, price: Double, val warranty: Int) : Product(name, price) {
override val category = "Electronic"
}
116
fun main() {
// Creates an instance of the Electronic class
val laptop = Electronic(name = "Laptop", price = 1000.0, warranty = 2)
println(laptop.productInfo())
// Product: Laptop, Category: Electronic, Price: 1000.0
}
Although abstract classes are great for sharing code in this way, they are restricted because classes in Kotlin only support single inheritance. If you need to inherit
from multiple sources, consider using interfaces.
Interfaces
Interfaces are similar to classes, but they have some differences:
You can't create an instance of an interface. They don't have a constructor or header.
Their functions and properties are implicitly inheritable by default. In Kotlin, we say that they are "open".
You don't need to mark their functions as abstract if you don't give them an implementation.
Similar to abstract classes, you use interfaces to define a set of functions and properties that classes can inherit and implement later. This approach helps you
focus on the abstraction described by the interface, rather than the specific implementation details. Using interfaces makes your code:
Easier to test, as you can quickly swap an implementation with a mock for testing.
interface PaymentMethod
Interface implementation
Interfaces support multiple inheritance so a class can implement multiple interfaces at once. First, let's consider the scenario where a class implements one
interface.
To create a class that implements an interface, add a colon after your class header, followed by the interface name that you want to implement. You don't use
parentheses () after the interface name because interfaces don't have a constructor:
For example:
interface PaymentMethod {
// Functions are inheritable by default
fun initiatePayment(amount: Double): String
}
class CreditCardPayment(val cardNumber: String, val cardHolderName: String, val expiryDate: String) : PaymentMethod {
override fun initiatePayment(amount: Double): String {
// Simulate processing payment with credit card
return "Payment of $$amount initiated using Credit Card ending in ${cardNumber.takeLast(4)}."
}
}
fun main() {
val paymentMethod = CreditCardPayment("1234 5678 9012 3456", "John Doe", "12/25")
println(paymentMethod.initiatePayment(100.0))
// Payment of $100.0 initiated using Credit Card ending in 3456.
}
In the example:
117
CreditCardPayment is a class that implements the PaymentMethod interface.
The overridden initiatePayment() function is called on the paymentMethod instance with a parameter of 100.0.
To create a class that implements multiple interfaces, add a colon after your class header followed by the name of the interfaces that you want to implement
separated by a comma:
For example:
interface PaymentMethod {
fun initiatePayment(amount: Double): String
}
interface PaymentType {
val paymentType: String
}
class CreditCardPayment(val cardNumber: String, val cardHolderName: String, val expiryDate: String) : PaymentMethod,
PaymentType {
override fun initiatePayment(amount: Double): String {
// Simulate processing payment with credit card
return "Payment of $$amount initiated using Credit Card ending in ${cardNumber.takeLast(4)}."
}
fun main() {
val paymentMethod = CreditCardPayment("1234 5678 9012 3456", "John Doe", "12/25")
println(paymentMethod.initiatePayment(100.0))
// Payment of $100.0 initiated using Credit Card ending in 3456.
println("Payment is by ${paymentMethod.paymentType}")
// Payment is by Credit Card
}
In the example:
PaymentType is an interface that has the paymentType property that isn't initialized.
The CreditCardPayment class overrides the inherited initiatePayment() function and the paymentType property.
The overridden initiatePayment() function is called on the paymentMethod instance with a parameter of 100.0.
For more information about interfaces and interface inheritance, see Interfaces.
Delegation
Interfaces are useful, but if your interface contains many functions, child classes may end up with a lot of boilerplate code. When you only want to override a small
part of your parent's behavior, you need to repeat yourself a lot.
Boilerplate code is a chunk of code that is reused with little or no alteration in multiple parts of a software project.
For example, let's say that you have an interface called Drawable that contains a number of functions and one property called color:
118
interface Drawable {
fun draw()
fun resize()
val color: String?
}
You create a class called Circle which implements the Drawable interface and provides implementations for all of its members:
If you wanted to create a child class of the Circle class which had the same behavior except for the value of the color property, you still need to add
implementations for each member function of the Circle class:
You can see that if you have a large number of member functions in the Drawable interface, the amount of boilerplate code in the RedCircle class can be very large.
However, there is an alternative.
In Kotlin, you can use delegation to delegate the interface implementation to an instance of a class. For example, you can create an instance of the Circle class and
delegate the implementations of the member functions of the Circle class to this instance. To do this, use the by keyword. For example:
Here, param is the name of the instance of the Circle class that the implementations of member functions are delegated to.
Now you don't have to add implementations for the member functions in the RedCircle class. The compiler does this for you automatically from the Circle class.
This saves you from having to write a lot of boilerplate code. Instead, you add code only for the behavior you want to change for your child class.
For example, if you want to change the value of the color property:
If you want to, you can also override the behavior of an inherited member function in the RedCircle class, but now you don't have to add new lines of code for every
inherited member function.
Practice
Exercise 1
119
Imagine you're working on a smart home system. A smart home typically has different types of devices that all have some basic features but also unique behaviors.
In the code sample below, complete the abstract class called SmartDevice so that the child class SmartLight can compile successfully.
Then, create another child class called SmartThermostat that inherits from the SmartDevice class and implements turnOn() and turnOff() functions that return print
statements describing which thermostat is heating or turned off. Finally, add another function called adjustTemperature() that accepts a temperature measurement
as an input and prints: $name thermostat set to $temperature°C.
Hint
In the SmartDevice class, add the turnOn() and turnOff() functions so that you can override their behavior later in the SmartThermostat class.
fun main() {
val livingRoomLight = SmartLight("Living Room Light")
val bedroomThermostat = SmartThermostat("Bedroom Thermostat")
livingRoomLight.turnOn()
// Living Room Light is now ON.
livingRoomLight.adjustBrightness(10)
// Adjusting Living Room Light brightness to 10%.
livingRoomLight.turnOff()
// Living Room Light is now OFF.
bedroomThermostat.turnOn()
// Bedroom Thermostat thermostat is now heating.
bedroomThermostat.adjustTemperature(5)
// Bedroom Thermostat thermostat set to 5°C.
bedroomThermostat.turnOff()
// Bedroom Thermostat thermostat is now off.
}
120
println("$name thermostat set to $temperature°C.")
}
}
fun main() {
val livingRoomLight = SmartLight("Living Room Light")
val bedroomThermostat = SmartThermostat("Bedroom Thermostat")
livingRoomLight.turnOn()
// Living Room Light is now ON.
livingRoomLight.adjustBrightness(10)
// Adjusting Living Room Light brightness to 10%.
livingRoomLight.turnOff()
// Living Room Light is now OFF.
bedroomThermostat.turnOn()
// Bedroom Thermostat thermostat is now heating.
bedroomThermostat.adjustTemperature(5)
// Bedroom Thermostat thermostat set to 5°C.
bedroomThermostat.turnOff()
// Bedroom Thermostat thermostat is now off.
}
Exercise 2
Create an interface called Media that you can use to implement specific media classes like Audio, Video, or Podcast. Your interface must include:
Then, create a class called Audio that implements the Media interface. The Audio class must use the title property in its constructor as well as have an additional
property called composer that has String type. In the class, implement the play() function to print the following: "Playing audio: $title, composed by $composer".
Hint
You can use the override keyword in class headers to implement a property from an interface in the constructor.
fun main() {
val audio = Audio("Symphony No. 5", "Beethoven")
audio.play()
// Playing audio: Symphony No. 5, composed by Beethoven
}
interface Media {
val title: String
fun play()
}
fun main() {
val audio = Audio("Symphony No. 5", "Beethoven")
audio.play()
// Playing audio: Symphony No. 5, composed by Beethoven
}
Exercise 3
You're building a payment processing system for an e-commerce application. Each payment method needs to be able to authorize a payment and process a
transaction. Some payments also need to be able to process refunds.
121
2. In the PaymentMethod abstract class:
Add a function called authorize() that takes an amount and prints a message containing the amount.
3. Create a class called CreditCard that implements the Refundable interface and PaymentMethod abstract class. In this class, add implementations for the
refund() and processPayment() functions so that they print the following statements:
interface Refundable {
// Write your code here
}
fun main() {
val visa = CreditCard("Visa")
visa.authorize(100.0)
// Authorizing payment of $100.0.
visa.processPayment(100.0)
// Processing credit card payment of $100.0.
visa.refund(50.0)
// Refunding $50.0 to the credit card.
}
interface Refundable {
fun refund(amount: Double)
}
fun main() {
val visa = CreditCard("Visa")
visa.authorize(100.0)
// Authorizing payment of $100.0.
visa.processPayment(100.0)
// Processing credit card payment of $100.0.
visa.refund(50.0)
// Refunding $50.0 to the credit card.
}
Exercise 4
You have a simple messaging app that has some basic functionality, but you want to add some functionality for smart messages without significantly duplicating
your code.
In the code below, define a class called SmartMessenger that inherits from the BasicMessenger class but delegates the implementation to an instance of the
122
BasicMessenger class.
In the SmartMessenger class, override the sendMessage() function to send smart messages. The function must accept a message as an input and return a printed
statement: "Sending a smart message: $message". In addition, call the sendMessage() function from the BasicMessenger class and prefix the message with
[smart].
You don't need to rewrite the receiveMessage() function in the SmartMessenger class.
interface Messenger {
fun sendMessage(message: String)
fun receiveMessage(): String
}
fun main() {
val basicMessenger = BasicMessenger()
val smartMessenger = SmartMessenger(basicMessenger)
basicMessenger.sendMessage("Hello!")
// Sending message: Hello!
println(smartMessenger.receiveMessage())
// You've got a new message!
smartMessenger.sendMessage("Hello from SmartMessenger!")
// Sending a smart message: Hello from SmartMessenger!
// Sending message: [smart] Hello from SmartMessenger!
}
interface Messenger {
fun sendMessage(message: String)
fun receiveMessage(): String
}
fun main() {
val basicMessenger = BasicMessenger()
val smartMessenger = SmartMessenger(basicMessenger)
basicMessenger.sendMessage("Hello!")
// Sending message: Hello!
println(smartMessenger.receiveMessage())
// You've got a new message!
smartMessenger.sendMessage("Hello from SmartMessenger!")
// Sending a smart message: Hello from SmartMessenger!
// Sending message: [smart] Hello from SmartMessenger!
}
123
Next step
Intermediate: Objects
Intermediate: Objects
In this chapter, you'll expand your understanding of classes by exploring object declarations. This knowledge will help you efficiently manage behavior across your
projects.
Object declarations
In Kotlin, you can use object declarations to declare a class with a single instance. In a sense, you declare the class and create the single instance at the same time.
Object declarations are useful when you want to create a class to use as a single reference point for your program or to coordinate behavior across a system.
A class that has only one instance that is easily accessible is called a singleton.
Objects in Kotlin are lazy, meaning they are created only when accessed. Kotlin also ensures that all objects are created in a thread-safe manner so that you don't
have to check this manually.
object DoAuth {}
Following the name of your object, add any properties or member functions within the object body defined by curly braces {}.
Objects can't have constructors, so they don't have headers like classes.
For example, let's say that you wanted to create an object called DoAuth that is responsible for authentication:
object DoAuth {
fun takeParams(username: String, password: String) {
println("input Auth parameters = $username:$password")
}
}
fun main(){
// The object is created when the takeParams() function is called
DoAuth.takeParams("coding_ninja", "N1njaC0ding!")
// input Auth parameters = coding_ninja:N1njaC0ding!
}
The object has a member function called takeParams that accepts username and password variables as parameters and returns a string to the console. The
DoAuth object is only created when the function is called for the first time.
interface Auth {
fun takeParams(username: String, password: String)
}
124
Data objects
To make it easier to print the contents of an object declaration, Kotlin has data objects. Similar to data classes, which you learned about in the beginner tour, data
objects automatically come with additional member functions: toString() and equals().
Unlike data classes, data objects do not come automatically with the copy() member function because they only have a single instance that can't be
copied.
To create a data object, use the same syntax as for object declarations but prefix it with the data keyword:
For example:
fun main() {
println(AppConfig)
// AppConfig
println(AppConfig.appName)
// My Application
}
Companion objects
In Kotlin, a class can have an object: a companion object. You can only have one companion object per class. A companion object is created only when its class is
referenced for the first time.
Any properties or functions declared inside a companion object are shared across all class instances.
To create a companion object within a class, use the same syntax for an object declaration but prefix it with the companion keyword:
A companion object doesn't have to have a name. If you don't define one, the default is Companion.
To access any properties or functions of the companion object, reference the class name. For example:
class BigBen {
companion object Bonger {
fun getBongs(nTimes: Int) {
repeat(nTimes) { print("BONG ") }
}
}
}
fun main() {
// Companion object is created when the class is referenced for the
// first time.
BigBen.getBongs(12)
// BONG BONG BONG BONG BONG BONG BONG BONG BONG BONG BONG BONG
}
This example creates a class called BigBen that contains a companion object called Bonger. The companion object has a member function called getBongs() that
accepts an integer and prints "BONG" to the console the same number of times as the integer.
In the main() function, the getBongs() function is called by referring to the class name. The companion object is created at this point. The getBongs() function is
called with parameter 12.
125
For more information, see Companion objects.
Practice
Exercise 1
You run a coffee shop and have a system for tracking customer orders. Consider the code below and complete the declaration of the second data object so that
the following code in the main() function runs successfully:
interface Order {
val orderId: String
val customerName: String
val orderTotal: Double
}
fun main() {
// Print the name of each data object
println("Order name: $OrderOne")
// Order name: OrderOne
println("Order name: $OrderTwo")
// Order name: OrderTwo
if (OrderOne == OrderTwo) {
println("The orders are identical.")
} else {
println("The orders are unique.")
// The orders are unique.
}
println("Do the orders have the same customer name? ${OrderOne.customerName == OrderTwo.customerName}")
// Do the orders have the same customer name? false
}
interface Order {
val orderId: String
val customerName: String
val orderTotal: Double
}
fun main() {
// Print the name of each data object
println("Order name: $OrderOne")
// Order name: OrderOne
println("Order name: $OrderTwo")
// Order name: OrderTwo
126
if (OrderOne == OrderTwo) {
println("The orders are identical.")
} else {
println("The orders are unique.")
// The orders are unique.
}
println("Do the orders have the same customer name? ${OrderOne.customerName == OrderTwo.customerName}")
// Do the orders have the same customer name? false
}
Exercise 2
Create an object declaration that inherits from the Vehicle interface to create a unique vehicle type: FlyingSkateboard. Implement the name property and the move()
function in your object so that the following code in the main() function runs successfully:
interface Vehicle {
val name: String
fun move(): String
}
fun main() {
println("${FlyingSkateboard.name}: ${FlyingSkateboard.move()}")
// Flying Skateboard: Glides through the air with a hover engine
println("${FlyingSkateboard.name}: ${FlyingSkateboard.fly()}")
// Flying Skateboard: Woooooooo
}
interface Vehicle {
val name: String
fun move(): String
}
fun main() {
println("${FlyingSkateboard.name}: ${FlyingSkateboard.move()}")
// Flying Skateboard: Glides through the air with a hover engine
println("${FlyingSkateboard.name}: ${FlyingSkateboard.fly()}")
// Flying Skateboard: Woooooooo
}
Exercise 3
You have an app where you want to record temperatures. The class itself stores the information in Celsius, but you want to provide an easy way to create an
instance in Fahrenheit as well. Complete the data class so that the following code in the main() function runs successfully:
Hint
Use a companion object.
fun main() {
val fahrenheit = 90.0
val temp = Temperature.fromFahrenheit(fahrenheit)
println("${temp.celsius}°C is $fahrenheit °F")
// 32.22222222222222°C is 90.0 °F
}
127
data class Temperature(val celsius: Double) {
val fahrenheit: Double = celsius * 9 / 5 + 32
companion object {
fun fromFahrenheit(fahrenheit: Double): Temperature = Temperature((fahrenheit - 32) * 5 / 9)
}
}
fun main() {
val fahrenheit = 90.0
val temp = Temperature.fromFahrenheit(fahrenheit)
println("${temp.celsius}°C is $fahrenheit °F")
// 32.22222222222222°C is 90.0 °F
}
Next step
Intermediate: Open and special classes
Open classes
If you can't use interfaces or abstract classes, you can explicitly make a class inheritable by declaring it as open. To do this, use the open keyword before your
class declaration:
To create a class that inherits from another, add a colon after your class header followed by a call to the constructor of the parent class that you want to inherit
from:
In this example, the Car class inherits from the Vehicle class:
class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model)
fun main() {
// Creates an instance of the Car class
val car = Car("Toyota", "Corolla", 4)
Just like when creating a normal class instance, if your class inherits from a parent class, then it must initialize all the parameters declared in the parent class
header. So in the example, the car instance of the Car class initializes the parent class parameters: make and model.
By default, it's not possible to override a member function or property of a parent class. Just like with abstract classes, you need to add special keywords.
128
Member functions
To allow a function in the parent class to be overridden, use the open keyword before its declaration in the parent class:
To override an inherited member function, use the override keyword before the function declaration in the child class:
For example:
class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model) {
override fun displayInfo() {
println("Car Info: Make - $make, Model - $model, Number of Doors - $numberOfDoors")
}
}
fun main() {
val car1 = Car("Toyota", "Corolla", 4)
val car2 = Car("Honda", "Civic", 2)
This example:
Creates two instances of the Car class that inherit from the Vehicle class: car1 and car2.
Overrides the displayInfo() function in the Car class to also print the number of doors.
Properties
In Kotlin, it's not common practice to make a property inheritable by using the open keyword and overriding it later. Most of the time, you use an abstract class or
an interface where properties are inheritable by default.
Properties inside open classes are accessible by their child class. In general, it's better to access them directly rather than override them with a new property.
For example, let's say that you have a property called transmissionType that you want to override later. The syntax for overriding properties is exactly the same as
for overriding member functions. You can do this:
class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model) {
override val transmissionType: String = "Automatic"
}
However, this is not good practice. Instead, you can add the property to the constructor of your inheritable class and declare its value when you create the Car child
class:
open class Vehicle(val make: String, val model: String, val transmissionType: String = "Manual")
class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model, "Automatic")
Accessing properties directly, instead of overriding them, leads to simpler and more readable code. By declaring properties once in the parent class and passing
129
their values through the constructor, you eliminate the need for unnecessary overrides in child classes.
For more information about class inheritance and overriding class behavior, see Inheritance.
// Define interfaces
interface EcoFriendly {
val emissionLevel: String
}
interface ElectricVehicle {
val batteryCapacity: Double
}
// Parent class
open class Vehicle(val make: String, val model: String)
// Child class
open class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model)
// New class that inherits from Car and implements two interfaces
class ElectricCar(
make: String,
model: String,
numberOfDoors: Int,
val capacity: Double,
val emission: String
) : Car(make, model, numberOfDoors), EcoFriendly, ElectricVehicle {
override val batteryCapacity: Double = capacity
override val emissionLevel: String = emission
}
Special classes
In addition to abstract, open, and data classes, Kotlin has special types of classes designed for various purposes, such as restricting specific behavior or reducing
the performance impact of creating small objects.
Sealed classes
There may be times when you want to restrict inheritance. You can do this with sealed classes. Sealed classes are a special type of abstract class. Once you
declare that a class is sealed, you can only create child classes from it within the same package. It's not possible to inherit from the sealed class outside of this
scope.
A package is a collection of code with related classes and functions, typically within a directory. To learn more about packages in Kotlin, see Packages
and imports.
Sealed classes are particularly useful when combined with a when expression. By using a when expression, you can define the behavior for all possible child
classes. For example:
130
}
}
fun main() {
println(greetMammal(Cat("Snowy")))
// Hello Snowy
}
In the example:
There is a sealed class called Mammal that has the name parameter in the constructor.
The Cat class inherits from the Mammal sealed class and uses the name parameter from the Mammal class as the catName parameter in its own constructor.
The Human class inherits from the Mammal sealed class and uses the name parameter from the Mammal class as the humanName parameter in its own
constructor. It also has the job parameter in its constructor.
The greetMammal() function accepts an argument of Mammal type and returns a string.
Within the greetMammal() function body, there's a when expression that uses the is operator to check the type of mammal and decide which action to perform.
The main() function calls the greetMammal() function with an instance of the Cat class and name parameter called Snowy.
This tour discusses the is operator in more detail in the Null safety chapter.
For more information about sealed classes and their recommended use cases, see Sealed classes and interfaces.
Enum classes
Enum classes are useful when you want to represent a finite set of distinct values in a class. An enum class contains enum constants, which are themselves
instances of the enum class.
Let's say that you want to create an enum class that contains the different states of a process. Each enum constant must be separated by a comma ,:
The State enum class has enum constants: IDLE, RUNNING, and FINISHED. To access an enum constant, use the class name followed by a . and the name of the
enum constant:
You can use this enum class with a when expression to define the action to take depending on the value of the enum constant:
fun main() {
val state = State.RUNNING
val message = when (state) {
State.IDLE -> "It's idle"
State.RUNNING -> "It's running"
State.FINISHED -> "It's finished"
}
println(message)
// It's running
}
Enum classes can have properties and member functions just like normal classes.
131
For example, let's say you're working with HTML and you want to create an enum class containing some colors. You want each color to have a property, let's call it
rgb, that contains their RGB value as a hexadecimal. When creating the enum constants, you must initialize it with this property:
Kotlin stores hexadecimals as integers, so the rgb property has the Int type, not the String type.
To add a member function to this class, separate it from the enum constants with a semicolon ;:
fun main() {
val red = Color.RED
println(Color.YELLOW.containsRed())
// true
}
In this example, the containsRed() member function accesses the value of the enum constant's rgb property using the this keyword and checks if the hexadecimal
value contains FF as its first bits to return a boolean value.
To create an inline value class, use the value keyword and the @JvmInline annotation:
@JvmInline
value class Email
The @JvmInline annotation instructs Kotlin to optimize the code when it is compiled. To learn more, see Annotations.
An inline value class must have a single property initialized in the class header.
Let's say that you want to create a class that collects an email address:
132
}
fun main() {
val myEmail = Email("[email protected]")
sendEmail(myEmail)
// Sending email to [email protected]
}
In the example:
Email is an inline value class that has one property in the class header: address.
The sendEmail() function accepts objects with type Email and prints a string to the standard output.
By using an inline value class, you make the class inlined and can use it directly in your code without creating an object. This can significantly reduce memory
footprint and improve your code's runtime performance.
For more information about inline value classes, see Inline value classes.
Practice
Exercise 1
You manage a delivery service and need a way to track the status of packages. Create a sealed class called DeliveryStatus, containing data classes to represent
the following statuses: Pending, InTransit, Delivered, Canceled. Complete the DeliveryStatus class declaration so that the code in the main() function runs
successfully:
fun main() {
val status1: DeliveryStatus = DeliveryStatus.Pending("Alice")
val status2: DeliveryStatus = DeliveryStatus.InTransit("2024-11-20")
val status3: DeliveryStatus = DeliveryStatus.Delivered("2024-11-18", "Bob")
val status4: DeliveryStatus = DeliveryStatus.Canceled("Address not found")
printDeliveryStatus(status1)
// The package is pending pickup from Alice.
printDeliveryStatus(status2)
// The package is in transit and expected to arrive by 2024-11-20.
printDeliveryStatus(status3)
// The package was delivered to Bob on 2024-11-18.
printDeliveryStatus(status4)
// The delivery was canceled due to: Address not found.
}
133
data class InTransit(val estimatedDeliveryDate: String) : DeliveryStatus()
data class Delivered(val deliveryDate: String, val recipient: String) : DeliveryStatus()
data class Canceled(val reason: String) : DeliveryStatus()
}
fun main() {
val status1: DeliveryStatus = DeliveryStatus.Pending("Alice")
val status2: DeliveryStatus = DeliveryStatus.InTransit("2024-11-20")
val status3: DeliveryStatus = DeliveryStatus.Delivered("2024-11-18", "Bob")
val status4: DeliveryStatus = DeliveryStatus.Canceled("Address not found")
printDeliveryStatus(status1)
// The package is pending pickup from Alice.
printDeliveryStatus(status2)
// The package is in transit and expected to arrive by 2024-11-20.
printDeliveryStatus(status3)
// The package was delivered to Bob on 2024-11-18.
printDeliveryStatus(status4)
// The delivery was canceled due to: Address not found.
}
Exercise 2
In your program, you want to be able to handle different statuses and types of errors. You have a sealed class to capture the different statuses which are declared in
data classes or objects. Complete the code below by creating an enum class called Problem that represents the different problem types: NETWORK, TIMEOUT,
and UNKNOWN.
fun main() {
val status1: Status = Status.Error(Status.Error.Problem.NETWORK)
val status2: Status = Status.OK(listOf("Data1", "Data2"))
handleStatus(status1)
// Network issue
handleStatus(status2)
// Data received: [Data1, Data2]
}
134
sealed class Status {
data object Loading : Status()
data class Error(val problem: Problem) : Status() {
enum class Problem {
NETWORK,
TIMEOUT,
UNKNOWN
}
}
fun main() {
val status1: Status = Status.Error(Status.Error.Problem.NETWORK)
val status2: Status = Status.OK(listOf("Data1", "Data2"))
handleStatus(status1)
// Network issue
handleStatus(status2)
// Data received: [Data1, Data2]
}
Next step
Intermediate: Properties
Intermediate: Properties
In the beginner tour, you learned how properties are used to declare characteristics of class instances and how to access them. This chapter digs deeper into how
properties work in Kotlin and explores other ways that you can use them in your code.
Backing fields
In Kotlin, properties have default get() and set() functions, known as property accessors, which handle retrieving and modifying their values. While these default
functions are not explicitly visible in the code, the compiler automatically generates them to manage property access behind the scenes. These accessors use a
backing field to store the actual property value.
You use the default get() or set() functions for the property.
You try to access the property value in code by using the field keyword.
get() and set() functions are also called getters and setters.
For example, this code has the category property that has no custom get() or set() functions and therefore uses the default implementations:
135
Under the hood, this is equivalent to this pseudocode:
In this example:
The get() function retrieves the property value from the field: "".
The set() function accepts value as a parameter and assigns it to the field, where value is "".
Access to the backing field is useful when you want to add extra logic in your get() or set() functions without causing an infinite loop. For example, you have a
Person class with a name property:
class Person {
var name: String = ""
}
You want to ensure that the first letter of the name property is capitalized, so you create a custom set() function that uses the .replaceFirstChar() and .uppercase()
extension functions. However, if you refer to the property directly in your set() function, you create an infinite loop and see a StackOverflowError at runtime:
class Person {
var name: String = ""
set(value) {
// This causes a runtime error
name = value.replaceFirstChar { firstChar -> firstChar.uppercase() }
}
}
fun main() {
val person = Person()
person.name = "kodee"
println(person.name)
// Exception in thread "main" java.lang.StackOverflowError
}
To fix this, you can use the backing field in your set() function instead by referencing it with the field keyword:
class Person {
var name: String = ""
set(value) {
field = value.replaceFirstChar { firstChar -> firstChar.uppercase() }
}
}
fun main() {
val person = Person()
person.name = "kodee"
println(person.name)
// Kodee
}
Backing fields are also useful when you want to add logging, send notifications when a property value changes, or use additional logic that compares the old and
new property values.
Extension properties
Just like extension functions, there are also extension properties. Extension properties allow you to add new properties to existing classes without modifying their
source code. However, extension properties in Kotlin do not have backing fields. This means that you need to write the get() and set() functions yourself.
Additionally, the lack of a backing field means that they can't hold any state.
To declare an extension property, write the name of the class that you want to extend followed by a . and the name of your property. Just like with normal class
136
properties, you need to declare a receiver type for your property. For example:
Extension properties are most useful when you want a property to contain a computed value without using inheritance. You can think of extension properties
working like a function with only one parameter: the receiver object.
For example, let's say that you have a data class called Person with two properties: firstName and lastName.
You want to be able to access the person's full name without modifying the Person data class or inheriting from it. You can do this by creating an extension
property with a custom get() function:
fun main() {
val person = Person(firstName = "John", lastName = "Doe")
Just like with extension functions, the Kotlin standard library uses extension properties widely. For example, see the lastIndex property for a CharSequence.
Delegated properties
You already learned about delegation in the Classes and interfaces chapter. You can also use delegation with properties to delegate their property accessors to
another object. This is useful when you have more complex requirements for storing properties that a simple backing field can't handle, such as storing values in a
database table, browser session, or map. Using delegated properties also reduces boilerplate code because the logic for getting and setting your properties is
contained only in the object that you delegate to.
The syntax is similar to using delegation with classes but operates on a different level. Declare your property, followed by the by keyword and the object you want to
delegate to. For example:
Here, the delegated property displayName refers to the Delegate object for its property accessors.
Every object you delegate to must have a getValue() operator function, which Kotlin uses to retrieve the value of the delegated property. If the property is mutable, it
must also have a setValue() operator function for Kotlin to set its value.
By default, the getValue() and setValue() functions have the following construction:
In these functions:
The operator keyword marks these functions as operator functions, enabling them to overload the get() and set() functions.
The thisRef parameter refers to the object containing the delegated property. By default, the type is set to Any?, but you may need to declare a more specific
type.
The property parameter refers to the property whose value is accessed or changed. You can use this parameter to access information like the property's name
137
or type. By default, the type is set to Any?. You don't need to worry about changing this in your code.
The getValue() function has a return type of String by default, but you can adjust this if you want.
The setValue() function has an additional parameter value, which is used to hold the new value that's assigned to the property.
So, how does this look in practice? Suppose you want to have a computed property, like a user's display name, that is calculated only once because the operation
is expensive and your application is performance-sensitive. You can use a delegated property to cache the display name so that it is only computed once but can
be accessed anytime without performance impact.
First, you need to create the object to delegate to. In this case, the object will be an instance of the CachedStringDelegate class:
class CachedStringDelegate {
var cachedValue: String? = null
}
The cachedValue property contains the cached value. Within the CachedStringDelegate class, add the behavior that you want from the get() function of the
delegated property to the getValue() operator function body:
class CachedStringDelegate {
var cachedValue: String? = null
The getValue() function checks whether the cachedValue property is null. If it is, the function assigns the "Default value" and prints a string for logging purposes. If
the cachedValue property has already been computed, the property isn't null. In this case, another string is printed for logging purposes. Finally, the function uses
the Elvis operator to return the cached value or "Unknown" if the value is null.
Now you can delegate the property that you want to cache (val displayName) to an instance of the CachedStringDelegate class:
class CachedStringDelegate {
var cachedValue: String? = null
fun main() {
val user = User("John", "Doe")
This example:
138
Creates a User class that has two properties in the header, firstName, and lastName, and one property in the class body, displayName.
Prints the result of accessing the displayName property on the user instance.
Note that in the getValue() function, the type for the thisRef parameter is narrowed from Any? type to the object type: User. This is so that the compiler can access
the firstName and lastName properties of the User class.
Standard delegates
The Kotlin standard library provides some useful delegates for you so you don't have to always create yours from scratch. If you use one of these delegates, you
don't need to define getValue() and setValue() functions because the standard library automatically provides them.
Lazy properties
To initialize a property only when it's first accessed, use a lazy property. The standard library provides the Lazy interface for delegation.
To create an instance of the Lazy interface, use the lazy() function by providing it with a lambda expression to execute when the get() function is called for the first
time. Any further calls of the get() function return the same result that was provided on the first call. Lazy properties use the trailing lambda syntax to pass the
lambda expression.
For example:
class Database {
fun connect() {
println("Connecting to the database...")
}
fun fetchData() {
val data = databaseConnection.query("SELECT * FROM data")
println("Data: $data")
}
fun main() {
// First time accessing databaseConnection
fetchData()
// Connecting to the database...
// Data: [Data1, Data2, Data3]
In this example:
The connect() function prints a string to the console, and the query() function accepts an SQL query and returns a list.
139
Returns the instance.
Creates an SQL query by calling the query() function on the databaseConnection property.
The main() function calls the fetchData() function. The first time it is called, the lazy property is initialized. The second time, the same result is returned as the first
call.
Lazy properties are useful not only when initialization is resource-intensive but also when a property might not be used in your code. Additionally, lazy properties are
thread-safe by default, which is particularly beneficial if you are working in a concurrent environment.
Observable properties
To monitor whether the value of a property changes, use an observable property. An observable property is useful when you want to detect a change in the
property value and use this knowledge to trigger a reaction. The standard library provides the Delegates object for delegation.
To create an observable property, you must first import kotlin.properties.Delegates.observable. Then, use the observable() function and provide it with a lambda
expression to execute whenever the property changes. Just like with lazy properties, observable properties use the trailing lambda syntax to pass the lambda
expression.
For example:
import kotlin.properties.Delegates.observable
class Thermostat {
var temperature: Double by observable(20.0) { _, old, new ->
if (new > 25) {
println("Warning: Temperature is too high! ($old°C -> $new°C)")
} else {
println("Temperature updated: $old°C -> $new°C")
}
}
}
fun main() {
val thermostat = Thermostat()
thermostat.temperature = 22.5
// Temperature updated: 20.0°C -> 22.5°C
thermostat.temperature = 27.0
// Warning: Temperature is too high! (22.5°C -> 27.0°C)
}
In this example:
The observable() function accepts 20.0 as a parameter and uses it to initialize the property.
Checks if the new parameter is greater than 25 and, depending on the result, prints a string to console.
140
Updates the value of the temperature property of the instance to 22.5, which triggers a print statement with a temperature update.
Updates the value of the temperature property of the instance to 27.0, which triggers a print statement with a warning.
Observable properties are useful not only for logging and debugging purposes. You can also use them for use cases like updating a UI or to perform additional
checks, like verifying the validity of data.
Practice
Exercise 1
You manage an inventory system at a bookstore. The inventory is stored in a list where each item represents the quantity of a specific book. For example, listOf(3,
0, 7, 12) means the store has 3 copies of the first book, 0 of the second, 7 of the third, and 12 of the fourth.
Write a function called findOutOfStockBooks() that returns a list of indices for all the books that are out of stock.
Hint 1
Use the indices extension property from the standard library.
Hint 2
You can use the buildList() function to create and manage a list instead of manually creating and returning a mutable list. The buildList() function uses a lambda with
a receiver, which you learned about in earlier chapters.
fun main() {
val inventory = listOf(3, 0, 7, 0, 5)
println(findOutOfStockBooks(inventory))
// [1, 3]
}
fun main() {
val inventory = listOf(3, 0, 7, 0, 5)
println(findOutOfStockBooks(inventory))
// [1, 3]
}
fun main() {
val inventory = listOf(3, 0, 7, 0, 5)
println(findOutOfStockBooks(inventory))
// [1, 3]
}
Exercise 2
141
You have a travel app that needs to display distances in both kilometers and miles. Create an extension property for the Double type called asMiles to convert a
distance in kilometers to miles:
Hint
Remember that extension properties need a custom get() function.
fun main() {
val distanceKm = 5.0
println("$distanceKm km is ${distanceKm.asMiles} miles")
// 5.0 km is 3.106855 miles
fun main() {
val distanceKm = 5.0
println("$distanceKm km is ${distanceKm.asMiles} miles")
// 5.0 km is 3.106855 miles
Exercise 3
You have a system health checker that can determine the state of a cloud system. However, the two functions it can run to perform a health check are performance
intensive. Use lazy properties to initialize the checks so that the expensive functions are only run when needed:
fun main() {
// Write your code here
when {
isAppServerHealthy -> println("Application server is online and healthy")
isDatabaseHealthy -> println("Database is healthy")
else -> println("System is offline")
}
// Performing application server health check...
// Application server is online and healthy
}
142
return false
}
fun main() {
val isAppServerHealthy by lazy { checkAppServer() }
val isDatabaseHealthy by lazy { checkDatabase() }
when {
isAppServerHealthy -> println("Application server is online and healthy")
isDatabaseHealthy -> println("Database is healthy")
else -> println("System is offline")
}
// Performing application server health check...
// Application server is online and healthy
}
Exercise 4
You're building a simple budget tracker app. The app needs to observe changes to the user's remaining budget and notify them whenever it goes below a certain
threshold. You have a Budget class that is initialized with a totalBudget property that contains the initial budget amount. Within the class, create an observable
property called remainingBudget that prints:
A warning when the value is lower than 20% of the initial budget.
An encouraging message when the budget is increased from the previous value.
import kotlin.properties.Delegates.observable
fun main() {
val myBudget = Budget(totalBudget = 1000)
myBudget.remainingBudget = 800
myBudget.remainingBudget = 150
// Warning: Your remaining budget (150) is below 20% of your total budget.
myBudget.remainingBudget = 50
// Warning: Your remaining budget (50) is below 20% of your total budget.
myBudget.remainingBudget = 300
// Good news: Your remaining budget increased to 300.
}
import kotlin.properties.Delegates.observable
fun main() {
val myBudget = Budget(totalBudget = 1000)
myBudget.remainingBudget = 800
myBudget.remainingBudget = 150
// Warning: Your remaining budget (150) is below 20% of your total budget.
myBudget.remainingBudget = 50
// Warning: Your remaining budget (50) is below 20% of your total budget.
myBudget.remainingBudget = 300
// Good news: Your remaining budget increased to 300.
}
Next step
Intermediate: Null safety
143
Intermediate: Null safety
In the beginner tour, you learned how to handle null values in your code. This chapter covers common use cases for null safety features and how to make the most
of them.
is checks if the object has the type and returns a boolean value.
!is checks if the object doesn't have the type and returns a boolean value.
For example:
fun main() {
val myInt = 42
val myDouble = 3.14
val myList = listOf(1, 2, 3)
You've already seen an example of how to use a when conditional expression with the is and !is operators in the Open and other special classes chapter.
fun main() {
val a: String? = null
val b = a as String
To explicitly cast an object to a non-nullable type, but return null instead of throwing an error on failure, use the as? operator. Since the as? operator doesn't trigger
an error on failure, it is called the safe operator.
144
fun main() {
val a: String? = null
val b = a as? String
You can combine the as? operator with the Elvis operator ?: to reduce several lines of code down to one. For example, the following calculateTotalStringLength()
function calculates the total length of all strings provided in a mixed list:
return totalLength
}
The example:
The example uses the .sumOf() extension function and provides a lambda expression that:
For each item in the list, performs a safe cast to String using as?.
Uses a safe call ?. to access the length property if the call doesn't return a null value.
Uses the Elvis operator ?: to return 0 if the safe call returns a null value.
fun main() {
val emails: List<String?> = listOf("[email protected]", null, "[email protected]", null, "[email protected]")
println(validEmails)
// [[email protected], [email protected], [email protected]]
}
145
If you want to perform filtering of null values directly when creating a list, use the listOfNotNull() function:
fun main() {
val serverConfig = mapOf(
"appConfig.json" to "App Configuration",
"dbConfig.json" to "Database Configuration"
)
println(configFiles)
// [App Configuration]
}
In both of these examples, if all items are null values, an empty list is returned.
Kotlin also provides functions that you can use to find values in collections. If a value isn't found, they return null values instead of triggering an error:
singleOrNull() looks for only one item by its exact value. If one doesn't exist or there are multiple items with the same value, returns a null value.
maxOrNull() finds the highest value. If one doesn't exist, returns a null value.
minOrNull() finds the lowest value. If one doesn't exist, returns a null value.
For example:
fun main() {
// Temperatures recorded over a week
val temperatures = listOf(15, 18, 21, 21, 19, 17, 16)
This example uses the Elvis operator ?: to return a printed statement if the functions return a null value.
The singleOrNull(), maxOrNull(), and minOrNull() functions are designed to be used with collections that don't contain null values. Otherwise, you can't tell
whether the function couldn't find the desired value or whether it found a null value.
Some functions use a lambda expression to transform a collection and return null values if they can't fulfill their purpose.
For example, to transform a collection with a lambda expression and return the first value that isn't null, use the firstNotNullOfOrNull() function. If no such value
exists, the function returns a null value:
fun main() {
data class User(val name: String?, val age: Int?)
146
To use a lambda function to process each collection item sequentially and create an accumulated value (or return a null value if the collection is empty) use the
reduceOrNull() function:
fun main() {
// Prices of items in a shopping cart
val itemPrices = listOf(20, 35, 15, 40, 10)
This example also uses the Elvis operator ?: to return a printed statement if the function returns a null value.
The reduceOrNull() function is designed to be used with collections that don't contain null values.
Explore Kotlin's standard library to find more functions that you can use to make your code safer.
fun main() {
// Creates some sample users
val user1 = User(1, "Alice", listOf(2, 3))
val user2 = User(2, "Bob", listOf(1))
val user3 = User(3, "Charlie", listOf(1))
println(getNumberOfFriends(users, 1))
// 2
println(getNumberOfFriends(users, 2))
// 1
println(getNumberOfFriends(users, 4))
// -1
}
In this example:
There is a User data class that has properties for the user's id, name and list of friends.
147
Accesses the value of the map of User instances with the provided user ID.
Uses an Elvis operator to return the function early with the value of -1 if the map value is a null value.
Assigns the value found from the map to the user variable.
Returns the number of friends in the user's friends list by using the size property.
Creates a map of these User instances and assigns them to the users variable.
Calls the getNumberOfFriends() function on the users variable with values 1 and 2 that returns two friends for "Alice" and one friend for "Bob".
Calls the getNumberOfFriends() function on the users variable with value 4, which triggers an early return with a value of -1.
You may notice that the code could be more concise without an early return. However, this approach needs multiple safe calls because the users[userId] might
return a null value, making the code slightly harder to read:
Although this example checks only one condition with the Elvis operator, you can add multiple checks to cover any critical error paths. Early returns with the Elvis
operator prevent your program from doing unnecessary work and make your code safer by stopping as soon as a null value or invalid case is detected.
For more information about how you can use return in your code, see Returns and jumps.
Practice
Exercise 1
You are developing a notification system for an app where users can enable or disable different types of notifications. Complete the getNotificationPreferences()
function so that:
1. The validUser variable uses the as? operator to check if user is an instance of the User class. If it isn't, return an empty list.
2. The userName variable uses the Elvis ?: operator to ensure that the user's name defaults to "Guest" if it is null.
3. The final return statement uses the .takeIf() function to include email and SMS notification preferences only if they are enabled.
4. The main() function runs successfully and prints the expected output.
The takeIf() function returns the original value if the given condition is true, otherwise it returns null. For example:
fun main() {
// The user is logged in
val userIsLoggedIn = true
// The user has an active session
val hasSession = true
148
fun getNotificationPreferences(user: Any, emailEnabled: Boolean, smsEnabled: Boolean): List<String> {
val validUser = // Write your code here
val userName = // Write your code here
fun main() {
val user1 = User("Alice")
val user2 = User(null)
val invalidUser = "NotAUser"
return listOfNotNull(
"Email Notifications enabled for $userName".takeIf { emailEnabled },
"SMS Notifications enabled for $userName".takeIf { smsEnabled }
)
}
fun main() {
val user1 = User("Alice")
val user2 = User(null)
val invalidUser = "NotAUser"
Exercise 2
You are working on a subscription-based streaming service where users can have multiple subscriptions, but only one can be active at a time. Complete the
getActiveSubscription() function so that it uses the singleOrNull() function with a predicate to return a null value if there is more than one active subscription:
fun main() {
val userWithPremiumPlan = listOf(
Subscription("Basic Plan", false),
Subscription("Premium Plan", true)
)
println(getActiveSubscription(userWithPremiumPlan))
// Subscription(name=Premium Plan, isActive=true)
println(getActiveSubscription(userWithConflictingPlans))
// null
}
149
fun getActiveSubscription(subscriptions: List<Subscription>): Subscription? {
return subscriptions.singleOrNull { subscription -> subscription.isActive }
}
fun main() {
val userWithPremiumPlan = listOf(
Subscription("Basic Plan", false),
Subscription("Premium Plan", true)
)
println(getActiveSubscription(userWithPremiumPlan))
// Subscription(name=Premium Plan, isActive=true)
println(getActiveSubscription(userWithConflictingPlans))
// null
}
fun main() {
val userWithPremiumPlan = listOf(
Subscription("Basic Plan", false),
Subscription("Premium Plan", true)
)
println(getActiveSubscription(userWithPremiumPlan))
// Subscription(name=Premium Plan, isActive=true)
println(getActiveSubscription(userWithConflictingPlans))
// null
}
Exercise 3
You are working on a social media platform where users have usernames and account statuses. You want to see the list of currently active usernames. Complete
the getActiveUsernames() function so that the mapNotNull() function has a predicate that returns the username if it is active or a null value if it isn't:
fun main() {
val allUsers = listOf(
User("alice123", true),
User("bob_the_builder", false),
User("charlie99", true)
)
println(getActiveUsernames(allUsers))
// [alice123, charlie99]
}
Just like in Exercise 1, you can use the takeIf() function when you check if the user is active.
150
data class User(val username: String, val isActive: Boolean)
fun main() {
val allUsers = listOf(
User("alice123", true),
User("bob_the_builder", false),
User("charlie99", true)
)
println(getActiveUsernames(allUsers))
// [alice123, charlie99]
}
fun main() {
val allUsers = listOf(
User("alice123", true),
User("bob_the_builder", false),
User("charlie99", true)
)
println(getActiveUsernames(allUsers))
// [alice123, charlie99]
}
Exercise 4
You are working on an inventory management system for an e-commerce platform. Before processing a sale, you need to check if the requested quantity of a
product is valid based on the available stock.
Complete the validateStock() function so that it uses early returns and the Elvis operator (where applicable) to check if:
The amount in the requested variable is higher than in the available variable.
In all of the above cases, the function must return early with the value of -1.
fun main() {
println(validateStock(5,10))
// 5
println(validateStock(null,10))
// -1
println(validateStock(-2,10))
// -1
}
151
return validRequested
}
fun main() {
println(validateStock(5,10))
// 5
println(validateStock(null,10))
// -1
println(validateStock(-2,10))
// -1
}
Next step
Intermediate: Libraries and APIs
Libraries distribute reusable code that simplifies common tasks. Within libraries, there are packages and objects that group related classes, functions, and utilities.
Libraries expose APIs (Application Programming Interfaces) as a set of functions, classes, or properties that developers can use in their code.
fun main() {
val text = "emosewa si niltoK"
However, some parts of the standard library require an import before you can use them in your code. For example, if you want to use the standard library's time
measurement features, you need to import the kotlin.time package.
At the top of your file, add the import keyword followed by the package that you need:
import kotlin.time.*
The asterisk * is a wildcard import that tells Kotlin to import everything within the package. You can't use the asterisk * with companion objects. Instead, you need to
explicitly declare the members of a companion object that you want to use.
152
For example:
import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
fun main() {
val thirtyMinutes: Duration = 30.minutes
val halfHour: Duration = 0.5.hours
println(thirtyMinutes == halfHour)
// true
}
This example:
Imports the Duration class and the hours and minutes extension properties from its companion object.
Collections
Sequences
String manipulation
Time management
To learn more about what else is in the standard library, explore its API reference.
Kotlin libraries
The standard library covers many common use cases, but there are some that it doesn't address. Fortunately, the Kotlin team and the rest of the community have
developed a wide range of libraries to complement the standard library. For example, kotlinx-datetime helps you manage time across different platforms.
You can find useful libraries on our search platform. To use them, you need to take extra steps, like adding a dependency or plugin. Each library has a GitHub
repository with instructions on how to include it in your Kotlin projects.
Once you add the library, you can import any package within it. Here's an example of how to import the kotlinx-datetime package to find the current time in New
York:
import kotlinx.datetime.*
fun main() {
val now = Clock.System.now() // Get current instant
println("Current instant: $now")
This example:
Uses the Clock.System.now() function to create an instance of the Instant class that contains the current time and assigns the result to the now variable.
Uses the TimeZone.of() function to find the time zone for New York and assigns the result to the zone variable.
153
Calls the .toLocalDateTime() function on the instance containing the current time, with the New York time zone as an argument.
Prints the time adjusted for the time zone in New York.
To explore the functions and classes that this example uses in more detail, see the API reference.
Opt in to APIs
Library authors may mark certain APIs as requiring opt-in before you can use them in your code. They usually do this when an API is still in development and may
change in the future. If you don't opt in, you see warnings or errors like this:
This declaration needs opt-in. Its usage should be marked with '@...' or '@OptIn(...)'
To opt in, write @OptIn followed by parentheses containing the class name that categorizes the API, appended by two colons :: and class.
For example, the uintArrayOf() function from the standard library falls under @ExperimentalUnsignedTypes, as shown in the API reference:
@ExperimentalUnsignedTypes
inline fun uintArrayOf(vararg elements: UInt): UIntArray
@OptIn(ExperimentalUnsignedTypes::class)
Here's an example that opts in to use the uintArrayOf() function to create an array of unsigned integers and modifies one of its elements:
@OptIn(ExperimentalUnsignedTypes::class)
fun main() {
// Create an unsigned integer array
val unsignedArray: UIntArray = uintArrayOf(1u, 2u, 3u, 4u, 5u)
// Modify an element
unsignedArray[2] = 42u
println("Updated array: ${unsignedArray.joinToString()}")
// Updated array: 1, 2, 42, 4, 5
}
This is the easiest way to opt in, but there are other ways. To learn more, see Opt-in requirements.
Practice
Exercise 1
You are developing a financial application that helps users calculate the future value of their investments. The formula to calculate compound interest is:
Where:
154
1. Import the necessary functions from the kotlin.math package.
2. Add a body to the calculateCompoundInterest() function that calculates the final amount after applying compound interest.
fun main() {
val principal = 1000.0
val rate = 0.05
val timesCompounded = 4
val years = 5
val amount = calculateCompoundInterest(principal, rate, timesCompounded, years)
println("The accumulated amount is: $amount")
// The accumulated amount is: 1282.0372317085844
}
import kotlin.math.*
fun main() {
val principal = 1000.0
val rate = 0.05
val timesCompounded = 4
val years = 5
val amount = calculateCompoundInterest(principal, rate, timesCompounded, years)
println("The accumulated amount is: $amount")
// The accumulated amount is: 1282.0372317085844
}
Exercise 2
You want to measure the time it takes to perform multiple data processing tasks in your program. Update the code to add the correct import statements and
functions from the kotlin.time package:
fun main() {
val timeTaken = /* Write your code here */ {
// Simulate some data processing
val data = List(1000) { it * 2 }
val filteredData = data.filter { it % 3 == 0 }
import kotlin.time.measureTime
fun main() {
val timeTaken = measureTime {
// Simulate some data processing
val data = List(1000) { it * 2 }
val filteredData = data.filter { it % 3 == 0 }
155
println("Time taken: $timeTaken") // e.g. 16 ms
}
Exercise 3
There's a new feature in the standard library available in the latest Kotlin release. You want to try it out, but it requires opt-in. The feature falls under
@ExperimentalStdlibApi. What should the opt-in look like in your code?
@OptIn(ExperimentalStdlibApi::class)
What's next?
Congratulations! You've completed the intermediate tour! As a next step, check out our tutorials for popular Kotlin applications:
Create a cross-platform application for Android and iOS from scratch and:
Expressiveness: Kotlin's innovative language features, such as its support for type-safe builders and delegated properties, help build powerful and easy-to-use
abstractions.
Scalability: Kotlin's support for coroutines helps build server-side applications that scale to massive numbers of clients with modest hardware requirements.
Interoperability: Kotlin is fully compatible with all Java-based frameworks, so you can use your familiar technology stack while reaping the benefits of a more
modern language.
Migration: Kotlin supports gradual migration of large codebases from Java to Kotlin. You can start writing new code in Kotlin while keeping older parts of your
system in Java.
Tooling: In addition to great IDE support in general, Kotlin offers framework-specific tooling (for example, for Spring and Ktor) in the plugin for IntelliJ IDEA
Ultimate.
Learning Curve: For a Java developer, getting started with Kotlin is very easy. The automated Java-to-Kotlin converter included in the Kotlin plugin helps with
your first steps. Kotlin Koans guide you through key language features with a series of interactive exercises. Kotlin-specific frameworks like Ktor offer a simple,
straightforward approach without the hidden complexities of larger frameworks.
Spring makes use of Kotlin's language features to offer more concise APIs, starting with version 5.0. The online project generator allows you to quickly generate
a new project in Kotlin.
Ktor is a framework built by JetBrains for creating Web applications in Kotlin, making use of coroutines for high scalability and offering an easy-to-use and
idiomatic API.
Quarkus provides first class support for using Kotlin. The framework is open source and maintained by Red Hat. Quarkus was built from the ground up for
Kubernetes and provides a cohesive full-stack framework by leveraging a growing list of hundreds of best-of-breed libraries.
Vert.x, a framework for building reactive Web applications on the JVM, offers dedicated support for Kotlin, including full documentation.
kotlinx.html is a DSL that can be used to build HTML in Web applications. It serves as an alternative to traditional templating systems such as JSP and
156
FreeMarker.
Micronaut is a modern JVM-based full-stack framework for building modular, easily testable microservices and serverless applications. It comes with a lot of
useful built-in features.
http4k is the functional toolkit with a tiny footprint for Kotlin HTTP applications, written in pure Kotlin. The library is based on the "Your Server as a Function"
paper from Twitter and represents modeling both HTTP servers and clients as simple Kotlin functions that can be composed together.
Javalin is a very lightweight web framework for Kotlin and Java which supports WebSockets, HTTP2, and async requests.
The available options for persistence include direct JDBC access, JPA, and using NoSQL databases through their Java drivers. For JPA, the kotlin-jpa compiler
plugin adapts Kotlin-compiled classes to the requirements of the framework.
To deploy Kotlin applications on Heroku, you can follow the official Heroku tutorial.
AWS Labs provides a sample project showing the use of Kotlin for writing AWS Lambda functions.
Google Cloud Platform offers a series of tutorials for deploying Kotlin applications to GCP, both for Ktor and App Engine and Spring and App engine. In addition,
there is an interactive code lab for deploying a Kotlin Spring application.
JetBrains Account, the system responsible for the entire license sales and validation process at JetBrains, is written in 100% Kotlin and has been running in
production since 2015 with no major issues.
Chess.com is a website dedicated to chess and the millions of players around the world who love the game. Chess.com uses Ktor for the seamless configuration of
multiple HTTP clients.
Engineers at Adobe use Kotlin for server-side app development and Ktor for prototyping in the Adobe Experience Platform, which enables organizations to
centralize and standardize customer data before applying data science and machine learning.
Next steps
For a more in-depth introduction to the language, check out the Kotlin documentation on this site and Kotlin Koans.
Explore how to build asynchronous server applications with Ktor, a framework that uses Kotlin coroutines.
Watch a webinar "Micronaut for microservices with Kotlin" and explore a detailed guide showing how you can use Kotlin extension functions in the Micronaut
framework.
http4k provides the CLI to generate fully formed projects, and a starter repo to generate an entire CD pipeline using GitHub, Travis, and Heroku with a single
bash command.
Want to migrate from Java to Kotlin? Learn how to perform typical tasks with strings in Java and Kotlin.
Over 50% of professional Android developers use Kotlin as their primary language, while only 30% use Java as their main language. 70% of developers whose
primary language is Kotlin say that Kotlin makes them more productive.
157
Less code combined with greater readability. Spend less time writing your code and working to understand the code of others.
Fewer common errors. Apps built with Kotlin are 20% less likely to crash based on Google's internal data.
Kotlin support in Jetpack libraries. Jetpack Compose is Android's recommended modern toolkit for building native UI in Kotlin. KTX extensions add Kotlin
language features, like coroutines, extension functions, lambdas, and named parameters to existing Android libraries.
Support for multiplatform development. Kotlin Multiplatform allows development for not only Android but also iOS, backend, and web applications. Some
Jetpack libraries are already multiplatform. Compose Multiplatform, JetBrains' declarative UI framework based on Kotlin and Jetpack Compose, makes it
possible to share UIs across platforms – iOS, Android, desktop, and web.
Mature language and environment. Since its creation in 2011, Kotlin has developed continuously, not only as a language but as a whole ecosystem with robust
tooling. Now it's seamlessly integrated into Android Studio and is actively used by many companies for developing Android applications.
Interoperability with Java. You can use Kotlin along with the Java programming language in your applications without needing to migrate all your code to Kotlin.
Easy learning. Kotlin is very easy to learn, especially for Java developers.
Big community. Kotlin has great support and many contributions from the community, which is growing all over the world. Over 95% of the top thousand
Android apps use Kotlin.
Many startups and Fortune 500 companies have already developed Android applications using Kotlin, see the list on the Google website for Android developers.
Android development, read Google's documentation for developing Android apps with Kotlin.
Developing cross-platform mobile applications, see Create an app with shared logic and native UI.
Kotlin/Wasm
Kotlin/Wasm is in Alpha. It may be changed at any time. You can use it in scenarios before production. We would appreciate your feedback in YouTrack.
Kotlin/Wasm has the power to compile your Kotlin code into WebAssembly (Wasm) format. With Kotlin/Wasm, you can create applications that run on different
environments and devices, which support Wasm and meet Kotlin's requirements.
Wasm is a binary instruction format for a stack-based virtual machine. This format is platform-independent because it runs on its own virtual machine. Wasm
provides Kotlin and other languages with a compilation target.
You can use Kotlin/Wasm in different target environments, such as browsers, for developing web applications built with Compose Multiplatform, or outside the
browser in standalone Wasm virtual machines. In the outside-of-browser case, WebAssembly System Interface (WASI) provides access to platform APIs, which you
can also utilize.
Compose Multiplatform is a declarative framework based on Kotlin and Jetpack Compose that allows you to implement the UI once and share it across all the
platforms you target.
For web platforms, Compose Multiplatform uses Kotlin/Wasm as its compilation target. Applications built with Kotlin/Wasm and Compose Multiplatform use a
wasm-js target and run in browsers.
Explore our online demo of an application built with Compose Multiplatform and Kotlin/Wasm
158
Kotlin/Wasm demo
To run applications built with Kotlin/Wasm in a browser, you need a browser version that supports the new garbage collection and legacy exception
handling proposals. To check the browser support status, see the WebAssembly roadmap.
Additionally, you can use the most popular Kotlin libraries in Kotlin/Wasm out of the box. Like in other Kotlin and Multiplatform projects, you can include
dependency declarations in the build script. For more information, see Adding dependencies on multiplatform libraries.
Kotlin/Wasm leverages WASI to abstract away platform-specific details, allowing the same Kotlin code to run across diverse platforms. This expands the reach of
Kotlin/Wasm beyond web applications without requiring custom handling for each runtime.
WASI provides a secure standard interface for running Kotlin applications compiled to WebAssembly across different environments.
To see Kotlin/Wasm and WASI in action, check the Get started with Kotlin/Wasm and WASI tutorial.
Kotlin/Wasm performance
Although Kotlin/Wasm is still in Alpha, Compose Multiplatform running on Kotlin/Wasm already shows encouraging performance traits. You can see that its
execution speed outperforms JavaScript and is approaching that of the JVM:
159
Kotlin/Wasm performance
We regularly run benchmarks on Kotlin/Wasm, and these results come from our testing in a recent version of Google Chrome.
The declarations for browser API support are defined using JavaScript interoperability capabilities. You can use the same capabilities to define your own
declarations. In addition, Kotlin/Wasm–JavaScript interoperability allows you to use Kotlin code from JavaScript. For more information, see Use Kotlin code in
JavaScript.
Leave feedback
Kotlin/Wasm feedback
Slack: Get a Slack invite and provide your feedback directly to developers in our #webassembly channel.
Learn more
160
Learn more about Kotlin/Wasm in this YouTube playlist.
Kotlin/Native
Kotlin/Native is a technology for compiling Kotlin code to native binaries that can run without a virtual machine. Kotlin/Native includes an LLVM-based backend for
the Kotlin compiler and a native implementation of the Kotlin standard library.
Why Kotlin/Native?
Kotlin/Native is primarily designed to allow compilation for platforms on which virtual machines are not desirable or possible, such as embedded devices or iOS. It's
ideal for situations when you need to to produce a self-contained program that doesn't require an additional runtime or a virtual machine.
It's easy to include compiled Kotlin code in existing projects written in C, C++, Swift, Objective-C, and other languages. You can also use existing native code,
static or dynamic C libraries, Swift/Objective-C frameworks, graphical engines, and anything else directly from Kotlin/Native.
Target platforms
Kotlin/Native supports the following platforms:
Linux
Android NDK
To compile Apple targets, you need to install Xcode and its command-line tools.
Interoperability
Kotlin/Native supports two-way interoperability with native programming languages for different operating systems. The compiler can create executables for many
platforms, static or dynamic C libraries, and Swift/Objective-C frameworks.
Interoperability with C
Kotlin/Native provides interoperability with C. You can use existing C libraries directly from Kotlin code.
161
Kotlin/Native provides interoperability with Swift through Objective-C. You can use Kotlin code directly from Swift/Objective-C applications on macOS and iOS.
Kotlin/Native is a part of the Kotlin Multiplatform technology that helps share common code across multiple platforms, including Android, iOS, JVM, web, and
native. Multiplatform libraries provide the necessary APIs for common Kotlin code and allow writing shared parts of projects in Kotlin all in one place.
Memory manager
Kotlin/Native uses an automatic memory manager that is similar to the JVM and Go. It has its own tracing garbage collector, which is also integrated with
Swift/Objective-C's ARC.
The memory consumption is controlled by a custom memory allocator. It optimizes memory usage and helps prevent sudden surges in memory allocation.
The recommended way to use Kotlin/JS is via the kotlin.multiplatform Gradle plugin. It lets you easily set up and control Kotlin projects targeting JavaScript in one
place. This includes essential functionality such as controlling the bundling of your application, adding JavaScript dependencies directly from npm, and more. To
get an overview of the available options, check out Set up a Kotlin/JS project.
Kotlin/JS IR compiler
The Kotlin/JS IR compiler comes with a number of improvements over the old default compiler. For example, it reduces the size of generated executables via dead
code elimination and provides smoother interoperability with the JavaScript ecosystem and its tooling.
The old compiler has been deprecated since the Kotlin 1.8.0 release.
By generating TypeScript declaration files (d.ts) from Kotlin code, the IR compiler makes it easier to create "hybrid" applications that mix TypeScript and Kotlin code
and to leverage code-sharing functionality using Kotlin Multiplatform.
To learn more about the available features in the Kotlin/JS IR compiler and how to try it for your project, visit the Kotlin/JS IR compiler documentation page and the
migration guide.
Kotlin/JS frameworks
Modern web development benefits significantly from frameworks that simplify building web applications. Here are a few examples of popular web frameworks for
Kotlin/JS written by different authors:
Kobweb
Kobweb is an opinionated Kotlin framework for creating websites and web apps. It leverages Compose HTML and live-reloading for fast development. Inspired by
Next.js, Kobweb promotes a standard structure for adding widgets, layouts, and pages.
Out of the box, Kobweb provides page routing, light/dark mode, CSS styling, Markdown support, backend APIs, and more features. It also includes a UI library
called Silk, a set of versatile widgets for modern UIs.
Kobweb also supports site export, generating page snapshots for SEO and automatic search indexing. Additionally, Kobweb makes it easy to create DOM-based
UIs that efficiently update in response to state changes.
162
Visit the Kobweb site for documentation and examples.
For updates and discussions about the framework, join the #kobweb and #compose-web channels in the Kotlin Slack.
KVision
KVision is an object-oriented web framework that makes it possible to write applications in Kotlin/JS with ready-to-use components that can be used as building
blocks for your application's user interface. You can use both reactive and imperative programming models to build your frontend, use connectors for Ktor, Spring
Boot, and other frameworks to integrate it with your server-side applications, and share code using Kotlin Multiplatform.
For updates and discussions about the framework, join the #kvision and #javascript channels in the Kotlin Slack.
fritz2
fritz2 is a standalone framework for building reactive web user interfaces. It provides its own type-safe DSL for building and rendering HTML elements, and it makes
use of Kotlin's coroutines and flows to express components and their data bindings. It provides state management, validation, routing, and more out of the box, and
integrates with Kotlin Multiplatform projects.
For updates and discussions about the framework, join the #fritz2 and #javascript channels in the Kotlin Slack.
Doodle
Doodle is a vector-based UI framework for Kotlin/JS. Doodle applications use the browser's graphics capabilities to draw user interfaces instead of relying on
DOM, CSS, or Javascript. By using this approach, Doodle gives you precise control over the rendering of arbitrary UI elements, vector shapes, gradients, and
custom visualizations.
For updates and discussions about the framework, join the #doodle and #javascript channels in the Kotlin Slack.
Let's think about software development duties where data analysis is key: analyzing what's actually inside collections when debugging, digging into memory dumps
or databases, or receiving JSON files with large amounts of data when working with REST APIs, to mention some.
With Kotlin's Exploratory Data Analysis (EDA) tools, such as Kotlin notebooks, Kotlin DataFrame, and Kandy, you have at your disposal a rich set of capabilities to
enhance your analytics skills and support you across different scenarios:
Load, transform, and visualize data in various formats: with our Kotlin EDA tools, you can perform tasks like filtering, sorting, and aggregating data. Our tools can
seamlessly read data right in the IDE from different file formats, including CSV, JSON, and TXT.
Kandy, our plotting tool, allows you to create a wide range of charts to visualize and gain insights from the dataset.
Efficiently analyze data stored in relational databases: Kotlin DataFrame seamlessly integrates with databases and provides capabilities similar to SQL queries.
You can retrieve, manipulate, and visualize data directly from various databases.
Fetch and analyze real-time and dynamic datasets from web APIs: the EDA tools' flexibility allows integration with external APIs via protocols like OpenAPI. This
feature helps you fetch data from web APIs, to then clean and transform the data to your needs.
Would you like to try our Kotlin tools for data analysis?
163
Get started with Kotlin Notebook
Our Kotlin data analysis tools let you smoothly handle your data from start to finish. Effortlessly retrieve your data with simple drag-and-drop functionality in our
Kotlin Notebook. Clean, transform, and visualize it with just a few lines of code. Additionally, export your output charts in a matter of clicks.
Notebooks
Notebooks are interactive editors that integrate code, graphics, and text in a single environment. When using a notebook, you can run code cells and immediately
see the output.
Kotlin offers different notebook solutions, such as Kotlin Notebook, Datalore, and Kotlin-Jupyter Notebook, providing convenient features for data retrieving,
transformation, exploration, modeling, and more. These Kotlin notebook solutions are based on our Kotlin Kernel.
You can seamlessly share your code among Kotlin Notebook, Datalore, and Kotlin-Jupyter Notebook. Create a project in one of our Kotlin notebooks and continue
working in another notebook without compatibility issues.
Benefit from the features of our powerful Kotlin notebooks and the perks of coding with Kotlin. Kotlin integrates with these notebooks to help you manage data and
share your findings with colleagues while building up your data science and machine learning skills.
Discover the features of our different Kotlin notebook solutions and choose the one that best aligns with your project requirements.
164
Kotlin Notebook
Kotlin Notebook
The Kotlin Notebook is a plugin for IntelliJ IDEA that allows you to create notebooks in Kotlin. It provides our IDE experience with all common IDE features, offering
real-time code insights and project integration.
Kotlin DataFrame
165
The Kotlin DataFrame library lets you manipulate structured data in your Kotlin projects. From data creation and cleaning to in-depth analysis and feature
engineering, this library has you covered.
With the Kotlin DataFrame library, you can work with different file formats, including CSV, JSON, XLS, and XLSX. This library also facilitates the data retrieval
process with its ability to connect with SQL databases or APIs.
Kotlin DataFrame
Kandy
Kandy is an open-source Kotlin library that provides a powerful and flexible DSL for plotting charts of various types. This library is a simple, idiomatic, readable, and
type-safe tool to visualize data.
Kandy has seamless integration with Kotlin Notebook, Datalore, and Kotlin-Jupyter Notebook. You can also easily combine the Kandy and Kotlin DataFrame
libraries to complete different data-related tasks.
166
Kandy
What's next
Get started with Kotlin Notebook
Learn more about Kotlin and Java libraries for data analysis
This page introduces how Kotlin is used in real-world AI scenarios with working examples from the Kotlin-AI-Examples repository.
167
Koog is a Kotlin-based framework for creating and running AI agents locally, without requiring external services. Koog is JetBrains' innovative, open-source agentic
framework that empowers developers to build AI agents within the JVM ecosystem. It provides a pure Kotlin implementation for building intelligent agents that can
interact with tools, handle complex workflows, and communicate with users.
Retrieval-augmented generation
Use Kotlin to build retrieval-augmented generation (RAG) pipelines that connect language models to external sources like documentation, vector stores, or APIs.
For example:
springAI-demo: A Spring Boot app that loads Kotlin standard library docs into a vector store and supports document-based Q&A.
Agent-based applications
Build AI agents in Kotlin that reason, plan, and act using language models and tools. For example:
koog: Shows how to use the Kotlin agentic framework Koog to build AI agents.
mcp-demo: A desktop UI that connects to Claude and OpenAI, and presents responses using Compose Multiplatform.
Explore examples
You can explore and run examples from the Kotlin-AI-Examples repository.
Each project is self-contained. You can use each project as a reference or template for building Kotlin-based AI applications.
What's next
Complete the Build a Kotlin app that uses Spring AI to answer questions based on documents stored in the Qdrant tutorial to learn more about using Spring AI
with Kotlin in IntelliJ IDEA
Join the Kotlin community to connect with other developers building AI applications with Kotlin
168
Kotlin for competitive programming
This tutorial is designed both for competitive programmers that did not use Kotlin before and for Kotlin developers that did not participate in any competitive
programming events before. It assumes the corresponding programming skills.
Competitive programming is a mind sport where contestants write programs to solve precisely specified algorithmic problems within strict constraints. Problems
can range from simple ones that can be solved by any software developer and require little code to get a correct solution, to complex ones that require knowledge
of special algorithms, data structures, and a lot of practice. While not being specifically designed for competitive programming, Kotlin incidentally fits well in this
domain, reducing the typical amount of boilerplate that a programmer needs to write and read while working with the code almost to the level offered by
dynamically-typed scripting languages, while having tooling and performance of a statically-typed language.
See Get started with Kotlin/JVM on how to set up development environment for Kotlin. In competitive programming, a single project is usually created and each
problem's solution is written in a single source file.
Codeforces Round 555 was held on April 26th for 3rd Division, which means it had problems fit for any developer to try. You can use this link to read the problems.
The simplest problem in the set is the Problem A: Reachable Numbers. It asks to implement a straightforward algorithm described in the problem statement.
We'd start solving it by creating a Kotlin source file with an arbitrary name. A.kt will do well. First, you need to implement a function specified in the problem
statement as:
Let's denote a function f(x) in such a way: we add 1 to x, then, while there is at least one trailing zero in the resulting number, we remove that zero.
Kotlin is a pragmatic and unopinionated language, supporting both imperative and function programming styles without pushing the developer towards either one.
You can implement the function f in functional style, using such Kotlin features as tail recursion:
Alternatively, you can write an imperative implementation of the function f using the traditional while loop and mutable variables that are denoted in Kotlin with var:
Types in Kotlin are optional in many places due to pervasive use of type-inference, but every declaration still has a well-defined static type that is known at
compilation.
Now, all is left is to write the main function that reads the input and implements the rest of the algorithm that the problem statement asks for — to compute the
number of different integers that are produced while repeatedly applying function f to the initial number n that is given in the standard input.
By default, Kotlin runs on JVM and gives direct access to a rich and efficient collections library with general-purpose collections and data-structures like
dynamically-sized arrays (ArrayList), hash-based maps and sets (HashMap/HashSet), tree-based ordered maps and sets (TreeMap/TreeSet). Using a hash-set of
integers to track values that were already reached while applying function f, the straightforward imperative version of a solution to the problem can be written as
shown below:
fun main() {
var n = readln().toInt() // read integer from the input
val reached = HashSet<Int>() // a mutable hash set
while (reached.add(n)) n = f(n) // iterate function f
println(reached.size) // print answer to the output
}
There is no need to handle the case of misformatted input in competitive programming. An input format is always precisely specified in competitive programming, and the actual input
cannot deviate from the input specification in the problem statement. That's why you can use Kotlin's readln() function. It asserts that the input string is present and throws an exception
169
otherwise. Likewise, the String.toInt() function throws an exception if the input string is not an integer.
Earlier versions
fun main() {
var n = readLine()!!.toInt() // read integer from the input
val reached = HashSet<Int>() // a mutable hash set
while (reached.add(n)) n = f(n) // iterate function f
println(reached.size) // print answer to the output
}
Note the use of Kotlin's null-assertion operator !! after the readLine() function call. Kotlin's readLine() function is defined to return a nullable type String? and returns null on the end of the
input, which explicitly forces the developer to handle the case of missing input.
There is no need to handle the case of misformatted input in competitive programming. In competitive programming, an input format is always precisely specified and the actual input
cannot deviate from the input specification in the problem statement. That's what the null-assertion operator !! essentially does — it asserts that the input string is present and throws an
exception otherwise. Likewise, the String.toInt().
All online competitive programming events allow the use of pre-written code, so you can define your own library of utility functions that are geared towards
competitive programming to make your actual solution code somewhat easier to read and write. You would then use this code as a template for your solutions. For
example, you can define the following helper functions for reading inputs in competitive programming:
Earlier versions
Note the use of private visibility modifier here. While the concept of visibility modifier is not relevant for competitive programming at all, it allows you to place
multiple solution files based on the same template without getting an error for conflicting public declarations in the same package.
fun main() {
// read input
val n = readln().toInt()
val s = readln()
val fl = readln().split(" ").map { it.toInt() }
// define local function f
fun f(c: Char) = '0' + fl[c - '1']
// greedily find first and last indices
val i = s.indexOfFirst { c -> f(c) > c }
.takeIf { it >= 0 } ?: s.length
val j = s.withIndex().indexOfFirst { (j, c) -> j > i && f(c) < c }
.takeIf { it >= 0 } ?: s.length
// compose and write the answer
val ans =
s.substring(0, i) +
s.substring(i, j).map { c -> f(c) }.joinToString("") +
s.substring(j)
println(ans)
}
170
Earlier versions
fun main() {
// read input
val n = readLine()!!.toInt()
val s = readLine()!!
val fl = readLine()!!.split(" ").map { it.toInt() }
// define local function f
fun f(c: Char) = '0' + fl[c - '1']
// greedily find first and last indices
val i = s.indexOfFirst { c -> f(c) > c }
.takeIf { it >= 0 } ?: s.length
val j = s.withIndex().indexOfFirst { (j, c) -> j > i && f(c) < c }
.takeIf { it >= 0 } ?: s.length
// compose and write the answer
val ans =
s.substring(0, i) +
s.substring(i, j).map { c -> f(c) }.joinToString("") +
s.substring(j)
println(ans)
}
In this dense code, in addition to collection transformations, you can see such handy Kotlin features as local functions and the elvis operator ?: that allow to express
idioms like "take the value if it is positive or else use length" with a concise and readable expressions like .takeIf { it >= 0 } ?: s.length, yet it is perfectly fine with
Kotlin to create additional mutable variables and express the same code in imperative style, too.
To make reading the input in competitive programming tasks like this more concise, you can have the following list of helper input-reading functions:
Earlier versions
With these helpers, the part of code for reading input becomes simpler, closely following the input specification in the problem statement line by line:
// read input
val n = readInt()
val s = readStr()
val fl = readInts()
Note that in competitive programming it is customary to give variables shorter names than it is typical in industrial programming practice, since the code is to be
written just once and not supported thereafter. However, these names are usually still mnemonic — a for arrays, i, j, and others for indices, r, and c for row and
column numbers in tables, x and y for coordinates, and so on. It is easier to keep the same names for input data as it is given in the problem statement. However,
more complex problems require more code which leads to using longer self-explanatory variable and function names.
In Kotlin this line can be concisely parsed with the following statement using destructuring declaration from a list of integers:
171
It might be temping to use JVM's java.util.Scanner class to parse less structured input formats. Kotlin is designed to interoperate well with JVM libraries, so that
their use feels quite natural in Kotlin. However, beware that java.util.Scanner is extremely slow. So slow, in fact, that parsing 105 or more integers with it might not fit
into a typical 2 second time-limit, which a simple Kotlin's split(" ").map { it.toInt() } would handle.
Writing output in Kotlin is usually straightforward with println(...) calls and using Kotlin's string templates. However, care must be taken when output contains on
order of 105 lines or more. Issuing so many println calls is too slow, since the output in Kotlin is automatically flushed after each line. A faster way to write many
lines from an array or a list is using joinToString() function with "\n" as the separator, like this:
Learning Kotlin
Kotlin is easy to learn, especially for those who already know Java. A short introduction to the basic syntax of Kotlin for software developers can be found directly in
the reference section of the website starting from basic syntax.
IDEA has built-in Java-to-Kotlin converter. It can be used by people familiar with Java to learn the corresponding Kotlin syntactic constructions, but it is not perfect,
and it is still worth familiarizing yourself with Kotlin and learning the Kotlin idioms.
A great resource to study Kotlin syntax and API of the Kotlin standard library are Kotlin Koans.
The Kotlin 2.2.0 release is here! Here are the main highlights:
Language: new language features in preview, including context parameters. Several previously experimental features are now Stable, such as guard conditions,
non-local break and continue, and multi-dollar interpolation.
Kotlin/Native: LLVM 19 and new features for tracking and adjusting memory consumption.
Kotlin/Wasm: separated Wasm target and the ability to configure Binaryen per project.
Kotlin/JS: fix for the copy() method generated for @JsPlainObject interfaces.
Documentation: our documentation survey is open, and notable improvements have been made to the Kotlin documentation.
IDE support
The Kotlin plugins that support 2.2.0 are bundled in the latest versions of IntelliJ IDEA and Android Studio. You don't need to update the Kotlin plugin in your IDE. All
you need to do is to change the Kotlin version to 2.2.0 in your build scripts.
Language
This release promotes guard conditions, non-local break and continue, and multi-dollar interpolation to Stable. Additionally, several features, such as context
parameters and context-sensitive resolution, are introduced in preview.
172
With context parameters, you don't need to manually pass around values, such as services or dependencies, that are shared and rarely change across sets of
function calls.
Context parameters replace an older experimental feature called context receivers. To migrate from context receivers to context parameters, you can use assisted
support in IntelliJ IDEA, as described in the blog post.
The main difference is that context parameters are not introduced as receivers in the body of a function. As a result, you need to use the name of the context
parameters to access their members, unlike with context receivers, where the context is implicitly available.
Context parameters in Kotlin represent a significant improvement in managing dependencies through simplified dependency injection, improved DSL design, and
scoped operations. For more information, see the feature's KEEP.
You can use _ as a context parameter name. In this case, the parameter's value is available for resolution but is not accessible by name inside the block:
-Xcontext-parameters
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xcontext-parameters")
}
}
Specifying both -Xcontext-receivers and -Xcontext-parameters compiler options simultaneously leads to an error.
173
Preview of context-sensitive resolution
Kotlin 2.2.0 introduces an implementation of context-sensitive resolution in preview.
Previously, you had to write the full name of enum entries or sealed class members, even when the type could be inferred from the context. For example:
Now, with context-sensitive resolution, you can omit the type name in contexts where the expected type is known:
The compiler uses this contextual type information to resolve the correct member. This information includes, among other things:
Context-sensitive resolution doesn't apply to functions, properties with parameters, or extension properties with receivers.
To try out context-sensitive resolution in your project, use the following compiler option in the command line:
-Xcontext-sensitive-resolution
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xcontext-sensitive-resolution")
}
}
We plan to stabilize and improve this feature in future Kotlin releases and would appreciate your feedback on our issue tracker YouTrack.
174
@all meta-target for properties
Kotlin allows you to attach annotations to specific parts of a declaration, known as use-site targets. However, annotating each target individually was complex and
error-prone:
To simplify this, Kotlin introduces the new @all meta-target for properties. This feature tells the compiler to apply the annotation to all relevant parts of the property.
When you use it, @all attempts to apply the annotation to:
set_param: the parameter of the setter method, if the property is defined as var.
RECORD_COMPONENT: if the class is a @JvmRecord, the annotation applies to the Java record component. This behavior mimics the way Java handles
annotations on record components.
The compiler only applies the annotation to the targets for the given property.
In the example below, the @Email annotation is applied to all relevant targets of each property:
You can use the @all meta-target with any property, both inside and outside the primary constructor. However, you can't use the @all meta-target with multiple
annotations.
This new feature simplifies the syntax, ensures consistency, and improves interoperability with Java records.
To enable the @all meta-target in your project, use the following compiler option in the command line:
-Xannotation-target-all
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xannotation-target-all")
}
}
This feature is in preview. Please report any problems to our issue tracker, YouTrack. For more information about the @all meta-target, read this KEEP proposal.
175
New defaulting rules for use-site annotation targets
Kotlin 2.2.0 introduces new defaulting rules for propagating annotations to parameters, fields, and properties. Where previously an annotation was applied by
default only to one of param, property, or field, defaults are now more in line with what is expected of an annotation.
If the field target (field) is applicable while property isn't, field is used.
If there are multiple targets, and none of param, property, or field are applicable, the annotation results in an error.
To enable this feature, add it to the compilerOptions {} block of your Gradle build file:
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xannotation-default-target=param-property")
}
}
-Xannotation-default-target=param-property
In a specific case, define the necessary target explicitly, for example, using @param:Annotation instead of @Annotation.
For a whole project, use this flag in your Gradle build file:
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xannotation-default-target=first-only")
}
}
This feature is in preview. Please report any problems to our issue tracker, YouTrack. For more information about the new defaulting rules for annotation use-site
targets, read this KEEP proposal.
Starting from 2.2.0, you can define type aliases inside other declarations, as long as they don't capture type parameters from their outer class:
class Dijkstra {
typealias VisitedNodes = Set<Node>
Nested type aliases have a few additional constraints, like not being able to mention type parameters. Check the documentation for the entire set of rules.
Nested type aliases allow for cleaner, more maintainable code by improving encapsulation, reducing package-level clutter, and simplifying internal implementations.
-Xnested-type-aliases
176
Or add it to the compilerOptions {} block of your Gradle build file:
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xnested-type-aliases")
}
}
Stable features: guard conditions, non-local break and continue, and multi-dollar interpolation
In Kotlin 2.1.0, several new language features were introduced in preview. We're happy to announce that the following language features are now Stable in this
release:
See the full list of Kotlin language design features and proposals.
Previously, you could only apply general module-wide rules, like disabling all warnings with -nowarn, turning all warnings to compilation errors with -Werror, or
enabling additional compiler checks with -Wextra. The only option to adjust them for specific warnings was the -Xsuppress-warning option.
With the new solution, you can override general rules and exclude specific diagnostics in a consistent way.
How to apply
The new compiler option has the following syntax:
-Xwarning-level=DIAGNOSTIC_NAME:(error|warning|disabled)
Keep in mind that you can only configure the severity level of warnings with the new compiler option.
Use cases
With the new solution, you can better fine-tune warning reporting in your project by combining general rules with specific ones. Choose your use case:
Suppress warnings
Command Description
177
Command Description
-nowarn -Xwarning-level=DIAGNOSTIC_NAME:warning Suppresses all warnings except for the specified ones.
Command Description
-Werror -Xwarning-level=DIAGNOSTIC_NAME:warning Raises all warnings to errors except for the specified ones.
Command Description
-Wextra Enables all additional declaration, expression, and type compiler checks that emit warnings if true.
-Wextra -Xwarning-level=DIAGNOSTIC_NAME:disabled Enables all additional checks except for the specified ones.
Warning lists
In case you have many warnings you want to exclude from general rules, you can list them in a separate file through @argfile.
Leave feedback
The new compiler option is still Experimental. Please report any problems to our issue tracker, YouTrack.
Kotlin/JVM
Kotlin 2.2.0 brings many updates to the JVM. The compiler now supports Java 24 bytecode and introduces changes to default method generation for interface
functions. The release also simplifies working with annotations in Kotlin metadata, improves Java interop with inline value classes, and includes better support for
annotating JVM records.
178
This behavior is controlled by the new stable compiler option -jvm-default, replacing the deprecated -Xjvm-default option.
You can control the behavior of the -jvm-default option using the following values:
enable (default): generates default implementations in interfaces and includes bridge functions in subclasses and DefaultImpls classes. Use this mode to
maintain binary compatibility with older Kotlin versions.
no-compatibility: generates only default implementations in interfaces. This mode skips compatibility bridges and DefaultImpls classes, making it suitable for
new code.
disable: disables default implementations in interfaces. Only bridge functions and DefaultImpls classes are generated, matching the behavior before Kotlin 2.2.0.
To configure the -jvm-default compiler option, set the jvmDefault property in your Gradle Kotlin DSL:
// build.gradle.kts
kotlin {
compilerOptions {
jvmDefault = JvmDefaultMode.NO_COMPATIBILITY
}
}
Now, in Kotlin 2.2.0, the Kotlin Metadata JVM library introduces support for reading annotations stored in Kotlin metadata.
To make annotations available in the metadata for your compiled files, add the following compiler option:
-Xannotations-in-metadata
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xannotations-in-metadata")
}
}
With this option enabled, the Kotlin compiler writes annotations into metadata alongside the JVM bytecode, making them accessible to the kotlin-metadata-jvm
library.
KmClass.annotations
KmFunction.annotations
KmProperty.annotations
KmConstructor.annotations
KmPropertyAccessorAttributes.annotations
KmValueParameter.annotations
KmFunction.extensionReceiverAnnotations
KmProperty.extensionReceiverAnnotations
KmProperty.backingFieldAnnotations
KmProperty.delegateFieldAnnotations
KmEnumEntry.annotations
These APIs are Experimental. To opt in, use the @OptIn(ExperimentalAnnotationsInMetadata::class) annotation.
179
Here's an example of reading annotations from Kotlin metadata:
@file:OptIn(ExperimentalAnnotationsInMetadata::class)
import kotlin.metadata.ExperimentalAnnotationsInMetadata
import kotlin.metadata.jvm.KotlinClassMetadata
@Label("Message class")
class Message
fun main() {
val metadata = Message::class.java.getAnnotation(Metadata::class.java)
val kmClass = (KotlinClassMetadata.readStrict(metadata) as KotlinClassMetadata.Class).kmClass
println(kmClass.annotations)
// [@Label(value = StringValue("Message class"))]
}
If you use the kotlin-metadata-jvm library in your projects, we recommend testing and updating your code to support annotations. Otherwise, when
annotations in metadata become enabled by default in a future Kotlin version, your projects may produce invalid or incomplete metadata.
If you experience any problems, please report them in our issue tracker.
By default, Kotlin compiles inline value classes to use unboxed representations, which are more performant but often hard or even impossible to use from Java. For
example:
In this case, because the class is unboxed, there is no constructor available for Java to call. There's also no way for Java to trigger the init block to ensure that
number is positive.
When you annotate the class with @JvmExposeBoxed, Kotlin generates a public constructor that Java can call directly, ensuring that the init block also runs.
You can apply the @JvmExposeBoxed annotation at the class, constructor, or function level to gain fine-grained control over what's exposed to Java.
For example, in the following code, the extension function .timesTwoBoxed() is not accessible from Java:
@JvmInline
value class MyInt(val value: Int)
To make it possible to create an instance of the MyInt class and call the .timesTwoBoxed() function from Java code, add the @JvmExposeBoxed annotation to both
the class and the function:
@JvmExposeBoxed
@JvmInline
value class MyInt(val value: Int)
@JvmExposeBoxed
fun MyInt.timesTwoBoxed(): MyInt = MyInt(this.value * 2)
With these annotations, the Kotlin compiler generates a Java-accessible constructor for the MyInt class. It also generates an overload for the extension function
that uses the boxed form of the value class. As a result, the following Java code runs successfully:
If you don't want to annotate every part of the inline value classes that you want to expose, you can effectively apply the annotation to a whole module. To apply
180
this behavior to a module, compile it with the -Xjvm-expose-boxed option. Compiling with this option has the same effect as if every declaration in the module had
the @JvmExposeBoxed annotation.
This new annotation does not change how Kotlin compiles or uses value classes internally, and all existing compiled code remains valid. It simply adds new
capabilities to improve Java interoperability. The performance of Kotlin code using value classes is not impacted.
The @JvmExposeBoxed annotation is useful for library authors who want to expose boxed variants of member functions and receive boxed return types. It
eliminates the need to choose between an inline value class (efficient but Kotlin-only) and a data class (Java-compatible but always boxed).
For a more detailed explanation of how the @JvmExposedBoxed annotation works and the problems it solves, see this KEEP proposal.
Firstly, if you want to use a RECORD_COMPONENT as an annotation target, you need to manually add annotations for Kotlin (@Target) and Java. This is because
Kotlin's @Target annotation doesn't support RECORD_COMPONENT. For example:
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
@java.lang.annotation.Target(ElementType.CLASS, ElementType.RECORD_COMPONENT)
annotation class exampleClass
Maintaining both lists manually can be error-prone, so Kotlin 2.2.0 introduces a compiler warning if the Kotlin and Java targets don't match. For instance, if you omit
ElementType.CLASS in the Java target list, the compiler reports:
Incompatible annotation targets: Java target 'CLASS' missing, corresponding to Kotlin targets 'CLASS'.
Secondly, Kotlin's behavior differs from Java when it comes to propagating annotations in records. In Java, annotations on a record component automatically apply
to the backing field, getter, and constructor parameter. Kotlin doesn't do this by default, but you can now replicate the behavior using the @all: use-site target.
For example:
@JvmRecord
data class Person(val name: String, @all:Positive val age: Int)
Propagates the annotation to the property, backing field, constructor parameter, and getter.
Applies the annotation to the record component, as well, if the annotation supports Java's RECORD_COMPONENT.
Kotlin/Native
Starting with 2.2.0, Kotlin/Native uses LLVM 19. This release also brings several experimental features designed to track and adjust memory consumption.
The new feature is designed to replace the -Xallocator=std compiler option, which enabled the system memory allocator instead of the default one. Now, you can
disable buffering (paging of allocations) without switching memory allocators.
The feature is currently Experimental. To enable it, set the following option in your gradle.properties file:
kotlin.native.binary.pagedAllocator=false
181
Kotlin now supports Latin-1-encoded strings, similarly to the JVM. This should help reduce the application's binary size and adjust memory consumption.
By default, strings in Kotlin are stored using UTF-16 encoding, where each character is represented by two bytes. In some cases, this leads to strings taking up
twice as much space in the binary compared to the source code, and reading data from a simple ASCII file can take up twice as much memory as storing the file on
disk.
In turn, Latin-1 (ISO 8859-1) encoding represents each of the first 256 Unicode characters with just one byte. With Latin-1 support enabled, strings are stored in
Latin-1 encoding as long as all the characters fall within its range. Otherwise, the default UTF-16 encoding is used.
kotlin.native.binary.latin1Strings=true
Known issues
As long as the feature is Experimental, the cinterop extension functions String.pin, String.usePinned, and String.refTo become less efficient. Each call to them may
trigger automatic string conversion to UTF-16.
The Kotlin team is very grateful to our colleagues at Google and Sonya Valchuk in particular for implementing this feature.
When inspecting your application's high memory usage, you can now identify how much memory is reserved by Kotlin code. Kotlin's share is tagged with an
identifier and can be tracked through tools like VM Tracker in Xcode Instruments.
This feature is enabled by default but is available only in the Kotlin/Native default memory allocator when all the following conditions are met:
Tagging enabled. The memory should be tagged with a valid identifier. Apple recommends numbers between 240 and 255; the default value is 246.
Allocation with mmap. The allocator should use the mmap system call to map files into memory.
If you set up the kotlin.native.binary.disableMmap=true Gradle property, the default allocator uses malloc instead of mmap.
If you set up the kotlin.native.binary.pagedAllocator=false Gradle property, the memory is reserved on a per-object basis instead.
This update shouldn't affect your code, but if you encounter any issues, please report them to our issue tracker.
Kotlin/Wasm
In this release, the build infrastructure for the Wasm target is separated from the JavaScript target. Additionally, now you can configure the Binaryen tool per project
182
or module.
Now, the wasmJs target has its own infrastructure separate from the js target. This allows Wasm tasks and types to be distinct from JavaScript ones, enabling
independent configuration.
Additionally, Wasm-related project files and NPM dependencies are now stored in a separate build/wasm directory.
New NPM-related tasks have been introduced for Wasm, while existing JavaScript tasks are now dedicated only to JavaScript:
kotlinWasmNpmInstall kotlinNpmInstall
wasmRootPackageJson rootPackageJson
WasmNodeJsRootPlugin NodeJsRootPlugin
WasmNodeJsPlugin NodeJsPlugin
WasmYarnPlugin YarnPlugin
WasmNodeJsRootExtension NodeJsRootExtension
WasmNodeJsEnvSpec NodeJsEnvSpec
WasmYarnRootEnvSpec YarnRootEnvSpec
You can now work with the Wasm target independently of the JavaScript target, which simplifies the configuration process.
Now, you can configure the Binaryen tool per project or module. This change aligns with Gradle's best practices and ensures better support for features like project
isolation, improving build performance and reliability in complex builds.
Additionally, you can now configure different versions of Binaryen for different modules, if needed.
This feature is enabled by default. However, if you have a custom configuration of Binaryen, you now need to apply it per project, rather than only in the root project.
183
Kotlin/JS
This release improves the copy() function in @JsPlainObject interfaces, type aliases in files with the @JsModule annotation, and other Kotlin/JS features.
However, the initial implementation of copy() was not compatible with inheritance, and this caused issues when a @JsPlainObject interface extended other
interfaces.
To avoid limitations on plain objects, the copy() function has been moved from the object itself to its companion object:
@JsPlainObject
external interface User {
val name: String
val age: Int
}
fun main() {
val user = User(name = "SomeUser", age = 21)
// This syntax is not valid anymore
val copy = user.copy(age = 35)
// This is the correct syntax
val copy = User.copy(user, age = 35)
}
This change resolves conflicts in the inheritance hierarchy and eliminates ambiguity. It is enabled by default starting from Kotlin 2.2.0.
Starting with Kotlin 2.2.0, you can declare type aliases inside files marked with @JsModule:
@file:JsModule("somepackage")
package somepackage
typealias SomeClass = Any
This change reduces an aspect of Kotlin/JS interoperability limitations, and more improvements are planned for future releases.
Starting with this release, you can apply @JsExport directly to expect declarations:
// commonMain
@JsExport
fun acceptWindowManager(manager: WindowManager) {
...
}
// jsMain
@JsExport
actual class WindowManager {
184
fun close() {
window.close()
}
}
You must also annotate with @JsExport the corresponding actual implementation in the JavaScript source set, and it has to use only exportable types.
This fix allows shared code defined in commonMain to be correctly exported to JavaScript. You can now expose your multiplatform code to JavaScript consumers
without having to use manual workarounds.
While return types like Promise<Int> worked correctly, using Promise<Unit> triggered a "non-exportable type" warning, even though it correctly mapped to
Promise<void> in TypeScript.
This restriction has been removed. Now, the following code compiles without error:
This change removes an unnecessary restriction in the Kotlin/JS interop model. This fix is enabled by default.
Gradle
Kotlin 2.2.0 is fully compatible with Gradle 7.6.3 through 8.14. You can also use Gradle versions up to the latest Gradle release. However, be aware that doing so
may result in deprecation warnings, and some new Gradle features might not work.
In this release, the Kotlin Gradle plugin comes with several improvements to its diagnostics. It also introduces an experimental integration of binary compatibility
validation, making it easier to work on libraries.
The original binary compatibility validator continues to be maintained during this experimental phase.
Kotlin libraries can use one of two binary formats: JVM class files or klib. Since these formats aren't compatible, the KGP works with each of them separately.
To enable the binary compatibility validation feature set, add the following to the kotlin{} block in your build.gradle.kts file:
// build.gradle.kts
kotlin {
@OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class)
abiValidation {
// Use the set() function to ensure compatibility with older Gradle versions
enabled.set(true)
}
}
If your project has multiple modules where you want to check for binary compatibility, configure the feature in each module separately. Each module can have its
own custom configuration.
185
Once enabled, run the checkLegacyAbi Gradle task to check for binary compatibility issues. You can run the task in IntelliJ IDEA or from the command line in your
project directory:
./gradlew checkLegacyAbi
This task generates an application binary interface (ABI) dump from the current code as a UTF-8 text file. The task then compares the new dump with the one from
the previous release. If the task finds any differences, it reports them as errors. After reviewing the errors, if you decide the changes are acceptable, you can update
the reference ABI dump by running the updateLegacyAbi Gradle task.
Filter classes
The feature lets you filter classes in the ABI dump. You can include or exclude classes explicitly by name or partial name, or by the annotations (or parts of
annotation names) that mark them.
For example, this sample excludes all classes in the com.company package:
// build.gradle.kts
kotlin {
@OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class)
abiValidation {
filters.excluded.byNames.add("com.company.**")
}
}
Explore the KGP API reference to learn more about configuring the binary compatibility validator.
Multiplatform limitations
In multiplatform projects, if your host doesn't support cross-compilation for all targets, the KGP tries to infer the ABI changes for unsupported targets by checking
the ABI dumps from other ones. This approach helps avoid false validation failures if you later switch to a host that can compile all targets.
You can change this default behavior so that the KGP doesn't infer ABI changes for unsupported targets by adding the following to your build.gradle.kts file:
// build.gradle.kts
kotlin {
@OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class)
abiValidation {
klib {
keepUnsupportedTargets = false
}
}
}
However, if you have an unsupported target in your project, running the checkLegacyAbi task fails because the task can't create an ABI dump. This behavior may
be desirable if it's more important for the check to fail than to miss an incompatible change due to inferred ABI changes from other targets.
Rich output is available in supported terminal emulators for Linux and macOS, and we're working on adding support for Windows.
186
Gradle console
This feature is enabled by default, but if you want to override it, add the following Gradle property to your gradle.properties file:
org.gradle.console=plain
For more information about this property and its options, see Gradle's documentation on Customizing log format.
Starting with 2.2.0, the KGP introduces an additional reporting mechanism: it now uses Gradle's Problems API, a standardized way to report rich, structured
problem information during the build process.
The KGP diagnostics are now easier to read and more consistently displayed across different interfaces, such as the Gradle CLI and IntelliJ IDEA.
This integration is enabled by default, starting with Gradle 8.6 or later. As the API is still evolving, use the most recent Gradle version to benefit from the latest
improvements.
Now, the KGP diagnostics are compatible with the --warning-mode option, providing more flexibility. For example, you can convert all warnings into errors or
disable warnings entirely.
With this change, the KGP diagnostics adjust the output based on the selected warning mode:
When you set --warning-mode=fail, diagnostics with Severity.Warning are now elevated to Severity.Error.
When you set --warning-mode=none, diagnostics with Severity.Warning are not logged.
To ignore the --warning-mode option, set the following Gradle property to your gradle.properties file:
kotlin.internal.diagnostics.ignoreWarningMode=true
187
You can use Kotlin with various build systems, such as Gradle, Maven, Amper, and others. However, integrating Kotlin into each system to support the full feature
set, such as incremental compilation and compatibility with Kotlin compiler plugins, daemons, and Kotlin Multiplatform, requires significant effort.
To simplify this process, Kotlin 2.2.0 introduces a new experimental build tools API (BTA). The BTA is a universal API that acts as an abstraction layer between build
systems and the Kotlin compiler ecosystem. With this approach, each build system only needs to support a single BTA entry point.
Currently, the BTA supports Kotlin/JVM only. The Kotlin team at JetBrains already uses it in the Kotlin Gradle plugin (KGP) and the kotlin-maven-plugin. You can try
the BTA through these plugins, but the API itself isn't ready yet for general use in your own build tool integrations. If you're curious about the BTA proposal or want
to share your feedback, see this KEEP proposal.
kotlin.compiler.runViaBuildToolsApi=true
The BTA currently has no direct benefits for the Maven plugin, but it lays a solid foundation for the faster delivery of new features, such as support for the Kotlin
daemon and the stabilization of incremental compilation.
For the KGP, using the BTA already has the following benefits:
Now, using the BTA, the “in-process” strategy does support incremental compilation. To use it, add the following property to your gradle.properties file:
kotlin.compiler.execution.strategy=in-process
The BTA makes this possible. Here's how you can configure it in your build.gradle.kts file:
// build.gradle.kts
import org.jetbrains.kotlin.buildtools.api.ExperimentalBuildToolsApi
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
plugins {
kotlin("jvm") version "2.2.0"
}
group = "org.jetbrains.example"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
kotlin {
jvmToolchain(8)
@OptIn(ExperimentalBuildToolsApi::class, ExperimentalKotlinGradlePluginApi::class)
compilerVersion.set("2.1.21") // Different version than 2.2.0
}
The BTA supports configuring the KGP and Kotlin compiler versions with the three previous major versions and one subsequent major version. So in KGP 2.2.0,
Kotlin compiler versions 2.1.x, 2.0.x, and 1.9.25 are supported. KGP 2.2.0 is also compatible with future Kotlin compiler versions 2.2.x and 2.3.x.
However, keep in mind that using different compiler versions together with compiler plugins may lead to Kotlin compiler exceptions. The Kotlin team plans to
188
address these kinds of problems in future releases.
Try out the BTA with these plugins and send us your feedback in the dedicated YouTrack tickets for the KGP and the Maven plugin.
The Base64.Default is the companion object of the Base64 class. As a result, you can call its functions with Base64.encode() and Base64.decode()
instead of Base64.Default.encode() and Base64.Default.decode().
Base64.Mime uses the MIME encoding scheme, inserting a line separator every 76 characters during encoding and skipping illegal characters during decoding.
Base64.Pem encodes data like Base64.Mime but limits the line length to 64 characters.
You can use the Base64 API to encode binary data into a Base64 string and decode it back into bytes.
Here's an example:
Base64.Default.decode("Zm8=") // foBytes
// Alternatively:
// Base64.decode("Zm8=")
Base64.UrlSafe.decode("Zm9vYmFy") // foobarBytes
On the JVM, use the .encodingWith() and .decodingWith() extension functions to encode and decode Base64 with input and output streams:
import kotlin.io.encoding.*
import java.io.ByteArrayOutputStream
fun main() {
val output = ByteArrayOutputStream()
val base64Output = output.encodingWith(Base64.Default)
println(output.toString())
// SGVsbG8gV29ybGQhIQ==
}
For example:
fun main() {
189
println(93.toHexString())
//sampleEnd
}
For more information, see New HexFormat class to format and parse hexadecimals.
Compose compiler
In this release, the Compose compiler introduces support for composable function references and changes defaults for several feature flags.
Composable function references behave slightly differently from composable lambda objects at runtime. In particular, composable lambdas allow for finer control
over skipping by extending the ComposableLambda class. Function references are expected to implement the KCallable interface, so the same optimization cannot
be applied to them.
To disable this feature flag, add the following to your Gradle configuration:
// build.gradle.kts
composeCompiler {
featureFlag = setOf(ComposeFeatureFlag.PausableComposition.disabled())
}
If you encounter any issues, you can validate that this change causes the issue by disabling the feature flag. Please report any issues to the Jetpack Compose issue
tracker.
To disable the OptimizeNonSkippingGroups flag, add the following to your Gradle configuration:
composeCompiler {
featureFlag = setOf(ComposeFeatureFlag.OptimizeNonSkippingGroups.disabled())
}
190
We plan to remove Ant support in 2.3.0. However, Kotlin remains open to contribution. If you're interested in becoming an external maintainer for Ant, leave a
comment with the “jetbrains-team” visibility setting in this YouTrack issue.
Kotlin 2.2.0 raises the deprecation level of the kotlinOptions{} block in Gradle to error. Use the compilerOptions{} block instead. For guidance on updating your
build scripts, see Migrate from kotlinOptions{} to compilerOptions{}.
Kotlin scripting remains an important part of Kotlin's ecosystem, but we're focusing on specific use cases such as custom scripting, as well as gradle.kts and
main.kts scripts, to provide a better experience. To learn more, see our updated blog post. As a result, Kotlin 2.2.0 deprecates support for:
REPL: To continue to use REPL via kotlinc, opt in with the -Xrepl compiler option.
JSR-223: Since this JSR is in the Withdrawn state, the JSR-223 implementation continues to work with language version 1.9 but won't be migrated to use the
K2 compiler in the future.
The KotlinScriptMojo Maven plugin: We didn't see enough traction with this plugin. You will see compiler warnings if you continue to use it.
In Kotlin 2.2.0, the setSource() function in KotlinCompileTool now replaces configured sources instead of adding to them. If you want to add sources without
replacing existing ones, use the source() function.
The type of annotationProcessorOptionProviders in BaseKapt has been changed from MutableList<Any> to MutableList<CommandLineArgumentProvider>. If
your code currently adds a list as a single element, use the addAll() function instead of the add() function.
Following the deprecation of the dead code elimination (DCE) tool used in the legacy Kotlin/JS backend, the remaining DSLs related to DCE are now removed
from the Kotlin Gradle plugin:
The current JS IR compiler supports DCE out of the box, and the @JsExport annotation allows specifying which Kotlin functions and classes to retain during
DCE.
The deprecated kotlin-android-extensions plugin is removed in Kotlin 2.2.0. Use the kotlin-parcelize plugin for the Parcelable implementation generator and the
Android Jetpack's view bindings for synthetic views instead.
Experimental kotlinArtifacts API is deprecated in Kotlin 2.2.0. Use the current DSL available in the Kotlin Gradle plugin to build final native binaries. If it's not
sufficient for migration, leave a comment in this YT issue.
KotlinCompilation.source, deprecated in Kotlin 1.9.0, is now removed from the Kotlin Gradle plugin.
The parameters for experimental commonization modes are deprecated in Kotlin 2.2.0. Clear the commonization cache to delete invalid compilation artifacts.
The deprecated konanVersion property is now removed from the CInteropProcess task. Use CInteropProcess.kotlinNativeVersion instead.
Usage of the deprecated destinationDir property will now lead to an error. Use CInteropProcess.destinationDirectory.set() instead.
Documentation updates
This release brings notable documentation changes, including the migration of Kotlin Multiplatform documentation to the KMP portal.
Additionally, we launched a documentation survey, created new pages and tutorials, and revamped existing ones.
The survey takes around 15 minutes to complete, and your input will help shape the future of Kotlin docs.
191
Kotlin intermediate tour – Take your understanding of Kotlin to the next level. Learn when to use extension functions, interfaces, classes, and more.
Build a Kotlin app that uses Spring AI – Learn how to create a Kotlin app that answers questions using OpenAI and a vector database.
Create a Spring Boot project with Kotlin – Learn how to create a Spring Boot project with Gradle using IntelliJ IDEA's New Project wizard.
Mapping Kotlin and C tutorial series – Learn how different types and constructs are mapped between Kotlin and C.
Create an app using C interop and libcurl – Create a simple HTTP client that can run natively using the libcurl C library.
Create your Kotlin Multiplatform library – Learn how to create and publish a multiplatform library using IntelliJ IDEA.
Build a full-stack application with Ktor and Kotlin Multiplatform – This tutorial now uses IntelliJ IDEA instead of Fleet, along with Material 3 and the latest versions
of Ktor and Kotlin.
Manage local resource environment in your Compose Multiplatform app – Learn how to manage the application's resource environment, like in-app theme and
language.
Dokka migration guide – Learn how to migrate to v2 of the Dokka Gradle plugin.
Kotlin Metadata JVM library – Explore guidance on reading, modifying, and generating metadata for Kotlin classes compiled for the JVM.
CocoaPods integration – Learn how to set up the environment, add Pod dependencies, or use a Kotlin project as a CocoaPod dependency through tutorials and
sample projects.
New pages for Compose Multiplatform to support the iOS stable release:
Localizing strings and other i18n pages like support for RTL languages.
Compose Hot Reload – Learn how to use Compose Hot Reload with your desktop targets and how to add it to an existing project.
Exposed migrations – Learn about the tools Exposed provides for managing database schema changes.
To update to the new Kotlin version, change the Kotlin version to 2.2.0 in your build scripts.
The Kotlin 2.1.0 release is here! Here are the main highlights:
New language features in preview: Guard conditions in when with a subject, non-local break and continue, and multi-dollar string interpolation.
K2 compiler updates: More flexibility around compiler checks and improvements to the kapt implementation.
Kotlin Multiplatform: Introduced basic support for Swift export, stable Gradle DSL for compiler options, and more.
Gradle support: Improved compatibility with newer versions of Gradle and the Android Gradle plugin, along with updates to the Kotlin Gradle plugin API.
192
IDE support
The Kotlin plugins that support 2.1.0 are bundled in the latest IntelliJ IDEA and Android Studio. You don't need to update the Kotlin plugin in your IDE. All you need
to do is change the Kotlin version to 2.1.0 in your build scripts.
Language
After the Kotlin 2.0.0 release with the K2 compiler, the JetBrains team is focusing on improving the language with new features. In this release, we are excited to
announce several new language design improvements.
These features are available in preview, and we encourage you to try them and share your feedback:
All the features have IDE support in the latest 2024.3 version of IntelliJ IDEA with K2 mode enabled.
See the full list of Kotlin language design features and proposals.
Starting from 2.1.0, you can use guard conditions in when expressions or statements with subjects.
Guard conditions allow you to include more than one condition for the branches of a when expression, making complex control flows more explicit and concise as
well as flattening the code structure.
To include a guard condition in a branch, place it after the primary condition, separated by if:
193
}
}
In a single when expression, you can combine branches with and without guard conditions. The code in a branch with a guard condition runs only if both the
primary condition and the guard condition are true. If the primary condition does not match, the guard condition is not evaluated. Additionally, guard conditions
support else if.
To enable guard conditions in your project, use the following compiler option in the command line:
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xwhen-guards")
}
}
Kotlin 2.1.0 adds a preview of another long-awaited feature, the ability to use non-local break and continue. This feature expands the toolset you can use in the
scope of inline functions and reduces boilerplate code in your project.
Previously, you could use only non-local returns. Now, Kotlin also supports break and continue jump expressions non-locally. This means that you can apply them
within lambdas passed as arguments to an inline function that encloses the loop:
To try the feature in your project, use the -Xnon-local-break-continue compiler option in the command line:
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xnon-local-break-continue")
}
}
We're planning to make this feature Stable in future Kotlin releases. If you encounter any issues when using non-local break and continue, please report them to our
issue tracker.
194
The feature is In preview and opt-in is required (see details below).
Kotlin 2.1.0 introduces support for multi-dollar string interpolation, improving how the dollar sign ($) is handled within string literals. This feature is helpful in
contexts that require multiple dollar signs, such as templating engines, JSON schemas, or other data formats.
String interpolation in Kotlin uses a single dollar sign. However, using a literal dollar sign in a string, which is common in financial data and templating systems,
required workarounds such as ${'$'}. With the multi-dollar interpolation feature enabled, you can configure how many dollar signs trigger interpolation, with fewer
dollar signs being treated as string literals.
Here's an example of how to generate an JSON schema multi-line string with placeholders using $:
In this example, the initial $$ means that you need two dollar signs ($$) to trigger interpolation. It prevents $schema, $id, and $dynamicAnchor from being
interpreted as interpolation markers.
This approach is especially helpful when working with systems that use dollar signs for placeholder syntax.
To enable the feature, use the following compiler option in the command line:
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xmulti-dollar-interpolation")
}
}
If your code already uses standard string interpolation with a single dollar sign, no changes are needed. You can use $$ whenever you need literal dollar signs in
your strings.
This feature can be useful when a library API is stable enough to use but might evolve with new abstract functions, making it unstable for inheritance.
To add the opt-in requirement to an API element, use the @SubclassOptInRequired annotation with a reference to the annotation class:
@RequiresOptIn(
level = RequiresOptIn.Level.WARNING,
message = "Interfaces in this library are experimental"
)
annotation class UnstableApi()
@SubclassOptInRequired(UnstableApi::class)
interface CoreLibraryApi
In this example, the CoreLibraryApi interface requires users to opt in before they can implement it. A user can opt in like this:
@OptIn(UnstableApi::class)
195
interface MyImplementation: CoreLibraryApi
When you use the @SubclassOptInRequired annotation to require opt-in, the requirement is not propagated to any inner or nested classes.
For a real-world example of how to use the @SubclassOptInRequired annotation in your API, check out the SharedFlow interface in the kotlinx.coroutines library.
This led to different behavior depending on whether your overloads were member functions or extension functions. For example:
// Extension functions
kvs.storeExtension("", 1) // Resolves to 1
kvs.storeExtension("") { 1 } // Doesn't resolve
}
In this example, the KeyValueStore class has two overloads for the store() function, where one overload has function parameters with generic types K and V, and
another has a lambda function that returns a generic type V. Similarly, there are two overloads for the extension function: storeExtension().
When the store() function was called with and without a lambda function, the compiler successfully resolved the correct overloads. However, when the extension
function storeExtension() was called with a lambda function, the compiler didn't resolve the correct overload because it incorrectly considered both overloads to be
applicable.
To fix this problem, we've introduced a new heuristic so that the compiler can discard a possible overload when a function parameter with a generic type can't
accept a lambda function based on information from a different argument. This change makes the behavior of member functions and extension functions
consistent, and it is enabled by default in Kotlin 2.1.0.
Kotlin K2 compiler
With Kotlin 2.1.0, the K2 compiler now provides more flexibility when working with compiler checks and warnings, as well as improved support for the kapt plugin.
196
Extra compiler checks
With Kotlin 2.1.0, you can now enable additional checks in the K2 compiler. These are extra declaration, expression, and type checks that are usually not crucial for
compilation but can still be useful if you want to validate the following cases:
CAN_BE_VAL var local = 0 is defined but never reassigned, can be val local = 42 instead
ASSIGNED_VALUE_IS_NEVER_READ val local = 42 is defined but never used afterward in the code
If the check is true, you'll receive a compiler warning with a suggestion on how to fix the problem.
Extra checks are disabled by default. To enable them, use the -Wextra compiler option in the command line or specify extraWarnings in the compilerOptions {}
block of your Gradle build file:
// build.gradle.kts
kotlin {
compilerOptions {
extraWarnings.set(true)
}
197
}
For more information on how to define and use compiler options, see Compiler options in the Kotlin Gradle plugin.
You can now suppress specific warnings in the whole project by using the -Xsuppress-warning=WARNING_NAME syntax in the command line or the
freeCompilerArgs attribute in the compilerOptions {} block of your build file.
For example, if you have extra compiler checks enabled in your project but want to suppress one of them, use:
// build.gradle.kts
kotlin {
compilerOptions {
extraWarnings.set(true)
freeCompilerArgs.add("-Xsuppress-warning=CAN_BE_VAL")
}
}
If you want to suppress a warning but don't know its name, select the element and click the light bulb icon (or use Cmd + Enter/Alt + Enter):
The new compiler option is currently Experimental. The following details are also worth noting:
Command line
198
Build
file
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.addAll(
listOf(
"-Xsuppress-warning=NOTHING_TO_INLINE",
"-Xsuppress-warning=NO_TAIL_CALLS_FOUND"
)
)
}
}
The kapt plugin for the K2 compiler (K2 kapt) is in Alpha. It may be changed at any time.
Currently, projects using the kapt plugin work with the K1 compiler by default, supporting Kotlin versions up to 1.9.
In Kotlin 1.9.20, we launched an experimental implementation of the kapt plugin with the K2 compiler (K2 kapt). We have now improved K2 kapt's internal
implementation to mitigate technical and performance issues.
While the new K2 kapt implementation doesn't introduce new features, its performance has significantly improved compared to the previous K2 kapt
implementation. Additionally, the K2 kapt plugin's behavior is now much closer to that of K1 kapt.
To use the new K2 kapt plugin implementation, enable it just like you did the previous K2 kapt plugin. Add the following option to the gradle.properties file of your
project:
kapt.use.k2=true
In upcoming releases, the K2 kapt implementation will be enabled by default instead of K1 kapt, so you will no longer need to enable it manually.
fun main() {
val uByte: UByte = UByte.MIN_VALUE
uByte.doStuff() // Overload resolution ambiguity before Kotlin 2.1.0
}
In earlier versions, calling uByte.doStuff() led to ambiguity because both the Any and UByte extensions were applicable.
fun main() {
val uByte: UByte = UByte.MIN_VALUE
199
doStuff(uByte) // Overload resolution ambiguity before Kotlin 2.1.0
}
Similarly, the call to doStuff(uByte) was ambiguous because the compiler couldn't decide whether to use the Any or UByte version. With 2.1.0, the compiler now
handles these cases correctly, resolving the ambiguity by giving precedence to the more specific type, in this case UByte.
Kotlin/JVM
Starting with version 2.1.0, the compiler can generate classes containing Java 23 bytecode.
org.jspecify.annotations.Nullable
org.jspecify.annotations.NonNull
org.jspecify.annotations.NullMarked
Starting from Kotlin 2.1.0, nullability mismatches are raised from warnings to errors by default. This ensures that annotations like @NonNull and @Nullable are
enforced during type checks, preventing unexpected nullability issues at runtime.
The @NullMarked annotation also affects the nullability of all members within its scope, making the behavior more predictable when you're working with annotated
Java code.
// Java
import *;
public class SomeJavaClass {
@NonNull
public String foo() { //...
}
@Nullable
public String bar() { //...
}
}
// Kotlin
fun test(sjc: SomeJavaClass) {
// Accesses a non-null result, which is allowed
sjc.foo().length
// Raises an error in the default strict mode because the result is nullable
// To avoid the error, use ?.length instead
sjc.bar().length
}
You can manually control the severity of diagnostics for these annotations. To do so, use the -Xnullability-annotations compiler option to choose a mode:
Kotlin Multiplatform
Kotlin 2.1.0 introduces basic support for Swift export and makes publishing Kotlin Multiplatform libraries easier. It also focuses on improvements around Gradle that
200
stabilize the new DSL for configuring compiler options and bring a preview of the Isolated Projects feature.
New Gradle DSL for compiler options in multiplatform projects promoted to Stable
In Kotlin 2.0.0, we introduced a new Experimental Gradle DSL to simplify the configuration of compiler options across your multiplatform projects. In Kotlin 2.1.0,
this DSL has been promoted to Stable.
The overall project configuration now has three layers. The highest is the extension level, then the target level, and the lowest is the compilation unit (which is
usually a compilation task):
To learn more about the different levels and how compiler options can be configured between them, see Compiler options.
This feature is Experimental and is currently in a pre-Alpha state in Gradle. Use it only with Gradle version 8.10 and solely for evaluation purposes. The
feature may be dropped or changed at any time.
We would appreciate your feedback on it in YouTrack. Opt-in is required (see details below).
In Kotlin 2.1.0, you can preview Gradle's Isolated Projects feature in your multiplatform projects.
The Isolated Projects feature in Gradle improves build performance by "isolating" configuration of individual Gradle projects from each other. Each project's build
logic is restricted from directly accessing the mutable state of other projects, allowing them to safely run in parallel. To support this feature, we made some changes
to the Kotlin Gradle plugin's model, and we are interested in hearing about your experiences during this preview phase.
There are two ways to enable the Kotlin Gradle plugin's new model:
Option 1: Testing compatibility without enabling Isolated Projects – To check compatibility with the Kotlin Gradle plugin's new model without enabling the
Isolated Projects feature, add the following Gradle property in the gradle.properties file of your project:
# gradle.properties
kotlin.kmp.isolated-projects.support=enable
Option 2: Testing with Isolated Projects enabled – Enabling the Isolated Projects feature in Gradle automatically configures the Kotlin Gradle plugin to use the
new model. To enable the Isolated Projects feature, set the system property. In this case, you don't need to add the Gradle property for the Kotlin Gradle plugin
to your project.
201
Basic support for Swift export
This feature is currently in the early stages of development. It may be dropped or changed at any time. Opt-in is required (see the details below), and you
should use it only for evaluation purposes. We would appreciate your feedback on it in YouTrack.
Version 2.1.0 takes the first step towards providing support for Swift export in Kotlin, allowing you to export Kotlin sources directly to the Swift interface without
using Objective-C headers. This should make multiplatform development for Apple targets easier.
Set collapse rules for the package structure with the flattenPackage property.
You can use the following build file in your project as a starting point for setting up Swift export:
// build.gradle.kts
kotlin {
iosX64()
iosArm64()
iosSimulatorArm64()
@OptIn(ExperimentalSwiftExportDsl::class)
swiftExport {
// Root module name
moduleName = "Shared"
// Collapse rule
// Removes package prefix from generated Swift code
flattenPackage = "com.example.sandbox"
You can also clone our public sample with Swift export already set up.
The compiler automatically generates all the necessary files (including swiftmodule files, static a library, and header and modulemap files) and copies them into the
app's build directory, which you can access from Xcode.
Swift export currently works in projects that use direct integration to connect the iOS framework to the Xcode project. This is a standard configuration for Kotlin
Multiplatform projects created in Android Studio or through the web wizard.
# gradle.properties
kotlin.experimental.swift-export.enabled=true
3. On the Build Phases tab, locate the Run Script phase with the embedAndSignAppleFrameworkForXcode task.
4. Adjust the script to feature the embedSwiftExportForXcode task instead in the run script phase:
202
./gradlew :<Shared module name>:embedSwiftExportForXcode
This feature is currently Experimental. Opt-in is required (see the details below), and you should use it only for evaluation purposes. We would appreciate
your feedback on it in YouTrack.
The Kotlin compiler produces .klib artifacts for publishing Kotlin libraries. Previously, you could get the necessary artifacts from any host, except for Apple platform
targets that required a Mac machine. That put a special restraint on Kotlin Multiplatform projects that targeted iOS, macOS, tvOS, and watchOS targets.
Kotlin 2.1.0 lifts this restriction, adding support for cross-compilation. Now you can use any host to produce .klib artifacts, which should greatly simplify the
publishing process for Kotlin and Kotlin Multiplatform libraries.
# gradle.properties
kotlin.native.enableKlibsCrossCompilation=true
This feature is currently Experimental and has some limitations. You still need to use a Mac machine if:
203
Leave feedback on publishing libraries from any host
We're planning to stabilize this feature and further improve library publication in future Kotlin releases. Please leave your feedback in our issue tracker YouTrack.
This change can also improve performance, decreasing compilation and linking time in your Kotlin/Wasm, Kotlin/JS, and Kotlin/Native projects.
For example, our benchmark shows a performance improvement of roughly 3% in total build time on the project with 1 linking and 10 compilation tasks (the project
builds a single native executable binary that depends on 9 simplified projects). However, the actual impact on build time depends on both the number of
subprojects and their respective sizes.
If you have set up custom build logic for resolving klibs and want to use the new unpacked artifacts, you need to explicitly specify the preferred variant of klib
package resolution in your Gradle build file:
// build.gradle.kts
import org.jetbrains.kotlin.gradle.plugin.attributes.KlibPackaging
// ...
val resolvableConfiguration = configurations.resolvable("resolvable") {
Non-packed .klib files are generated at the same path in your project's build directory as the packed ones previously were. In turn, packed klibs are now located in
the build/libs directory.
If no attribute is specified, the packed variant is used. You can check the list of available attributes and variants with the following console command:
./gradlew outgoingVariants
Currently, we recommend using the androidTarget option in your Kotlin Multiplatform projects targeting Android. This is a temporary solution that is necessary to
free the android name for the upcoming Android/KMP plugin from Google.
We'll provide further migration instructions when the new plugin is available. The new DSL from Google will be the preferred option for Android target support in
Kotlin Multiplatform.
Kotlin 1.9.20 triggered a deprecation warning if you declared multiple targets of the same type in your multiplatform projects. In Kotlin 2.1.0, this deprecation
warning is now an error for all targets except Kotlin/JS ones. To learn more about why Kotlin/JS targets are exempt, see this issue in YouTrack.
204
Kotlin/Native
Kotlin 2.1.0 includes an upgrade for the iosArm64 target support, improved cinterop caching process, and other updates.
This means the target is regularly tested on the CI pipeline to ensure that it's able to compile and run. We also provide source and binary compatibility between
compiler releases for the target.
If you have Linux targets in your project, take note that the Kotlin/Native compiler now uses the lld linker by default for all Linux targets.
This update shouldn't affect your code, but if you encounter any issues, please report them to our issue tracker.
This should resolve issues where UP-TO-DATE checks failed to detect changes to header files specified in the definition file, preventing the build system from
recompiling the code.
The new memory allocator replaced the previous default allocator, mimalloc. Now, it's time to deprecate mimalloc in the Kotlin/Native compiler.
You can now remove the -Xallocator=mimalloc compiler option from your build scripts. If you encounter any issues, please report them to our issue tracker.
For more information on the memory allocator and garbage collection in Kotlin, see Kotlin/Native memory management.
Kotlin/Wasm
Kotlin/Wasm received multiple updates along with support for incremental compilation.
Starting from 2.1.0, incremental compilation is supported for Wasm targets. In development tasks, the compiler now recompiles only files relevant to changes from
the last compilation, which noticeably reduces the compilation time.
This change currently doubles the compilation speed, and there are plans to improve it further in future releases.
In the current setup, incremental compilation for Wasm targets is disabled by default. To enable incremental compilation, add the following line to your project's
local.properties or gradle.properties file:
# gradle.properties
kotlin.incremental.wasm=true
Try out Kotlin/Wasm incremental compilation and share your feedback. Your insights will help make this feature Stable and enabled by default sooner.
205
Browser APIs moved to the kotlinx-browser stand-alone library
Previously, the declarations for web APIs and related target utilities were part of the Kotlin/Wasm standard library.
In this release, the org.w3c.* declarations have been moved from the Kotlin/Wasm standard library to the new kotlinx-browser library. This library also includes other
web-related packages, such as org.khronos.webgl, kotlin.dom, and kotlinx.browser.
This separation provides modularity, enabling independent updates for web-related APIs outside of Kotlin's release cycle. Additionally, the Kotlin/Wasm standard
library now contains only declarations available in any JavaScript environment.
To use the declarations from the moved packages, you need to add the kotlinx-browser dependency to your project's build configuration file:
// build.gradle.kts
val wasmJsMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-browser:0.3")
}
}
To improve this experience, custom formatters have been added in the variable view. The implementation uses the custom formatters API, which is supported
across major browsers like Firefox and Chromium-based ones.
With this change, you can now display and locate variable values in a more user-friendly and comprehensible manner.
206
Kotlin/Wasm improved debugger
// build.gradle.kts
kotlin {
wasmJs {
// ...
compilerOptions {
freeCompilerArgs.add("-Xwasm-debugger-custom-formatters")
}
}
}
207
Enable custom formatters in Chrome
208
Enable custom formatters in Firefox
This gap required creating custom functions for array transformations, complicating interoperability between Kotlin and JavaScript code.
This release introduces an adapter function that automatically converts JsArray<T> to Array<T> and vice versa, simplifying array operations.
Here's an example of conversion between generic types: Kotlin List<T> and Array<T> to JavaScript JsArray<T>.
Similar methods are available for converting typed arrays to their Kotlin equivalents (for example, IntArray and Int32Array). For detailed information and
implementation, see the kotlinx-browser repository.
Here's an example of conversion between typed arrays: Kotlin IntArray to JavaScript Int32Array.
import org.khronos.webgl.*
// ...
209
val intArray: IntArray = intArrayOf(1, 2, 3)
Starting from Kotlin 2.1.0, you can configure JsException to include the original error message and stack trace by enabling a specific compiler option. This provides
more context to help diagnose issues originating from JavaScript.
This behavior depends on the WebAssembly.JSTag API, which is available only in certain browsers:
To enable this feature, which is disabled by default, add the following compiler option to your build.gradle.kts file:
// build.gradle.kts
kotlin {
wasmJs {
compilerOptions {
freeCompilerArgs.add("-Xwasm-attach-js-exception")
}
}
}
fun main() {
try {
JSON.parse("an invalid JSON")
} catch (e: JsException) {
println("Thrown value is: ${e.thrownValue}")
// SyntaxError: Unexpected token 'a', "an invalid JSON" is not valid JSON
println("Message: ${e.message}")
// Message: Unexpected token 'a', "an invalid JSON" is not valid JSON
println("Stacktrace:")
// Stacktrace:
With the -Xwasm-attach-js-exception option enabled, JsException provides specific details from the JavaScript error. Without the option, JsException includes only
a generic message stating that an exception was thrown while running JavaScript code.
In 2.1.0, default imports have been completely removed to fully support named exports.
When coding in JavaScript for the Kotlin/Wasm target, you now need to use the corresponding named imports instead of default imports.
This change marks the last phase of a deprecation cycle to migrate to named exports:
210
In version 2.0.0: A warning message was printed to the console, explaining that exporting entities via default exports is deprecated.
In version 2.0.20: An error occurred, requesting the use of the corresponding named import.
In version 2.1.0: The use of default imports has been completely removed.
// build.gradle.kts
project.plugins.withType<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin> {
project.the<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsEnvSpec>().version = "22.0.0"
}
To use the new class for the entire project, add the same code in the allprojects {} block:
// build.gradle.kts
allprojects {
project.plugins.withType<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin> {
project.the<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsEnvSpec>().version = "your Node.js version"
}
}
You can also use Gradle convention plugins to apply the settings to a particular set of subprojects.
Kotlin/JS
Similarly, it was not possible to access JavaScript object properties that contained characters not permitted in Kotlin identifiers, such as hyphens or spaces:
This behavior differed from JavaScript and TypeScript, which allow such properties to be accessed using non-identifier characters.
Starting from Kotlin 2.1.0, this feature is enabled by default. Kotlin/JS now allows you to use the backticks (``) and the @JsName annotation to interact with
JavaScript properties containing non-identifier characters and to use names for test methods.
Additionally, you can use the @JsName and @JsQualifier annotations to map Kotlin property names to JavaScript equivalents:
object Bar {
val `property example`: String = "bar"
}
@JsQualifier("fooNamespace")
external object Foo {
val `property example`: String
}
@JsExport
object Baz {
val `property example`: String = "bar"
}
211
fun main() {
// In JavaScript, this is compiled into Bar.property_example_HASH
println(Bar.`property example`)
// In JavaScript, this is compiled into fooNamespace["property example"]
println(Foo.`property example`)
// In JavaScript, this is compiled into Baz["property example"]
println(Baz.`property example`)
}
Using arrow functions can reduce the bundle size of your project, especially when using the experimental -Xir-generate-inline-anonymous-functions mode. This also
makes the generated code more aligned with modern JS.
This feature is enabled by default when targeting ES2015. Alternatively, you can enable it by using the -Xes-arrow-functions command line argument.
Learn more about ES2015 (ECMAScript 2015, ES6) in the official documentation.
Gradle improvements
Kotlin 2.1.0 is fully compatible with Gradle 7.6.3 through 8.6. Gradle versions 8.7 to 8.10 are also supported, with only one exception. If you use the Kotlin
Multiplatform Gradle plugin, you may see deprecation warnings in your multiplatform projects calling the withJava() function in the JVM target. We plan to fix this
issue as soon as possible.
You can also use Gradle versions up to the latest Gradle release, but if you do, keep in mind that you might encounter deprecation warnings or some new Gradle
features might not work.
Name Description
KotlinBaseExtension A plugin DSL extension type for configuring common Kotlin JVM, Android, and Multiplatform plugin options for the entire project:
org.jetbrains.kotlin.jvm
org.jetbrains.kotlin.android
org.jetbrains.kotlin.multiplatform
KotlinJvmExtension A plugin DSL extension type for configuring Kotlin JVM plugin options for the entire project.
KotlinAndroidExtension A plugin DSL extension type for configuring Kotlin Android plugin options for the entire project.
For example, if you want to configure compiler options for both JVM and Android projects, use KotlinBaseExtension:
212
configure<KotlinBaseExtension> {
if (this is HasConfigurableKotlinCompilerOptions<*>) {
with(compilerOptions) {
if (this is KotlinJvmCompilerOptions) {
jvmTarget.set(JvmTarget.JVM_17)
}
}
}
}
This configures the JVM target to 17 for both JVM and Android projects.
configure<KotlinJvmExtension> {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
target.mavenPublication {
groupId = "com.example"
artifactId = "example-project"
version = "1.0-SNAPSHOT"
}
}
This example similarly configures the JVM target to 17 for JVM projects. It also configures a Maven publication for the project so that its output is published to a
Maven repository.
Starting with Kotlin 2.1.0, KGP bundles a subset of org.jetbrains.kotlin:kotlin-compiler-embeddable class files in its JAR file and progressively removes them. This
change aims to prevent compatibility issues and simplify KGP maintenance.
If other parts of your build logic, such as plugins like kotlinter, depend on a different version of org.jetbrains.kotlin:kotlin-compiler-embeddable than the one bundled
with KGP, it can lead to conflicts and runtime exceptions.
To prevent such issues, KGP now shows a warning if org.jetbrains.kotlin:kotlin-compiler-embeddable is present in the build classpath alongside KGP.
As a long-term solution, if you're a plugin author using org.jetbrains.kotlin:kotlin-compiler-embeddable classes, we recommend running them in an isolated
classloader. For example, you can achieve it using the Gradle Workers API with classloader or process isolation.
// build.gradle.kts
dependencies {
compileOnly("org.jetbrains.kotlin:kotlin-compiler-embeddable:2.2.0")
}
Next, define a Gradle work action to print the Kotlin compiler version:
import org.gradle.workers.WorkAction
import org.gradle.workers.WorkParameters
import org.jetbrains.kotlin.config.KotlinCompilerVersion
abstract class ActionUsingKotlinCompiler : WorkAction<WorkParameters.None> {
override fun execute() {
println("Kotlin compiler version: ${KotlinCompilerVersion.getVersion()}")
}
}
213
Now create a task that submits this action to the worker executor using classloader isolation:
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.tasks.Classpath
import org.gradle.api.tasks.TaskAction
import org.gradle.workers.WorkerExecutor
import javax.inject.Inject
abstract class TaskUsingKotlinCompiler: DefaultTask() {
@get:Inject
abstract val executor: WorkerExecutor
@get:Classpath
abstract val kotlinCompiler: ConfigurableFileCollection
@TaskAction
fun compile() {
val workQueue = executor.classLoaderIsolation {
classpath.from(kotlinCompiler)
}
workQueue.submit(ActionUsingKotlinCompiler::class.java) {}
}
}
import org.gradle.api.Plugin
import org.gradle.api.Project
abstract class MyPlugin: Plugin<Project> {
override fun apply(target: Project) {
val myDependencyScope = target.configurations.create("myDependencyScope")
target.dependencies.add(myDependencyScope.name, "$KOTLIN_COMPILER_EMBEDDABLE:$KOTLIN_COMPILER_VERSION")
val myResolvableConfiguration = target.configurations.create("myResolvable") {
extendsFrom(myDependencyScope)
}
target.tasks.register("myTask", TaskUsingKotlinCompiler::class.java) {
kotlinCompiler.from(myResolvableConfiguration)
}
}
companion object {
const val KOTLIN_COMPILER_EMBEDDABLE = "org.jetbrains.kotlin:kotlin-compiler-embeddable"
const val KOTLIN_COMPILER_VERSION = "2.2.0"
}
}
Here's how to pass several files to the Compose compiler using the new option:
// build.gradle.kt
composeCompiler {
stabilityConfigurationFiles.addAll(
project.layout.projectDirectory.file("configuration-file1.conf"),
project.layout.projectDirectory.file("configuration-file2.conf"),
)
}
Pausable composition
214
Pausable composition is a new Experimental feature that changes how the compiler generates skippable functions. With this feature enabled, composition can be
suspended on skipping points during runtime, allowing long-running composition processes to be split across multiple frames. Pausable composition is used in lazy
lists and other performance-intensive components for prefetching content that might cause frames to drop when executed in a blocking manner.
To try out pausable composition, add the following feature flag in the Gradle configuration for the Compose compiler:
// build.gradle.kts
composeCompiler {
featureFlags = setOf(
ComposeFeatureFlag.PausableComposition
)
}
Runtime support for this feature was added in the 1.8.0-alpha02 version of androidx.compose.runtime. The feature flag has no effect when used with
older runtime versions.
This means that virtual functions won't be restarted or skipped: whenever their state is invalidated, runtime will recompose their parent composable instead. If your
code is sensitive to recompositions, you may notice changes in runtime behavior.
Performance improvements
The Compose compiler used to create a full copy of module's IR to transform @Composable types. Apart from increased memory consumption when copying
elements that were not related to Compose, this behavior was also breaking downstream compiler plugins in certain edge cases.
This copy operation was removed, resulting in potentially faster compilation times.
Standard library
Locale-sensitive case conversion functions for Char and String are deprecated: Functions like Char.toLowerCase(), Char.toUpperCase(), String.toUpperCase(),
and String.toLowerCase() are now deprecated, and using them results in an error. Replace them with locale-agnostic function alternatives or other case
conversion mechanisms. If you want to continue using the default locale, replace calls like String.toLowerCase() with String.lowercase(Locale.getDefault()),
explicitly specifying the locale. For a locale-agnostic conversion, replace them with String.lowercase(), which uses the invariant locale by default.
Kotlin/Native freezing API is deprecated: Using the freezing-related declarations previously marked with the @FreezingIsDeprecated annotation now results in an
error. This change reflects the transition from the legacy memory manager in Kotlin/Native, which required freezing objects to share them between threads. To
learn how to migrate from freezing-related APIs in the new memory model, see the Kotlin/Native migration guide. For more information, see the announcement
about the deprecation of freezing.
appendln() is deprecated in favor of appendLine(): The StringBuilder.appendln() and Appendable.appendln() functions are now deprecated, and using them
results in an error. To replace them, use the StringBuilder.appendLine() or Appendable.appendLine() functions instead. The appendln() function is deprecated
because, on Kotlin/JVM, it uses the line.separator system property, which has a different default value on each OS. On Kotlin/JVM, this property defaults to \r\n
(CR LF) on Windows and \n (LF) on other systems. On the other hand, the appendLine() function consistently uses \n (LF) as the line separator, ensuring
consistent behavior across platforms.
For a complete list of affected APIs in this release, see the KT-71628 YouTrack issue.
215
walk() lazily traverses the file tree rooted at the specified path.
fileVisitor() makes it possible to create a FileVisitor separately. FileVisitor specifies the actions to be performed on directories and files during traversal.
visitFileTree(fileVisitor: FileVisitor, ...) traverses through a file tree, invoking the specified FileVisitor on each encountered entry, and it uses the
java.nio.file.Files.walkFileTree() function under the hood.
visitFileTree(..., builderAction: FileVisitorBuilder.() -> Unit) creates a FileVisitor with the provided builderAction and calls the visitFileTree(fileVisitor, ...) function.
enum class PathWalkOption provides traversal options for the Path.walk() function.
The examples below demonstrate how to use these file traversal APIs to create custom FileVisitor behaviors, which allows you to define specific actions for visiting
files and directories.
For instance, you can explicitly create a FileVisitor and use it later:
You can also create a FileVisitor with the builderAction and use it immediately for the traversal:
projectDirectory.visitFileTree {
// Defines the builderAction:
onPreVisitDirectory { directory, attributes ->
// Some logic on visiting directories
FileVisitResult.CONTINUE
}
Additionally, you can traverse a file tree rooted at the specified path with the walk() function:
fun traverseFileTree() {
val cleanVisitor = fileVisitor {
onPreVisitDirectory { directory, _ ->
if (directory.name == "build") {
directory.toFile().deleteRecursively()
FileVisitResult.SKIP_SUBTREE
} else {
FileVisitResult.CONTINUE
}
}
216
srcDirectory.createDirectory()
srcDirectory.resolve("A.kt").createFile()
srcDirectory.resolve("A.class").createFile()
}
// Traverses the file tree with cleanVisitor, applying the rootDirectory.visitFileTree(cleanVisitor) cleanup rules
val directoryStructureAfterClean = rootDirectory.walk(PathWalkOption.INCLUDE_DIRECTORIES)
.map { it.relativeTo(rootDirectory).toString() }
.toList().sorted()
println(directoryStructureAfterClean)
// "[, src, src/A.kt]"
}
Documentation updates
The Kotlin documentation has received some notable changes:
Language concepts
Improved Null safety page – Learn how to handle null values safely in your code.
Improved Objects declarations and expressions page – Learn how to define a class and create an instance in a single step.
Improved When expressions and statements section – Learn about the when conditional and how you can use it.
Updated Kotlin roadmap, Kotlin evolution principles, and Kotlin language features and proposals pages – Learn about Kotlin's plans, ongoing developments, and
guiding principles.
Compose compiler
Compose compiler documentation now located in the Compiler and plugins section – Learn about the Compose compiler, the compiler options, and the steps to
migrate.
API references
New Kotlin Gradle plugins API reference – Explore the API references for the Kotlin Gradle plugin and the Compose compiler Gradle plugin.
Multiplatform development
New Building a Kotlin library for multiplatform page – Learn how to design your Kotlin libraries for Kotlin Multiplatform.
New Introduction to Kotlin Multiplatform page – Learn about Kotlin Multiplatform's key concepts, dependencies, libraries, and more.
Updated Kotlin Multiplatform overview page – Navigate through the essentials of Kotlin Multiplatform and popular use cases.
New iOS integration section – Learn how to integrate a Kotlin Multiplatform shared module into your iOS app.
New Kotlin/Native's definition file page – Learn how to create a definition file to consume C and Objective-C libraries.
Get started with WASI – Learn how to run a simple Kotlin/Wasm application using WASI in various WebAssembly virtual machines.
Tooling
New Dokka migration guide – Learn how to migrate to Dokka Gradle plugin v2.
217
Compatibility guide for Kotlin 2.1.0
Kotlin 2.1.0 is a feature release and can, therefore, bring changes that are incompatible with your code written for earlier versions of the language. Find the detailed
list of these changes in the Compatibility guide for Kotlin 2.1.0.
To update to the new Kotlin version, change the Kotlin version to 2.1.0 in your build scripts.
The Kotlin 2.1.20 release is here! Here are the main highlights:
Kotlin/Wasm: default custom formatters, support for DWARF, and migration to Provider API
Gradle support: compatibility with Gradle's Isolated Projects and custom publication variants
Standard library: common atomic types, improved UUID support, and new time-tracking functionality
IDE support
The Kotlin plugins that support 2.1.20 are bundled in the latest IntelliJ IDEA and Android Studio. You don't need to update the Kotlin plugin in your IDE. All you need
to do is to change the Kotlin version to 2.1.20 in your build scripts.
Kotlin K2 compiler
We're continuing to improve plugin support for the new Kotlin K2 compiler. This release brings updates to the new kapt and Lombok plugins.
The JetBrains team launched the new implementation of the kapt plugin with the K2 compiler back in Kotlin 1.9.20. Since then, we have further developed the
internal implementation of K2 kapt and made its behavior similar to that of the K1 version, while significantly improving its performance as well.
If you encounter any issues when using kapt with the K2 compiler, you can temporarily revert to the previous plugin implementation.
To do this, add the following option to the gradle.properties file of your project:
218
kapt.use.k2=false
The @Builder annotation now works on constructors, allowing more flexible object creation. For more details, see the corresponding YouTrack issue.
Several issues related to Lombok's code generation in Kotlin have been resolved, improving overall compatibility. For more details, see the GitHub changelog.
For more information about the @SuperBuilder annotation, see the official Lombok documentation.
Before the executable {} block in your build script, add the following @OptIn annotation:
@OptIn(ExperimentalKotlinGradlePluginApi::class)
For example:
kotlin {
jvm {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
binaries {
// Configures a JavaExec task named "runJvm" and a Gradle distribution for the "main" compilation in this target
executable {
mainClass.set("foo.MainKt")
}
// Configures a JavaExec task named "runJvmAnother" and a Gradle distribution for the "main" compilation
executable(KotlinCompilation.MAIN_COMPILATION_NAME, "another") {
// Set a different class
mainClass.set("foo.MainAnotherKt")
}
// Configures a JavaExec task named "runJvmTest" and a Gradle distribution for the "test" compilation
executable(KotlinCompilation.TEST_COMPILATION_NAME) {
mainClass.set("foo.MainTestKt")
}
// Configures a JavaExec task named "runJvmTestAnother" and a Gradle distribution for the "test" compilation
executable(KotlinCompilation.TEST_COMPILATION_NAME, "another") {
mainClass.set("foo.MainAnotherTestKt")
}
}
}
}
In this example, Gradle's Distribution plugin is applied on the first executable {} block.
If you run into any issues, report them in our issue tracker or let us know in our public Slack channel.
Kotlin/Native
219
working on your Kotlin projects for Apple operating systems.
The 2.1.21 release also fixes the related cinterop issue that caused compilation failures in Kotlin Multiplatform projects.
The new inlining pass in the Kotlin/Native compiler should perform better than the standard LLVM inliner and improve the runtime performance of the generated
code.
The new inlining pass is currently Experimental. To try it out, use the following compiler option:
-Xbinary=preCodegenInlineThreshold=40
Our experiments show that setting the threshold to 40 tokens (code units parsed by the compiler) provides a reasonable compromise for compilation optimization.
According to our benchmarks, this gives an overall performance improvement of 9.5%. Of course, you can try out other values, too.
If you experience increased binary size or compilation time, please report such issues via YouTrack.
Kotlin/Wasm
This release improves Kotlin/Wasm debugging and property usage. Custom formatters now work out of the box in development builds, while DWARF debugging
facilitates code inspection. Additionally, the Provider API simplifies property usage in Kotlin/Wasm and Kotlin/JS.
In this release, custom formatters are enabled by default in development builds, so you don't need additional Gradle configurations.
To use this feature, you only need to ensure that custom formatters are enabled in your browser's developer tools:
In Chrome DevTools, find the custom formatters checkbox in Settings | Preferences | Console:
220
Enable custom formatters in Chrome
In Firefox DevTools, find the custom formatters checkbox in Settings | Advanced settings:
221
Enable custom formatters in Firefox
This change primarily affects Kotlin/Wasm development builds. If you have specific requirements for production builds, you need to adjust your Gradle configuration
accordingly. To do so, add the following compiler option to the wasmJs {} block:
// build.gradle.kts
kotlin {
wasmJs {
// ...
compilerOptions {
freeCompilerArgs.add("-Xwasm-debugger-custom-formatters")
}
}
}
With this change, the Kotlin/Wasm compiler is able to embed DWARF data into the generated WebAssembly (Wasm) binary. Many debuggers and virtual machines
can read this data to provide insights into the compiled code.
DWARF is mainly useful for debugging Kotlin/Wasm applications inside standalone Wasm virtual machines (VMs). To use this feature, the Wasm VM and debugger
must support DWARF.
With DWARF support, you can step through Kotlin/Wasm applications, inspect variables, and gain code insights. To enable this feature, use the following compiler
option:
-Xwasm-generate-dwarf
222
the<NodeJsExtension>().version = "2.0.0"
Now, properties are exposed through the Provider API, and you must use the .set() function to assign values:
the<NodeJsEnvSpec>().version.set("2.0.0")
The Provider API ensures that values are lazily computed and properly integrated with task dependencies, improving build performance.
With this change, direct property assignments are deprecated in favor of *EnvSpec classes, such as NodeJsEnvSpec and YarnRootEnvSpec.
wasmJsRun wasmJsBrowserDevelopmentRun
wasmJsBrowserRun wasmJsBrowserDevelopmentRun
wasmJsNodeRun wasmJsNodeDevelopmentRun
jsRun jsBrowserDevelopmentRun
jsBrowserRun jsBrowserDevelopmentRun
jsNodeRun jsNodeDevelopmentRun
If you only use Kotlin/JS or Kotlin/Wasm in build scripts, no action is required as Gradle automatically handles assignments.
However, if you maintain a plugin based on the Kotlin Gradle Plugin, and your plugin does not apply kotlin-dsl, you must update property assignments to use the
.set() function.
Gradle
Kotlin 2.1.20 is fully compatible with Gradle 7.6.3 through 8.11. You can also use Gradle versions up to the latest Gradle release. However, be aware that doing so
may result in deprecation warnings, and some new Gradle features might not work.
This version of Kotlin includes compatibility of Kotlin Gradle plugins with Gradle's Isolated Projects as well as support for custom Gradle publication variants.
This feature is currently in a pre-Alpha state in Gradle. JS and Wasm targets are not supported at the moment. Use it only with Gradle version 8.10 or
higher and solely for evaluation purposes.
Since Kotlin 2.1.0, you've been able to preview Gradle's Isolated Projects feature in your projects.
Previously, you had to configure the Kotlin Gradle plugin to make your project compatible with the Isolated Projects feature before you could try it out. In Kotlin
223
2.1.20, this additional step is no longer necessary.
Now, to enable the Isolated Projects feature, you only need to set the system property.
Gradle's Isolated Projects feature is supported in Kotlin Gradle plugins for both multiplatform projects and projects that contain only the JVM or Android target.
Specifically for multiplatform projects, if you notice problems with your Gradle build after upgrading, you can opt out of the new Kotlin Gradle plugin behavior by
adding:
kotlin.kmp.isolated-projects.support=disable
However, if you use this Gradle property in your multiplatform project, you can't use the Isolated Projects feature.
To add a custom Gradle publication variant, invoke the adhocSoftwareComponent() function, which returns an instance of AdhocComponentWithVariants that you
can configure in the Kotlin DSL:
plugins {
// Only JVM and Multiplatform are supported
kotlin("jvm")
// or
kotlin("multiplatform")
}
kotlin {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
publishing {
// Returns an instance of AdhocSoftwareComponent
adhocSoftwareComponent()
// Alternatively, you can configure AdhocSoftwareComponent in the DSL block as follows
adhocSoftwareComponent {
// Add your custom variants here using the AdhocSoftwareComponent API
}
}
}
Standard library
This release brings new Experimental features to the standard library: common atomic types, improved support for UUIDs, and new time-tracking functionality.
The kotlin.concurrent.atomics package and its properties are Experimental. To opt in, use the @OptIn(ExperimentalAtomicApi::class) annotation or the compiler
option -opt-in=kotlin.ExperimentalAtomicApi.
Here's an example that shows how you can use AtomicInt to safely count processed items across multiple threads:
224
// Imports the necessary libraries
import kotlin.concurrent.atomics.*
import kotlinx.coroutines.*
@OptIn(ExperimentalAtomicApi::class)
suspend fun main() {
// Initializes the atomic counter for processed items
var processedItems = AtomicInt(0)
val totalItems = 100
val items = List(totalItems) { "item$it" }
// Splits the items into chunks for processing by multiple coroutines
val chunkSize = 20
val itemChunks = items.chunked(chunkSize)
coroutineScope {
for (chunk in itemChunks) {
launch {
for (item in chunk) {
println("Processing $item in thread ${Thread.currentThread()}")
processedItems += 1 // Increment counter atomically
}
}
}
}
// Prints the total number of processed items
println("Total processed items: ${processedItems.load()}")
}
To enable seamless interoperability between Kotlin's atomic types and Java's java.util.concurrent.atomic atomic types, the API provides the .asJavaAtomic() and
.asKotlinAtomic() extension functions. On the JVM, Kotlin atomics and Java atomics are the same types in runtime, so you can transform Java atomics into Kotlin
atomics and vice versa without any overhead.
Here's an example that shows how Kotlin and Java atomic types can work together:
@OptIn(ExperimentalAtomicApi::class)
fun main() {
// Converts Kotlin AtomicInt to Java's AtomicInteger
val kotlinAtomic = AtomicInt(42)
val javaAtomic: AtomicInteger = kotlinAtomic.asJavaAtomic()
println("Java atomic value: ${javaAtomic.get()}")
// Java atomic value: 42
Previously, the parse() function only accepted UUIDs in the hex-and-dash format. With Kotlin 2.1.20, you can use parse() for both the hex-and-dash and the plain
hexadecimal (without dashes) formats.
We've also introduced functions specific to operations with the hex-and-dash format in this release:
toHexDashString() converts a Uuid into a String in the hex-and-dash format (mirroring the functionality of toString()).
These functions work similarly to parseHex() and toHexString(), which were introduced earlier for the hexadecimal format. Explicit naming for parsing and formatting
functionality should improve code clarity and your overall experience with UUIDs.
UUIDs in Kotlin are now Comparable. Starting with Kotlin 2.1.20, you can directly compare and sort values of the Uuid type. This enables the use of the < and >
operators and standard library extensions available exclusively for Comparable types or their collections (such as sorted()), and it also allows passing UUIDs to any
functions or APIs that require the Comparable interface.
Remember that the UUID support in the standard library is still Experimental. To opt in, use the @OptIn(ExperimentalUuidApi::class) annotation or the compiler
option -opt-in=kotlin.uuid.ExperimentalUuidApi:
225
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@OptIn(ExperimentalUuidApi::class)
fun main() {
// parse() accepts a UUID in a plain hexadecimal format
val uuid = Uuid.parse("550e8400e29b41d4a716446655440000")
The kotlinx.datetime.Clock interface is introduced to the standard library as kotlin.time.Clock and the kotlinx.datetime.Instant class as kotlin.time.Instant. These
concepts naturally align with the time package in the standard library because they're only concerned with moments in time compared to a more complex calendar
and timezone functionality that remains in kotlinx-datetime.
Instant and Clock are useful when you need precise time tracking without considering time zones or dates. For example, you can use them to log events with
timestamps, measure durations between two points in time, and obtain the current moment for system processes.
To provide interoperability with other languages, additional converter functions are available:
Instant.toJSDate() converts the kotlin.time.Instant value to an instance of the JS Date class. This conversion is not precise; JS uses millisecond precision to
represent dates, while Kotlin allows for nanosecond resolution.
The new time features of the standard library are still Experimental. To opt in, use the @OptIn(ExperimentalTime::class) annotation:
import kotlin.time.*
@OptIn(ExperimentalTime::class)
fun main() {
Compose compiler
In 2.1.20, the Compose compiler relaxes some restrictions on @Composable functions introduced in previous releases. In addition, the Compose compiler Gradle
plugin is set to include source information by default, aligning the behavior on all platforms with Android.
226
Support for default arguments in open @Composable functions
The compiler previously restricted default arguments in open @Composable functions due to incorrect compiler output, which would result in crashes at runtime.
The underlying issue is now resolved, and default arguments are fully supported when used with Kotlin 2.1.20 or newer.
Compose compiler allowed default arguments in open functions before version 1.5.8, so the support depends on project configuration:
If an open composable function is compiled with Kotlin version 2.1.20 or newer, the compiler generates correct wrappers for default arguments. This includes
wrappers compatible with pre-1.5.8 binaries, meaning that downstream libraries will also be able to use this open function.
If the open composable function is compiled with Kotlin older than 2.1.20, Compose uses a compatibility mode, which might result in runtime crashes. When
using the compatibility mode, the compiler emits a warning to highlight potential problems.
You might observe some behavior changes in affected functions after upgrading to Kotlin 2.1.20. To force non-restartable logic from the previous version, apply the
@NonRestartableComposable annotation to the function.
However, this optimization was also applied to inline function bodies, which resulted in singleton lambda instances leaking into the public API. To fix this problem,
starting with 2.1.20, @Composable lambdas are no longer optimized into singletons inside inline functions. At the same time, the Compose compiler will continue
generating singleton classes and lambdas for inline functions to support binary compatibility for modules that were compiled under the previous model.
Remember to check if you set this option using freeCompilerArgs. This method can cause the build to fail when used alongside the plugin, due to an option being
effectively set twice.
The JetBrains team is proceeding with the deprecation of the kotlin-android-extensions plugin. If you try to use it in your project, you'll now get a configuration
error, and no plugin code will be executed.
The legacy kotlin.incremental.classpath.snapshot.enabled property has been removed from the Kotlin Gradle plugin. The property used to provide an
opportunity to fall back to a built-in ABI snapshot on the JVM. The plugin now uses other methods to detect and avoid unnecessary recompilations, making the
property obsolete.
Documentation updates
The Kotlin documentation has received some notable changes:
Gradle best practices page – learn essential best practices for optimizing your Gradle builds and improving performance.
Compose Multiplatform and Jetpack Compose – an overview of the relation between the two UI frameworks.
227
Kotlin Multiplatform and Flutter – see the comparison of two popular cross-platform frameworks.
Kotlin/Native as an Apple framework – create your own framework and use Kotlin/Native code from Swift/Objective-C applications on macOS and iOS.
To update to the new Kotlin version, change the Kotlin version to 2.1.20 in your build scripts.
The Kotlin 2.0.20 release is out! This version includes performance improvements and bug fixes for Kotlin 2.0.0, where we announced the Kotlin K2 compiler as
Stable. Here are some additional highlights from this release:
The data class copy function to have the same visibility as the constructor
Static accessors for source sets from the default target hierarchy are now available in multiplatform projects
Concurrent marking for Kotlin/Native has been made possible in the garbage collector
A new option allows sharing JVM artifacts between Gradle projects as class files
Support for UUIDs has been added to the common Kotlin standard library
IDE support
The Kotlin plugins that support 2.0.20 are bundled in the latest IntelliJ IDEA and Android Studio. You don't need to update the Kotlin plugin in your IDE. All you need
to do is to change the Kotlin version to 2.0.20 in your build scripts.
Language
Kotlin 2.0.20 begins to introduce changes to improve consistency in data classes and replace the Experimental context receivers feature.
228
Our migration plan starts with Kotlin 2.0.20, which issues warnings in your code where the visibility will change in the future. For example:
fun main() {
val positiveNumber = PositiveInteger.create(42) ?: return
// Triggers a warning in 2.0.20
val negativeNumber = positiveNumber.copy(number = -1)
// Warning: Non-public primary constructor is exposed via the generated 'copy()' method of the 'data' class.
// The generated 'copy()' will change its visibility in future releases.
}
For the latest information about our migration plan, see the corresponding issue in YouTrack.
To give you more control over this behavior, in Kotlin 2.0.20 we've introduced two annotations:
@ConsistentCopyVisibility to opt in to the behavior now before we make it the default in a later release.
@ExposedCopyVisibility to opt out of the behavior and suppress warnings at the declaration site. Note that even with this annotation, the compiler still reports
warnings when the copy() function is called.
If you want to opt in to the new behavior already in 2.0.20 for a whole module rather than in individual classes, you can use the -Xconsistent-data-class-copy-
visibility compiler option. This option has the same effect as adding the @ConsistentCopyVisibility annotation to all data classes in a module.
In future Kotlin releases, context receivers will be replaced by context parameters. Context parameters are still in the design phase, and you can find the proposal in
the KEEP.
Since the implementation of context parameters requires significant changes to the compiler, we've decided not to support context receivers and context
parameters simultaneously. This decision greatly simplifies the implementation and minimizes the risk of unstable behavior.
We understand that context receivers are already being used by a large number of developers. Therefore, we will begin gradually removing support for context
receivers. Our migration plan starts with Kotlin 2.0.20, where warnings are issued in your code when context receivers are used with the -Xcontext-receivers
compiler option. For example:
class MyContext
context(MyContext)
// Warning: Experimental context receivers are deprecated and will be superseded by context parameters.
// Please don't use context receivers. You can either pass parameters explicitly or use members with extensions.
fun someFunction() {
}
If you use context receivers in your code, we recommend that you migrate your code to use either of the following:
Explicit parameters.
Before After
229
Before After
Alternatively, you can wait until the Kotlin release where context parameters are supported in the compiler. Note that context parameters will initially be introduced
as an Experimental feature.
Kotlin Multiplatform
Kotlin 2.0.20 brings improvements to source set management in multiplatform projects as well as deprecates compatibility with some Gradle Java plugins due to
recent changes in Gradle.
Static accessors for source sets from the default target hierarchy
Since Kotlin 1.9.20, the default hierarchy template is automatically applied to all Kotlin Multiplatform projects. And for all of the source sets from the default
hierarchy template, the Kotlin Gradle plugin provided type-safe accessors. That way, you could finally access source sets for all the specified targets without having
to use by getting or by creating constructs.
Kotlin 2.0.20 aims to improve your IDE experience even further. It now provides static accessors in the sourceSets {} block for all the source sets from the default
hierarchy template. We believe this change will make accessing source sets by name easier and more predictable.
Each such source set now has a detailed KDoc comment with a sample and a diagnostic message with a warning in case you try to access the source set without
declaring the corresponding target first:
kotlin {
jvm()
linuxX64()
linuxArm64()
mingwX64()
sourceSets {
commonMain.languageSettings {
progressiveMode = true
}
jvmMain { }
linuxX64Main { }
linuxArm64Main { }
// Warning: accessing source set without registering the target
iosX64Main { }
}
}
230
Accessing the source sets by name
Deprecated compatibility with Kotlin Multiplatform Gradle plugin and Gradle Java plugins
In Kotlin 2.0.20, we introduce a deprecation warning when you apply the Kotlin Multiplatform Gradle plugin and any of the following Gradle Java plugins to the same
project: Java, Java Library, and Application. The warning also appears when another Gradle plugin in your multiplatform project applies a Gradle Java plugin. For
example, the Spring Boot Gradle Plugin automatically applies the Application plugin.
We've added this deprecation warning due to fundamental compatibility issues between Kotlin Multiplatform's project model and Gradle's Java ecosystem plugins.
Gradle's Java ecosystem plugins currently don't take into account that other plugins may:
Also publish or compile for the JVM target in a different way than the Java ecosystem plugins.
Have two different JVM targets in the same project, such as JVM and Android.
Have a complex multiplatform project structure with potentially multiple non-JVM targets.
Unfortunately, Gradle doesn't currently provide any API to address these issues.
We previously used some workarounds in Kotlin Multiplatform to help with the integration of Java ecosystem plugins. However, these workarounds never truly
solved the compatibility issues, and since the release of Gradle 8.8, these workarounds are no longer possible. For more information, see our YouTrack issue.
While we don't yet know exactly how to resolve this compatibility problem, we are committed to continuing support for some form of Java source compilation in
your Kotlin Multiplatform projects. At a minimum, we will support the compilation of Java sources and using Gradle's java-base plugin within your multiplatform
projects.
In the meantime, if you see this deprecation warning in your multiplatform project, we recommend that you:
1. Determine whether you actually need the Gradle Java plugin in your project. If not, consider removing it.
2. Check if the Gradle Java plugin is only used for a single task. If so, you might be able to remove the plugin without much effort. For example, if the task uses a
Gradle Java plugin to create a Javadoc JAR file, you can define the Javadoc task manually instead.
Otherwise, if you want to use both the Kotlin Multiplatform Gradle plugin and these Gradle plugins for Java in your multiplatform project, we recommend that you:
231
The separate subproject must not be a multiplatform project, and you must only use it to set up a dependency on your multiplatform project.
For example, you have a multiplatform project called my-main-project and you want to use the Application Gradle plugin to run a JVM application.
Once you've created a subproject, let's call it subproject-A, your parent project structure should look like this:
.
├── build.gradle.kts
├── settings.gradle
├── subproject-A
└── build.gradle.kts
└── src
└── Main.java
In your subproject's build.gradle.kts file, apply the Application plugin in the plugins {} block:
Kotlin
plugins {
id("application")
}
Groovy
plugins {
id('application')
}
In your subproject's build.gradle.kts file, add a dependency on your parent multiplatform project:
Kotlin
dependencies {
implementation(project(":my-main-project")) // The name of your parent multiplatform project
}
Groovy
dependencies {
implementation project(':my-main-project') // The name of your parent multiplatform project
}
Kotlin/Native
Kotlin/Native receives improvements in the garbage collector and for calling Kotlin suspending functions from Swift/Objective-C.
By default, application threads must be paused when GC is marking objects in the heap. This greatly affects the duration of the GC pause time, which is important
for the performance of latency-critical applications, such as UI applications built with Compose Multiplatform.
Now, the marking phase of the garbage collection can be run simultaneously with application threads. This should significantly shorten the GC pause time and help
improve app responsiveness.
232
How to enable
The feature is currently Experimental. To enable it, set the following option in your gradle.properties file:
kotlin.native.binary.gc=cms
Now, the embedBitcode parameter for the framework configuration, as well as the -Xembed-bitcode and -Xembed-bitcode-marker command line arguments are
deprecated.
If you still use earlier versions of Xcode but want to upgrade to Kotlin 2.0.20, disable bitcode embedding in your Xcode projects.
The feature was enabled by default, but unfortunately, it sometimes led to crashes when the application was run simultaneously with Xcode Instruments. Starting
with Kotlin 2.0.20, it requires an explicit opt-in with the following compiler option:
-Xbinary=enableSafepointSignposts=true
If you've previously switched the default behavior for non-main threads with the kotlin.native.binary.objcExportSuspendFunctionLaunchThreadRestriction=none
binary option, you can now remove it from your gradle.properties file.
Kotlin/Wasm
In Kotlin 2.0.20, Kotlin/Wasm continues the migration towards named exports and relocates the @ExperimentalWasmDsl annotation.
To fully support named exports, this warning has now been upgraded to an error. If you use a default import, you encounter the following error message:
Do not use default import. Use the corresponding named import instead.
This change is part of a deprecation cycle to migrate towards named exports. Here's what you can expect during each phase:
In version 2.0.0: A warning message is printed to the console, explaining that exporting entities via default exports is deprecated.
In version 2.0.20: An error occurs, requesting the use of the corresponding named import.
233
Previously, the @ExperimentalWasmDsl annotation for WebAssembly (Wasm) features was placed in this location within the Kotlin Gradle plugin:
org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
The previous location is now deprecated and might lead to build failures with unresolved references.
To reflect the new location of the @ExperimentalWasmDsl annotation, update the import statement in your Gradle build scripts. Use an explicit import for the new
@ExperimentalWasmDsl location:
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
Alternatively, remove this star import statement from the old package:
import org.jetbrains.kotlin.gradle.targets.js.dsl.*
Kotlin/JS
Kotlin/JS introduces some Experimental features to support static members in JavaScript and to create Kotlin collections from JavaScript.
This feature is Experimental. It may be dropped or changed at any time. Use it only for evaluation purposes. We would appreciate your feedback on it in
YouTrack.
Starting with Kotlin 2.0.20, you can use the @JsStatic annotation. It works similarly to @JvmStatic and instructs the compiler to generate additional static methods
for the target declaration. This helps you use static members from your Kotlin code directly in JavaScript.
You can use the @JsStatic annotation for functions defined in named objects, as well as in companion objects declared inside classes and interfaces. The compiler
generates both a static method of the object and an instance method in the object itself. For example:
class C {
companion object {
@JsStatic
fun callStatic() {}
fun callNonStatic() {}
}
}
It's also possible to apply the @JsStatic annotation to a property of an object or a companion object, making its getter and setter methods static members in that
object or the class containing the companion object.
This feature is Experimental. It may be dropped or changed at any time. Use it only for evaluation purposes. We would appreciate your feedback on it in
YouTrack.
234
Kotlin 2.0.0 introduced the ability to export Kotlin collections to JavaScript (and TypeScript). Now, the JetBrains team is taking another step to improve collection
interoperability. Starting with Kotlin 2.0.20, it's possible to create Kotlin collections directly from the JavaScript/TypeScript side.
You can create Kotlin collections from JavaScript and pass them as arguments to the exported constructors or functions. As soon as you mention a collection
inside an exported declaration, Kotlin generates a factory for the collection that is available in JavaScript/TypeScript.
// Kotlin
@JsExport
fun consumeMutableMap(map: MutableMap<String, Int>)
Since the MutableMap collection is mentioned, Kotlin generates an object with a factory method available from JavaScript/TypeScript. This factory method then
creates a MutableMap from a JavaScript Map:
// JavaScript
import { consumeMutableMap } from "an-awesome-kotlin-module"
import { KtMutableMap } from "an-awesome-kotlin-module/kotlin-kotlin-stdlib"
consumeMutableMap(
KtMutableMap.fromJsMap(new Map([["First", 1], ["Second", 2]]))
)
This feature is available for the Set, Map, and List Kotlin collection types and their mutable counterparts.
Gradle
Kotlin 2.0.20 is fully compatible with Gradle 6.8.3 through 8.6. Gradle 8.7 and 8.8 are also supported, with only one exception: If you use the Kotlin Multiplatform
Gradle plugin, you may see deprecation warnings in your multiplatform projects calling the withJava() function in the JVM target. We plan to fix this issue as soon as
possible.
You can also use Gradle versions up to the latest Gradle release, but if you do, keep in mind that you might encounter deprecation warnings or some new Gradle
features might not work.
This version brings changes such as beginning the deprecation process for the old incremental compilation approach based on JVM history files, as well as a new
way of sharing JVM artifacts between projects.
The incremental compilation approach based on JVM history files suffered from limitations, such as not working with Gradle's build cache and not supporting
compilation avoidance. In contrast, the new incremental compilation approach overcomes these limitations and has performed well since its introduction.
Given that the new incremental compilation approach has been used by default for the last two major Kotlin releases, the kotlin.incremental.useClasspathSnapshot
Gradle property is deprecated in Kotlin 2.0.20. Therefore, if you use it to opt out, you will see a deprecation warning.
This feature is Experimental. It may be dropped or changed at any time. Use it only for evaluation purposes. We would appreciate your feedback on it in
YouTrack. Opt-in is required (see details below).
In Kotlin 2.0.20, we introduce a new approach that changes the way the outputs of Kotlin/JVM compilations, such as JAR files, are shared between projects. With
this approach, Gradle's apiElements configuration now has a secondary variant that provides access to the directory containing compiled .class files. When
configured, your project uses this directory instead of requesting the compressed JAR artifact during compilation. This reduces the number of times JAR files are
compressed and decompressed, especially for incremental builds.
Our testing shows that this new approach can provide build performance improvements for Linux and macOS hosts. However, on Windows hosts, we have seen a
235
degradation in performance due to how Windows handles I/O operations when working with files.
To try this new approach, add the following property to your gradle.properties file:
kotlin.jvm.addClassesVariant=true
By default, this property is set to false and the apiElements variant in Gradle requests the compressed JAR artifact.
Gradle has a related property that you can use in your Java-only projects to only expose the compressed JAR artifact during compilation instead of the
directories containing compiled .class files:
org.gradle.java.compile-classpath-packaging=true
For more information on this property and its purpose, see Gradle's documentation on the Significant build performance drop on Windows for huge multi-
projects.
We would appreciate your feedback on this new approach. Have you noticed any performance improvements while using it? Let us know by adding a comment in
YouTrack.
From the java-test-fixtures plugin's implementation and api dependency types to the test source set compilation classpath.
From the main source set's implementation and api dependency types to the java-test-fixtures plugin's source set compilation classpath.
This difference in behavior led to some projects finding resource files multiple times in the classpath.
As of Kotlin 2.0.20, the Kotlin Gradle plugin's behavior is aligned with Gradle's java-test-fixtures plugin so this problem no longer occurs for this or other Gradle
plugins.
As a result of this change, some dependencies in the test and testFixtures source sets may no longer be accessible. If this happens, either change the dependency
declaration type from implementation to api or add a new dependency declaration on the affected source set.
Added task dependency for rare cases when the compile task lacks one on an artifact
Prior to 2.0.20, we found that there were scenarios where a compile task was missing a task dependency for one of its artifact inputs. This meant that the result of
the dependent compile task was unstable, as sometimes the artifact had been generated in time, but sometimes, it hadn't.
To fix this issue, the Kotlin Gradle plugin now automatically adds the required task dependency in these scenarios.
In very rare cases, we've found that this new behavior can cause a circular dependency error. For example, if you have multiple compilations where one compilation
can see all internal declarations of the other, and the generated artifact relies on the output of both compilation tasks, you could see an error like:
To fix this circular dependency error, we've added a Gradle property: archivesTaskOutputAsFriendModule.
By default, this property is set to true to track the task dependency. To disable the use of the artifact in the compilation task, so that no task dependency is
required, add the following in your gradle.properties file:
236
kotlin.build.archivesTaskOutputAsFriendModule=false
Compose compiler
In Kotlin 2.0.20, the Compose compiler gets a few improvements.
If your app is built with Compose compiler 2.0.10 or newer but uses dependencies built with version 2.0.0, these older dependencies may still cause recomposition
issues. To prevent this, update your dependencies to versions built with the same Compose compiler as your app.
This change has also been applied to the Compose compiler Gradle plugin. To configure feature flags going forward, use the following syntax (this code will flip all
of the default values):
composeCompiler {
featureFlags = setOf(
ComposeFeatureFlag.IntrinsicRemember.disabled(),
ComposeFeatureFlag.OptimizeNonSkippingGroups,
ComposeFeatureFlag.StrongSkipping.disabled()
)
}
Or, if you are configuring the Compose compiler directly, use the following syntax:
-P plugin:androidx.compose.compiler.plugins.kotlin:featureFlag=IntrinsicRemember
The enableIntrinsicRemember, enableNonSkippingGroupOptimization, and enableStrongSkippingMode properties have been therefore deprecated.
We would appreciate any feedback you have on this new approach in YouTrack.
Strong skipping mode is a Compose compiler configuration option that changes the rules for what composables can be skipped. With strong skipping mode
enabled, composables with unstable parameters can now also be skipped. Strong skipping mode also automatically remembers lambdas used in composable
functions, so you should no longer need to wrap your lambdas with remember to avoid recomposition.
237
This feature flag is now ready for wider testing. Any issues found when enabling the feature can be filed on the Google issue tracker.
Previously, the Compose compiler would report an error when attempting to do this even though it is valid Kotlin code. We've now added support for this in the
Compose compiler, and the restriction has been removed. This is especially useful for including default Modifier values:
Default parameters for open composable functions are still restricted in 2.0.20. This restriction will be addressed in future releases.
Standard library
The standard library now supports universally unique identifiers as an Experimental feature and includes some changes to Base64 decoding.
This feature is Experimental. To opt in, use the @ExperimentalUuidApi annotation or the compiler option -opt-in=kotlin.uuid.ExperimentalUuidApi.
Kotlin 2.0.20 introduces a class for representing UUIDs (universally unique identifiers) in the common Kotlin standard library to address the challenge of uniquely
identifying items.
Additionally, this feature provides APIs for the following UUID-related operations:
Generating UUIDs.
println(uuid1)
// 550e8400-e29b-41d4-a716-446655440000
println(uuid1 == uuid2)
// true
println(uuid2 == uuid3)
// true
println(uuid1 == randomUuid)
238
// false
To maintain compatibility with APIs that use java.util.UUID, there are two extension functions in Kotlin/JVM for converting between java.util.UUID and
kotlin.uuid.Uuid: .toJavaUuid() and .toKotlinUuid(). For example:
This feature and the provided APIs simplify multiplatform software development by allowing code sharing among multiple platforms. UUIDs are also ideal in
environments where generating unique identifiers is difficult.
The HexFormat class and its properties are Experimental. To opt in, use the @OptIn(ExperimentalStdlibApi::class) annotation or the compiler option -opt-
in=kotlin.ExperimentalStdlibApi.
Kotlin 2.0.20 adds a new minLength property to the NumberHexFormat class, accessed through HexFormat.number. This property lets you specify the minimum
number of digits in hexadecimal representations of numeric values, enabling padding with zeros to meet the required length. Additionally, leading zeros can be
trimmed using the removeLeadingZeros property:
fun main() {
println(93.toHexString(HexFormat {
number.minLength = 4
number.removeLeadingZeros = true
}))
// "005d"
}
The minLength property does not affect parsing. However, parsing now allows hex strings to have more digits than the type's width if the extra leading digits are
zeros.
The Base64 class and its related features are Experimental. To opt in, use the @OptIn(ExperimentalEncodingApi::class) annotation or the compiler option
-opt-in=kotlin.io.encoding.ExperimentalEncodingApi.
Two changes were introduced to the Base64 decoder's behavior in Kotlin 2.0.20:
239
withPadding function for padding configuration
A new .withPadding() function has been introduced to give users control over the padding behavior of Base64 encoding and decoding:
This function enables the creation of Base64 instances with different padding options:
You can create Base64 instances with different padding options and use them to encode and decode data:
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
@OptIn(ExperimentalEncodingApi::class)
fun main() {
// Example data to encode
val data = "fooba".toByteArray()
Documentation updates
The Kotlin documentation has received some notable changes:
Improved Standard input page - Learn how to use Java Scanner and readln().
Improved K2 compiler migration guide - Learn about performance improvements, compatibility with Kotlin libraries and what to do with your custom compiler
plugins.
Improved Exceptions page - Learn about exceptions, how to throw and catch them.
Improved Test code using JUnit in JVM - tutorial - Learn how to create tests using JUnit.
Improved Interoperability with Swift/Objective-C page - Learn how to use Kotlin declarations in Swift/Objective-C code and Objective-C declarations in Kotlin
240
code.
Improved Swift package export setup page - Learn how to set up Kotlin/Native output that can be consumed by a Swift package manager dependency.
To update to the new Kotlin version, change the Kotlin version to 2.0.20 in your build scripts.
The Kotlin 2.0.0 release is out and the new Kotlin K2 compiler is Stable! Additionally, here are some other highlights:
Kotlin 2.0 is a huge milestone for the JetBrains team. This release was the center of KotlinConf 2024. Check out the opening keynote, where we announced exciting
updates and addressed the recent work on the Kotlin language:
241
Gif
IDE support
The Kotlin plugins that support Kotlin 2.0.0 are bundled in the latest IntelliJ IDEA and Android Studio. You don't need to update the Kotlin plugin in your IDE. All you
need to do is to change the Kotlin version to Kotlin 2.0.0 in your build scripts.
For details about IntelliJ IDEA's support for the Kotlin K2 compiler, see Support in IDEs.
For more details about IntelliJ IDEA's support for Kotlin, see Kotlin releases.
Kotlin K2 compiler
The road to the K2 compiler has been a long one, but now the JetBrains team is finally ready to announce its stabilization. In Kotlin 2.0.0, the new Kotlin K2
compiler is used by default and it is Stable for all target platforms: JVM, Native, Wasm, and JS. The new compiler brings major performance improvements, speeds
up new language feature development, unifies all platforms that Kotlin supports, and provides a better architecture for multiplatform projects.
The JetBrains team has ensured the quality of the new compiler by successfully compiling 10 million lines of code from selected user and internal projects. 18,000
developers were involved in the stabilization process, testing the new K2 compiler across a total of 80,000 projects and reporting any problems they found.
To help make the migration process to the new compiler as smooth as possible, we've created a K2 compiler migration guide. This guide explains the many
benefits of the compiler, highlights any changes you might encounter, and describes how to roll back to the previous version if necessary.
In a blog post, we explored the performance of the K2 compiler in different projects. Check it out if you'd like to see real data on how the K2 compiler performs and
find instructions on how to collect performance benchmarks from your own projects.
You can also watch this talk from KotlinConf 2024, where Michail Zarečenskij, the lead language designer, discusses the feature evolution in Kotlin and the K2
compiler:
242
Gif
Compilation of other Gradle plugins if they are used in projects with Gradle versions below 8.3.
If you encounter any of the problems mentioned above, you can take the following steps to address them:
Set the language version for buildSrc, any Gradle plugins, and their dependencies:
kotlin {
compilerOptions {
languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9)
apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9)
}
}
If you configure language and API versions for specific tasks, these values will override the values set by the compilerOptions extension. In this case,
language and API versions should not be higher than 1.9.
In Kotlin 2.0.0, we've made improvements related to smart casts in the following areas:
243
Local variables and further scopes
Inline functions
Exception handling
However, if you declared the variable outside the if condition, no information about the variable would be available within the if condition, so it couldn't be smart-
cast. This behavior was also seen with when expressions and while loops.
From Kotlin 2.0.0, if you declare a variable before using it in your if, when, or while condition, then any information collected by the compiler about the variable will
be accessible in the corresponding block for smart-casting.
This can be useful when you want to do things like extract boolean conditions into variables. Then, you can give the variable a meaningful name, which will improve
your code readability and make it possible to reuse the variable later in your code. For example:
class Cat {
fun purr() {
println("Purr purr")
}
}
fun main() {
val kitty = Cat()
petAnimal(kitty)
// Purr purr
}
In this case, you still had to manually check the object type afterward before you could access any of its properties or call its functions. For example:
interface Status {
fun signal() {}
}
interface Ok : Status
interface Postponed : Status
interface Declined : Status
244
// to type Any, so calling the signal() function triggered an
// Unresolved reference error. The signal() function can only
// be called successfully after another type check:
// check(signalStatus is Status)
// signalStatus.signal()
}
}
The common supertype is an approximation of a union type. Union types are not supported in Kotlin.
Inline functions
In Kotlin 2.0.0, the K2 compiler treats inline functions differently, allowing it to determine in combination with other compiler analyses whether it's safe to smart-
cast.
Specifically, inline functions are now treated as having an implicit callsInPlace contract. This means that any lambda functions passed to an inline function are
called in place. Since lambda functions are called in place, the compiler knows that a lambda function can't leak references to any variables contained within its
function body.
The compiler uses this knowledge along with other compiler analyses to decide whether it's safe to smart-cast any of the captured variables. For example:
interface Processor {
fun process()
}
processor = nextProcessor()
}
return processor
}
245
}
}
This change also applies if you overload your invoke operator. For example:
interface Provider {
operator fun invoke()
}
Exception handling
In Kotlin 2.0.0, we've made improvements to exception handling so that smart cast information can be passed on to catch and finally blocks. This change makes
your code safer as the compiler keeps track of whether your object has a nullable type. For example:
fun testString() {
var stringInput: String? = null
// stringInput is smart-cast to String type
stringInput = ""
try {
// The compiler knows that stringInput isn't null
println(stringInput.length)
// 0
// Trigger an exception
if (2 > 1) throw Exception()
stringInput = ""
} catch (exception: Exception) {
// In Kotlin 2.0.0, the compiler knows stringInput
// can be null, so stringInput stays nullable.
println(stringInput?.length)
// null
fun main() {
testString()
}
interface Rho {
operator fun inc(): Sigma = TODO()
}
interface Tau {
fun tau() = Unit
}
246
fun main(input: Rho) {
var unknownObject: Rho = input
In Kotlin 2.0.0, our implementation of the new Kotlin K2 compiler included a redesign of the compilation scheme to ensure strict separation b