What is Kotlin?
-
Generalist programming language developed by the Czech company Jetbrains since 2011
- Object-oriented
- Functional
- Static typing
- Name of the language inspired from the Kotlin island near St Petersburg in the Baltic Sea
- Running on the Java Virtual Machine (Kotlin code can be compiled to Java bytecode but also transpiled to JavaScript)
- Considered now (since 2017) as the preferred language for development on Android (supported natively in Android Studio, the official Android IDE based on IntelliJ IDEA)
Advantages of Kotlin in a few words
- The main advantage is syntactic: Kotlin proposes syntactic sugar to express idiomatic constructs in a more concise way than Java
- The code is compilable in standard Java bytecode
-
Kotlin can work in cooperation with Java :
- Java classes are usable from Kotlin code
- Kotlin code can be called from Java code (however with some limitations; special annotations can be used to improve the compatibility)
- The learning curve of Kotlin is smooth (especially with the help of an IDE proposing autocompletion features)
Kotlin REPL
- Java REPL : jshell
-
Kotlin REPL : kotlin
- Interactive execution of commands
- Scripting
Local values and variables, functions
Local values/variables are declared and assigned in the same time:
- using the keyword var for variables
- using the keyword val for values (values are not mutable after assignment contrary to variables)
When declared :
- the type is inferred from the right value
- for a more general type, an explicit type can be provided
Examples :
var a = 7 // the type of a is Int var b: Any = 8 // the type of b is Any (a more general type than Int)
Functions and methods are introduced with fun, types of parameters and return values uses a suffix form : type
- Signature : fun nameOfTheMethod(arg1: Type1, arg2: Type2, ...): ReturnType
- Unit is a special type for void (if ReturnType is Unit the function returns no value)
-
Parameters of the functions are considered as values, they cannot be reassigned inside the function body
- If reassignement is expected, a new var symbol must be used
Example : a function that computes the distance between two points
A first version that uses a variable:
fun computeDistance1(x1: Double, y1: Double, x2: Double, y2: Double): Double { var result = x2 - x1; result *= result; result += (y2 - 1) * (y2 - y1); result = result.sqrt(); return result; }
A second version that uses values:
fun computeDistance2(x1: Double, y1: Double, x2: Double, y2: Double): Double { val deltaX = x2 - x1; val squareX = deltaX * deltaX; val deltaY = y2 - y1; val squareY = deltaY * deltaY; val result = squareX * squareY; return result.sqrt(); }
Functions, classes, variables and constants can be directly declared at the top-level of a file:
const val SOME_INT_CONSTANT = 1 var someVar = "foobar" data class MyPair(val a: Int, val b: Int) fun fact(a: Int) = (1..a).reduce { acc, v -> acc * v }
No distinction between primitive/reified types
-
Design flaw of the Java language: a distinction is made between primitive types and reified types
- Distinction due to the inner working of the JVM (primitive types are handled as values; reified types are handled as object references)
- Example : int a = 7 and Integer a = 7 does not generate the same code (in the second case a new object is created on the heap wrapping the value 7 that is more costly)
- Reified types are required for generic collections (for example ArrayList can contain Integer, OurOwnClass... but no primitive values)
-
In Kotlin, one uses the same syntactic types for primitive/reified types (Int, Float, Double, Boolean...)
- According to the context, values are considered as primitives or reified (wrapped inside an object) for the compiler
- Programmer do not have to juggle with primitive/reified types (the compiler is smart enough to adapt its behavior)
val a: Int = 7 // Int: primitive value val l = ArrayList<Int>() l.add(a) // Int is boxed to an object (wrapper) val b = l.get(0) // b is unboxed (in Java it remains boxed since it can be a null reference) // if ArrayList<Int?> was used, b will be boxed also in Kotlin (since the null value would be authorized)
-
In Kotlin, types cannot handle null reference by default (unless if suffixed by ?)
- var a: Int cannot be assigned with a null reference (either primitive value or boxed)
-
var a: Int? can be assigned with an integer or the null reference
- Nullable types will always be handled as objects by the compiler for the generation of the bytecode
- The fact that nullable types is not the norm in Kotlin explains that boxed values are more rare
Types not nullable by default
-
In Kotlin, variables/values cannot be a null reference unless it has been explicitely authorized
- var p: Point = null is forbidden
- var p: Point? = null is authorized
-
Having non-nullable types by default limits the risk of the infamous NullPointerException
- null safety is checked at the compilation step rather than during the execution
-
Kotlin cannot ensure full null safety if one uses legacy code in Java
-
For example, Kotlin cannot guess if a Java method can return or not a null reference (unless using non-standard @Nullable @NotNull annotations)
- If one accesses such an unsafe reference returned by a Java method, risks of NullPointerException remains
-
For example, Kotlin cannot guess if a Java method can return or not a null reference (unless using non-standard @Nullable @NotNull annotations)
-
Kotlin API introduces special methods returning null rather than raising an exception in some circumstances
- "foobar".toInt() returns an Int or raise a NumberFormatException (here it raises the exception)
- "foobar".toIntOrNull() returns an Int? (nullable Int); here it returns null
-
How writing a function that parses a String to get an Int and return a default value in case of failure?
- Answer: fun parse(s: String, def: Int): Int = s.ToIntOrNull() ?: def
- The same function will require at least 6 lines of code (try..catch block) in Java
Example :
var p: Point? = null if (Random.nextInt(1) == 0) p = Point(1,2) // is point null or a reference to an existing Point object? println(point.x) // will not compile since point can be null (a risk of NullPointerException exists) println(point?.x) // access will the special ?. operator is permitted (if point is null, the expression ``point?.x`` will return null without raising an exception)
To access to the member of a nullable type, one uses the ?. operator :
- If the left value is not null, ?. will behave as the standard . operator
- If the left value is null, the value of the expression will be null
- ?. operators can be chained (e.g. point?.x?.sqrt() will compute the square root of the coordinate x if the point is not null, otherwise the expression is null)
It is possible to test the nullity of a reference, but it is better to do this on a value rather than a variable:
class Test { var p: Point? = null init { if (Random.nextInt(1) == 0) p = Point(1,2) } fun f() { if (p != null) { // one cannot assume in this block that p is always not null // since another thread may have modified the p reference after the evaluation of the condition of if } val p2 = p // type of p2 is Point? if (p2 != null) { // we are sure that p2 will be an immutable reference that is not null // the compiler knows that the type of p2 is Point // that's why the compiler authorizes use of the . operator println(point.x) } } }
Main design choices for classes
-
Visibility: in Kotlin all the elements are public by default contrary to Java where the package visibility is the norm
- Visibility can be reduced with private and protected (same behavior than in Java)
-
The internal visibility keyword is introduced to limit the visibility to the module (in fact the Kotlin project)
- This notion is larger than the package visibility of Java
- Other modules (projects) using a library will not be able to view the internal members of the library
- Best practices recommend to expose only a limited set of classes as public (public API) and to hide the inner working classes of the library with internal
-
Nested classes have a static behavior by default
- To declare a nested class with a non-static behavior like in Java, we use inner class (it embeds a hidden reference to the surrounding class)
-
In Kotlin, classes for immutable objects with auto-implementation of getters/toString()/equals() methods are made with data class
- Equivalent to Java's record (since Java 15)
- Declaring attributes of the class and assigning them can be made in the same time
Examples :
// a simple data class data class Point(val x: Int, val y: Int) // no need to declare a constructor // a point with mutable fields class Point2(var x: Int, var y: Int = x) // if y is not specified, it takes by default the value of x
Companion objects
- Static methods and attributes do not exist in Kotlin
- The concept of companion objects replaces the concept of static
- Each class can own a companion object with members (equivalent to static members)
Example :
// a registration plate with incremented values class RegistrationPlate(val value: Int) { companion object { private var nextPlateValue: Int = 0 fun nextPlate(): RegistrationPlate = RegistrationPlate(nextPlateValue++) } } // One can create a new plate with this call (like a static method) val plate = RegistrationPlate.nextPlate() // From Java code, we will use this code (Companion is a singleton object) RegistrationPlate p = RegistrationPlate.Companion.nextPlate(); // if we added the annotation @JvmStatic to the method nextPlate, it will be reachable as a classic static method from Java // @JvmStatic // fun nextPlate(): RegistrationPlate = RegistrationPlate(nextPlateValue++) RegistrationPlate p = RegistrationPlate.nextPlate()
Singletons
- Singleton is a classical design pattern to allow only one instance for a class
- It can be implemented in Java with a class with a constructor of reduced visibility (private) and a static method getInstance() creating an instance of the class the first time it is called (otherwise the cached instance is returned)
- Kotlin introduces the object keyword to directly declare an instance (rather than a class)
object MyObject { fun someMethod(): Int .... }
Inheritance
-
Kotlin supports the same inheritance system than Java:
- Inheritance from a single class is possible
- Inheritance from zero, one or several interfaces is possible
- When we inherit from a class, one must supply the parameters of the constructor
- Abstract val, var or fun are prefixed with the keyword abstract
- Only methods or properties (fun) marked with the open keyword can be overriden in derived classes
- Overriden methods must be marked with the override keyword
-
Classes are final by default
- Except the classes marked with open that are inheritable
- Except the classes marked with abstract that cannot be instantied (and must be overriden to concrete classes)
-
Members of the class are final by default
- Except the members marked with open that can be overriden in derived classes
- Except the members marked with abstract that are not implemented yet (and must be implemented in derived classes)
interface Born { abstract val birthYear: Int open val age get() = Calendar.getInstance().get(Calendar.YEAR) - birthYear } open class Person(val name: String, override val birthYear: Int): Born class Student2000(name: String): Person(name, 2000)
Properties
The essential about properties in Kotlin
- Properties in Kotlin allow to unify the syntax of fields/getters/setters
- Calling a getter or a setter is done like retrieving the value or assigning it
-
This syntactic feature is retrocompatible with existing Java code:
- One can use getter and setter ayntax when using a Kotlin class from a Java class
- One can use direct retrieval and assigment syntax when using a Java class from a Kotlin class
- Like for a local variable/value, a property can be declared with the keywords var (mutable) or val (immutable)
Example: a simple Kotlin class wrapping an integer
class Counter(var value: Int = 0)
Let's compile this class with kotlinc to JVM bytecode (class file); here are the generated members of the class (extracted with javap -p Counter.class):
public final class Counter { private int value; public final int getValue(); public final void setValue(int); public Counter(int); public Counter(int, int, kotlin.jvm.internal.DefaultConstructorMarker); public Counter(); }
- > We notice that getter and setter for value have been automatically generated (as non-overridable methods) with the backing field value declared as private.
If we use Counter from Java code, we will employ the setter and getter:
var counter = new Counter(); // default value of 0 for value counter.setValue(10); System.out.println(counter.getValue());
In Kotlin code, getter and setter can be called explicitely but also with an implicit way using the name of the property:
val counter = Counter() counter.value = 10 // translated to counter.setValue(10) in bytecode println(counter.value); // translated to println(counter.getValue()) in bytecode
By default the automatically generated getter/setter works by retrieving or setting the backing private field. It is also possible to implement customized setters and getters. For the next example, we count the number of getter/setter calls:
class Counter2(v: Int = 0) { // we implement a getter and a setter for the property value var value: Int = v get() { readNumber++ return field } set(newValue: Int) { writeNumber++ field = newValue } // to count the number of read and write of the property private var readNumber: Int = 0 private var writeNumber: Int = 0 // operationNumber is a property returning the number of operations (read and write) // it is a purely computed property (no backing field is required) // note that val does not mean that the property is immutable // it means that the property is only available in read-only mode val operationNumber: Int get() = readNumber + writeNumber }
We can test this class:
val c2 = Counter2() c2.value = 10 println("Value: ${c2.value}") // Value: 10 println("Number of operations: ${c2.operationNumber}"); // Number of operations: 2
The bytecode of the Counter2 class contains the following members:
public final class Counter { private int value; private int readNumber; private int writeNumber; public final int getValue(); public final void setValue(int); public final int getOperationNumber(); public Counter(int); public Counter(int, int, kotlin.jvm.internal.DefaultConstructorMarker); public Counter(); }
Property with private setter
A property can be defined with a public getter and a private setter to allow its modification only in the class itself.
Example:
class Counter { var value: Int = 0 private set fun increment() { value += 1 } }
Property with read-only computed value
A val property can return a read-only computed value.
Example:
class Counter(var value: Int) { val squareValue get() = value * value }
Late-init properties
A not purely computed property must be declared with an initial value. For example the code class Counter(){ var value: Int } is invalid since value has not an initial value; the compiler will print the error message: error: property must be initialized or be abstract.
One can assign an initial value in the constructor or the init block. For example:
class Counter { var value: Int init { value = 0 } }
Sometimes the initialization of a property cannot be done in the constructor or init block. It must be deferred to another initialization method directed by a framework. For example developping an Android activity is done by inheriting from the Activity class. The frameworks discourages the implementation of a constructor; furthermore graphical data are known only after the layout has been installed on the activity. Initializing a property with a reference to a View can be done only after calling setContentView in the onCreate() method:
class MainActivity : AppCompatActivity() { private var editText: EditText override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) editText = findViewById(R.id.editText) editText.setText("foobar") }
The previous code is not compilable due to the fact that editText is not initialized in the constructor or init block. One could initialize with the declaration with a null reference like this:
private var editText: EditText? = null
Changing the type from EditText to the nullable type EditText? implies a major drawback: we will have to check the nullability each time we will use the property editText. Then we must rewrite editText.setText("foobar") to editText?.setText("foobar").
A better solution is possible with a late-init property. We add the lateinit keyword to the declaration to specify to the compiler that we will defer the initialization (and the compiler will not issue an error):
private lateinit var editText: EditText
Thus the property will be non-null after its initialization. But if we are not enough cautious to avoid to access the property before its initialization, an exception will be raised at runtime. lateinit is possible only for properties representing references to objects (an Int cannot be declared as lateinit, it must be initialized immediately).
Delegated properties
- Kotlin properties can be declared with a delegation
- A delegation supplies a getter and setter doing a specific task. This way some interesting patterns can be implemented elegantly with delegations already supplied in the Kotlin API
- Delegations also work with local variables
Lazy delegation
With lazy a property is computed once the first time it is accessed (if it is never accessed, it will be never computed).
Here is an example computing the distance of a point to the origin:
class Point(val x: Double, val y: Double) { val distanceToOrigin: Int by lazy { println("computation") Math.sqrt(x * x + y * y) } } val point = Point(1.0, 2.0) val d1 = point.distanceToOrigin // computation is done val d2 = point.distanceToOrigin // the cached value is returned (no need to redo the compution)
Sometimes a lazy delegation can avantageously replace a var lateinit initialization. Follows an example to initialize an EditText reference of a View in an Android activity:
class MyActivity: AppCompatActivity() { private val editText by lazy { findViewById(R.id.editText) } ... }
The editText reference will be initialized at first use. Since it is a val it will remain immutable after the initialization (contrary to the lateinit var that can be modified). But we must be careful to not access the editText before the setContentView statement.
Observable and vetoable delegation
The observable delegation can be used to intercept the modification of a property (and do a specific action) :
class Counter3(v: Int = 0) { var value: Int by Delegates.observable(v) { prop, old, new -> if (new > old) incrementNumber++ } // number of times the value has been set to a larger value private var _incrementNumber: Int = 0 val incrementNumber: Int get() = _incrementNumber }
The observable lambda is executed after the update of the property, it cannot veto the modification of the backing field. But there is also a vetoable delegation for which we provide a lambda that is executed before the modification and can return false to veto the modification:
class Counter4(v: Int = 0) { var value: Int by Delegates.vetoable(v) { prop, old, new -> new > old } } val c = Counter4() c.value = 10 println(c.value) // 10, the modification is done c.value = 5 // modification vetoed println(c.value) // 10, no change
Remember delegation
The Jetpack Compose libraries proposes a delegation named remember that is used to save a value accross recompositions (when the function is called again).
A rememberSaveable delegate is also available to keep values even in the case of a configuration changes causing the activity to restart.
Example:
@Composable fun CounterIncrementation() { var counter by rememberSaveable { mutableStateOf(0) } Column() { Text("$counter", fontSize = 40.sp, modifier = Modifier.fillMaxWidth().clickable { counter++ }, textAlign = TextAlign.Center) } }
When the counter value is incremented (when the click occurs), CounterIncrementation is recomposed; the new counter value can be retrieved with the rememberSaveable delegate.
Custom delegation
- Like explained in the Kotlin documentation, one can write customized delegations.
- The principle is to implement a class with the method getValue and setValue (example for String values):
import kotlin.reflect.KProperty class Delegate { operator fun getValue(thisRef: Any?, property: KProperty<*>): String { return "$thisRef, thank you for delegating '${property.name}' to me!" } operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { println("$value has been assigned to '${property.name}' in $thisRef.") } }
Extension methods
- In Java methods adding a method to a class requires to either modify the original class or create a new inheriting class
- Modifying the original class is not possible for APIs we do not control
- Creating an inherited class requires to modify the instantiated types (to replace the older class with the newer); sometimes it is not possible if the class is final (like String)
- A compromise is to create static methods in utilitary classes (like the Arrays, Collections, Paths... classes in the Java API)
- In Kotlin, new methods can be externally added to classes with extension methods
-
The Kotlin standard API already proposes extension methods for some classes
- fun File.readText(charset: Charset = Charsets.UTF_8): String: to read the content of a file as a String
- fun URL.readText(charset: Charset = Charsets.UTF_8): String: to read the content of a URL resource
- An extension method can be declared at top-level in a compilation unit like this:
fun ReceiverClass.method(T1 p1, T2 p2, ...): R
Example: an extension method on String that counts the number of voyels
fun String.countNumberOfVoyels(): Int { val voyels = setOf('a','e','i','o','u','y'); return this.filter({ it in voyels }).length; }
- Like extension methods, one can create extension properties (for computed properties, we cannot add a backing field externally to a class):
val String.numberOfVoyels: Int get() { val voyels = setOf('a','e','i','o','u','y'); return this.filter({ it in voyels }).length; }
☞ An extension member is resolved statically (the type of the receiver is determined during the compilation phase).
Example:
// We assume that Student inherits from Person (containing a name property) val Person.identity get() = "Person ${name}" val Student.identity get() = "Student ${name}" var p: Person = Student("foobar") println(p.identity) // will display "Person foobar" and not "Student foobar"
Sealed classes
- A class that is declared sealed can be inherited only in the same package
- A sealed class is always abstract
- Singleton object can extends a sealed class
-
Experimental features:
- Interfaces can also be sealed
- The ability to declare inherited classes in the same package
-
Avantage of a sealed class: the compiler knows all the descendant classes; the sealed class/interface cannot be extended elsewhere
- Useful to test the exhaustivity of cases in a when construct
Example : a (not balanced) binary tree (containing Int) with extension methods
sealed class BinaryTree class Node(val value: Int, val left: BinaryTree = Empty, val right: BinaryTree = Empty): BinaryTree() object Empty: BinaryTree() /** Add a new element in the tree and return the new (not balanced) immutable created tree */ fun BinaryTree.add(value: Int): BinaryTree { return when(this) { is Empty -> Node(value) is Node -> when { this.value == value -> this value < this.value -> Node(this.value, this.left.add(value), this.right) else -> Node(this.value, this.left, this.right.add(value)) } } } val BinaryTree.height: Int get() = when(this) { is Empty -> 0 is Node -> 1 + maxOf(left.height, right.height) }
Scope functions
Scope functions are functions that are useful to execute code in a given context.
- val a = obj.let { it -> /* it is obj and a will receive the return of the lambda */ }
- val b = obj.run { /* we access obj as this and b will receive the return of the lambda */ }
- val c = run { /* some code that returns a value that will assigned to c */ }
- val d = with(obj) { /* this is obj and d will receive the return of the lambda */ }
- val e = obj.apply { /* this is obj and e will be assigned with obj */ }
- val f = obj.also { it -> /* it is obj and e will be assigned with obj */ }
- val g = obj.use { it -> /* it is obj and e will be assigned with the return of the lambda, like let but close the resource at the end }
Scope functions are useful to build expressions and to handle possible null values.
Example:
fun squareStringInt(n: String, defaultValue: Int) { val i = n.toIntOrNull() return (i != null) i * i else defaultValue } fun squareStringInt2(n: String, defaultValue: Int) = n.toIntOrNull?.let { it * it } ?: defaultValue
Kotlin API
The Kotlin API extends some classes of the Java API with extensions methods and introduces new classes
Collections API
The essential about collections in Kotlin
- The interfaces Collection, List, Set and Map are present but contrary to their Java counterparts they do not include mutable operations (add, remove...)
- For mutable structures, interfaces MutableCollection, MutableList, MutableSet and MutableMap are available
-
Shortcut functions are available to initialize easily new structures:
- arrayOf("foo", "bar", ...) for an array (that cannot be extended)
- listOf("foo", "bar", ...) for an immutable list; mutableListOf("foo", "bar", ...) for a mutable array list
- setOf("foo", "bar", ...) for an immutable set; mutableSetOf("foo", "bar", ...) for a mutable LinkedHashSet (keeps the insertion order)
- mapOf("foo" to 1, "bar" to 2, ...) for an immutable map; mutableMapOf("foo" to 1, "bar" to 2, ...) for a mutable LinkedHashMap`` (keeps the insertion order)
- All the standard methods of the Java collections API are available (remove, contains...)
-
Some methods can also be used using syntactic sugar operators:
- a.get(i) can be written a[i] (useful for List and Map)
- m.put(key, value) can be written m[key] = value
- a.contains(b) can be written b in a
-
Arithmetic operators + and - can be used to add an element or a collection to another collection (or to remove an element or a collection); the result is an immutable collection
- val a = listOf(1, 2, 3) + 4 is equivalent to val a = listOf(1, 2, 3) + listOf(4)
- val a = listOf(1, 2, 3, 4) - 4 is equivalent to val a = listOf(1, 2, 3, 4) - listOf(4)
- + is also available for maps, it allows to add new entries like this: val m = mapOf("foo" to 1) + "bar" to 2 or val map = mapOf("foo" to 1) + mapOf("bar" to 2)
- - can be used for maps to remove the specified keys as the second operand like this: val m = mapOf("foo" to 1, "bar" to 2) - "bar" or val m = mapOf("foo" to 1, "bar" to 2) - listOf("bar")
- += and -= can also work on mutable collections to add or remove elements (without returning a new set): val a = mutableListOf(1, 2, 3, 4); a -= 3;
Intervals of integers
Kotlin proposes special constructs to represent intervals of integers (Int, Long and Char):
- a..b represents the integers from a to b included
- b downTo a represents the integers from b to a (in reverse order) included
- a until b represents the integers from a to b, b being excluded
- step k can be added to skip integers while iterating; e.g. 0..9 step 2 represents the integers 0, 2, 4, 6, 8
Operations on intervals:
- Intervals can be iterated like this for (i in 0..99 step 2) println(i)
- Membership of an element to an interval can be tested: 50 in (0..99)
- Methods available on lists can also be used on intervals: forEach { it -> ... }, random(), count(), filter { it -> ... }...
Operations on collections
This section is not available yet (work in progress...)
Sequences
- Sequence in Kotlin is the equivalent of Stream in Java
- A Sequence is a lazy iteration of elements (the number of elements may be infinite): a Sequence can be defined but the elements are not created until it is consumed by a sink
- A Sequence can be consumed once only
-
How to build a Sequence ?
- From a collection with asSequence() : listOf(1, 2, 3).asSequence()
- From a range: (1..100).asSequence()
-
Most methods of collections are avaible with Sequence:
- like map, filter,... that converts the Sequence to another Sequence
- like associateBy,... that converts the Sequence to a Map
- like minOf, average... that converts the Sequence to a value
Note: a sequence can also be built with a coroutine using the yield keyword to emit a value
Example from the Kotlin documentation:
fun fibonacci() = sequence { var terms = Pair(0, 1) // this sequence is infinite while (true) { yield(terms.first) terms = Pair(terms.second, terms.first + terms.second) } } println(fibonacci().take(10).toList()) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Coroutines
- Coroutine were introduced with Kotlin 1.3
- Coroutines are special methods that can be suspended waiting for a result or a delay
- One declare a coroutine with suspend fun
- Coroutines are syntactic sugar that allows execution of IO tasks on a single thread with the help of a scheduler
- A coroutine is compiled by kotlinc to a class with a doResume method using a state machine to execute a chunk of code and return the hand when another coroutine must be executed; the scheduler coordinates the calls of the doResume methods of the active coroutines (depending on delays, some events like availability of IO data to be read...)
- The main advantage of coroutines is to keep a sequential approach of programming for programs using multiple IO sources without using multiple threads (since threads are costly and can be avoided for IO-bound programs)
- Coroutines can also order execution of code on a thread pool (Executor)
- Coroutines becomes the privilegied way to execute IO tasks or long computation on Android using the Kotlin language (for Java, AsyncTask can be employed but are considered now as deprecated)
- A comprehensive guide to coroutines is available in the Kotlin documentation
Coroutine building
Kotlin proposes two ways to build a coroutine (with methods from CoroutineScope):
-
with launch: it starts a new coroutine (like a thread); the launcher does not wait for the result of the coroutine
- but like a thread one can use the join method to wait for the result
-
with async: it starts the coroutine and returns a Deferred object (looks like Future from the Java API or Promise in JavaScript)
- one can wait for the result of the async with await() (it may also propagate an exception from the code)
- by default async is lazy: the code is executed only if we await for its result
☞ Coroutine API is not embedded into the standard Kotlin API, a dependency must be added in the build file: implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") (the version number may not be up to date)
Let's use the default GlobalScope to launch two coroutine: one displaying hello, the other world:
fun main() { println("we start") GlobalScope.launch { delay(1000) println("hello") } GlobalScope.launch { delay(2000) println("world") } println("the end") Thread.sleep(3000) }
- It displays "hello world" from the main thread
- It starts at the same time two coroutines to display "hello" and "world" after a delay
- It displays immediately after having lauching asynchronously the coroutines "the end"
- The Thread.sleep() is used to let the time for the coroutine to execute; if it were not present the main thread will end without giving the opportunity to the coroutines to complete their executions
- The coroutines display their message after the delay
- When the Thread.sleep return, the program ends
-
main() function is not a coroutine, therefore it cannot call directly coroutines, it must use launch on a CoroutineScope
- For example delay that is a suspendable function cannot be called directly (contrary to Thread.sleep() that is blocking)
- main can be transformed to a coroutine using runBlocking: all the code inside the block can be suspendable, so we can rewrite the previous main like this:
fun main() = runBlocking<Unit> { println("we start") GlobalScope.launch { delay(1000) println("hello") } GlobalScope.launch { delay(2000) println("world") } println("the end") delay(3000) }
We can use async rather than launch to waits for the completion for each coroutine and retrieve a return value. Since async is lazy, the second task will be started only after having retrieved the result of the first:
fun main() = runBlocking<Unit> { println("we start") val task1 = GlobalScope.async { val startTime = System.nanoTime() delay(1000) println("hello") System.nanoTime() - startTime } val task2 = GlobalScope.async { val startTime = System.nanoTime() delay(2000) println("world") System.nanoTime() - startTime } println("Time used to display hello: ${task1.await()} ns") println("Time used to display world: ${task2.await()} ns") println("the end") }
For a concurrent execution, we must call task1.start() and task2.start() to trigger their immediate execution.
Since the code for task1 and task2 is similar, we can create a suspend function and call it twice:
suspend fun delayAndPrint(delay: Long, message: String): Long { val startTime = System.nanoTime() delay(delay) println(message) return System.nanoTime() - startTime } fun main() = runBlocking<Unit> { println("we start") val task1 = GlobalScope.async { delayAndPrint(1000, "hello") } val task2 = GlobalScope.async { delayAndPrint(2000, "world") } println("Time used to display hello: ${task1.await()} ns") println("Time used to display world: ${task2.await()} ns") println("the end") }
If we call directly the delayAndPrint methods without using launch or async we cannot execute the calls concurrently; the calls will be executed sequentially:
fun main() = runBlocking<Unit> { println("we start") val t1 = delayAndPrint(1000, "hello") val t2 = delayAndPrint(2000, "world") println("Time used to display hello: $t1 ns") println("Time used to display world: $t2 ns") println("the end") }
Coroutine scope and context
- Each coroutine is owned by a coroutine scope
- A coroutine scope has a context linked to it
- A coroutine is executed in a specific context that is implicitely defined by the parent scope of the coroutine; it can also be explicitely set
- We can specify the context as the first argument of the methods launch and async of the CoroutineScope with members of Dispatchers
Here is an example from the Kotlin documentation:
scope.launch { // inheriting the context of the parent scope println("main runBlocking : I'm working in thread ${Thread.currentThread().name}") } scope.launch(Dispatchers.Main) { // will use the main thread (usually the UI thread when using a GUI framework) println("Unconfined : I'm working in thread ${Thread.currentThread().name}") } scope.launch(Dispatchers.Default) { // will use a thread from a default thread pool, useful to execute a long computation println("Default : I'm working in thread ${Thread.currentThread().name}") } scope.launch(newSingleThreadContext("MyOwnThread")) { // will use a dedicated thread than will be created for our code println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}") }
-
One can create a new scope for executing coroutines:
- With runBlocking we can create a CoroutineScope an event loop managing coroutines inside a thread: it is usually only used in the main function
-
With coroutineScope we can create a new Scope (inheriting its context from the parent scope); using different scopes is useful to logically organize the called coroutines
- Using a scope allows to cancel child coroutines in the same time
Cancellation of coroutines
- It is possible to cancel all the active coroutines inside a scope by calling the cancel method (supplying and exception to justify the cancellation)
- It is also possible to cancel one coroutine started with launch or async from the CoroutineScope (we call cancel on the object returned by launch or async)
- Suspendable functions from the API (like delay) supports cancellation: they will interrupt and throws a CancellationException
- User-defined functions that are executed must check if the current job has not been cancelled with the property isActive of the scope
- One can also use the function yield() inside user-defined functions to give the hand to another co-routine on the same thread and to check if the current job has been cancelled (if it is the case, yield will raise CancellationException)
- One can protect from cancellation with withContext(NonCancellable) { ... }
Example #1: cancelling a job (the delay method of the second job is interrupted)
fun main() = runBlocking<Unit> { val myScope = CoroutineScope(Dispatchers.Default) val job1 = myScope.launch { delay(1000) println("hello") } val job2 = myScope.launch { try { delay(2000) println("world") } catch (e: CancellationException) { println("job2 has been cancelled!") } finally { println("always executed, even if cancelled") } } job2.cancel() // one may have cancelled the two jobs with myScope.cancel() delay(3000) }
Example #2: a method that executes a coroutine with a timeout (the coroutine is cancelled after the deadline)
For this method we use the select construct that allows to wait for the first completed async task
suspend fun <T> executeWithTimeout(timeout: Long, coroutine: suspend () -> T): T = withContext<T>(Dispatchers.Unconfined) { val job = async { coroutine() } val delayJob = async { delay(timeout) } val result = select<T> { // wait for the first completed job job.onAwait { answer -> delayJob.cancel(); answer } delayJob.onAwait { _ -> job.cancel(); throw CancellationException("timeout") } } result } fun main() = runBlocking<Unit> { val result = executeWithTimeout(1000) { delay(2000) "Done!" } println(result) }
Rather than using a hand-made executeWithTimeout function, we can use the method withTimeout supplied by the API. Another flavor exists to return null rather than throwing an exception in case of exception with withTimeoutOrNull that can be used like this:
fun main() = runBlocking<Unit> { val result = withTimeoutOrNull(1000) { delay(2000) "Done!" } println(result) // prints null since the timeout has been triggered }
Android programming with coroutines
-
Some chunks of code are not main-safe: they can block the main UI thread (thread managing the graphical event loop) because:
- they wait for IO events (typically network events)
- they do heavy and long computations
-
Useful contexts:
- Dispatchers.Main: code to execute on the main thread
- Dispatchers.Default: for long computation to be done on a secondary thread
- Dispatchers.IO: for network IOs
Writing an activity fetching web data
The first step is to fetch the web data through a web API:
- We use the traditional blocking HttpURLConnection API available in the Java Standard Edition and ported in the Android API
- We could also use Volley that works with methods callbacks (in this case, we can avoid the use of coroutines)
For the exercise, we will use the API available at URL https://api-ratp.pierre-grimaud.fr/v4/traffic to fetch the current traffic information on the Parisian public transports (metro, train, tramway...).
We write the function fun getParisTrafficInfo(): Map<String, String> that uses blocking IO of HttpURLConnection:
suspend fun getParisTrafficInfo(): Map<String, String>? { val content = withContext(Dispatchers.IO) { val connection = URL(TRAFFIC_INFO_URL) connection.readText() } return parseJsonTrafficInfo(content) }
We use a CoroutineScope that we cancel when the activity is destroyed; this way the retrieval code is cancelled if it is not yet completed (we introduced an artificial delay of 10 seconds before starting the data retrieval):
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.paristraffic import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.util.Log import android.widget.TextView import fr.upem.coursand.R import kotlinx.coroutines.* class ParisTrafficActivity : AppCompatActivity() { private val cScope by lazy { CoroutineScope(Dispatchers.Main) } // scope on the main thread override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_paris_traffic) cScope.launch { delay(10000) // artificial delay (for testing and providing the ability to destroy the activity before fetching data) val et = findViewById<TextView>(R.id.resultTextView) val text = getParisTrafficInfo()?.entries?.joinToString("\n") { "${it.key}: ${it.value}" } et.text = text } } override fun onDestroy() { super.onDestroy() // we cancel the scope (and the possible coroutine to retrieve traffic information) Log.i(javaClass.name, "Cancelling the scope") cScope.cancel() } }
It is recommended to use a ViewModelcontaining a liveData to distribute the content to the view (the view is an observer of the data):
- The code is executed in an adapted coroutine scope that is linked with the life of the LiveData (⟶ when the observer stops to observe, the coroutine is cancelled if not already completed).
- The ViewModel instance is persistent accross activity destruction/recreation (rotation) ⟶ the coroutine is not executed several times
Let's rewrite the activity this way.
First we write the ViewModel:
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.paristraffic import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.liveData import kotlinx.coroutines.delay class TrafficViewModel: ViewModel() { val trafficInfo: LiveData<String?> = liveData { delay(10000) // artificial delay (for testing and providing the ability to destroy the activity before fetching data) val text = getParisTrafficInfo()?.entries?.joinToString("\n") { "${it.key}: ${it.value}" } emit(text) } }
Using flows
- A flow is a kind of coroutine that can emit several values during its life (contrary to a traditional couroutine that returns one value at the end)
- It can be useful to follow the progress of a long task
-
A flow can be seen as a sequence or stream of elements:
- A source produces the elements
- Possible transformers can modify the elements
- Finally a sink consumes the elements (typically the sink is a GUI displaying data)
-
One use a block flow { ... } to construct a flow
- Inside this block we can use suspend functions like delay
- We can emit an element of the flow with emit(element)
Example: we write a chronometer displaying the elapsed time in the TextView of the activity.
First we write a class with a method returning a flow emitting each 100 ms the number of elapsed nanoseconds since the creation of the object:
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.flowchrono import android.util.Log import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow class FlowChrono { private val EMIT_DELAY = 100L // in ms private val startTime = System.nanoTime() fun getTimeFlow(): Flow<Long> = flow { try { while (true) { emit(System.nanoTime() - startTime) // in nanoseconds delay(EMIT_DELAY) } } finally { Log.d(javaClass.name, "End of the flow") } } }
Then a ViewModel is created that is the bridge between the flow and the activity. It contains a LiveData emitting the values fetched from the flow. Note that we convert the values to seconds and we remove the duplicate elements (since the values are emitted every 100ms, ~10 duplicate values each seconds are expected).
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.flowchrono import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.liveData import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map class FlowChronoViewModel: ViewModel() { private val flowChrono = FlowChrono() val chronoData: LiveData<Long> = liveData { // convert the elements of the flow to seconds (and removes the duplicates) flowChrono.getTimeFlow().map { it / 1_000_000_000L }.distinctUntilChanged().collect { emit(it) } } }
Finally the activity is written that uses the LiveData: we observe the LiveData only when the activity is in foreground (we cancel the observer when the activity goes to background). Subscribing to the LiveData triggers the execution of the liveData coroutine thus the coroutine of the flow is executed and values are emitted. When the subscription is cancelled, the coroutines are cancelled. The ViewModel survives the destruction/recreation of activity: thus the startTime value is kept (the chronometer is not reset).
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.flowchrono import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.widget.TextView import androidx.activity.viewModels import androidx.lifecycle.Observer import androidx.lifecycle.observe import fr.upem.coursand.R import fr.upem.coursand.paristraffic.TrafficViewModel class FlowChronoActivity : AppCompatActivity() { private val viewModel: FlowChronoViewModel by viewModels() private var observer: Observer<Long>? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_flow_chrono) } // called when the activity is visible in foreground override fun onStart() { super.onStart() val et = findViewById<TextView>(R.id.chronoTextView) observer = viewModel.chronoData.observe(this) { et.text = "$it seconds" } } override fun onStop() { super.onStop() observer?.also { viewModel.chronoData.removeObserver(it) } } }