Kotlin is officially the preferred language for Android apps, and support for Kotlin is positive. Developers are gravitating to Kotlin — so much so that it was even voted the fourth most loved programming language last year. Startups and even large financial institutions and technology ventures and enterprises are joining the Kotlin bandwagon.
Oursky’s developers first started using Kotlin to develop native Android apps, but we wanted to test our limits by using Kotlin to create our very own chat app/social commerce platform, Gesprek (which means “conversation” in Dutch). We had multiple clients asking for a social commerce platform. Rather than building separate and individual ones, we figured it would be better to create and own one, then integrate this technology to our clients. This is the brainchild behind Gesprek, Ourksy’s very own conversational marketing platform. For us, it was a win-win situation: We can explore new technologies while developing a product that will help our clients improve their customer experience.
Hopefully our experience with Kotlin would help fellow developers as well as tech-savvy businesses find their momentum when working on projects that involve Kotlin. Many of my takeaways and examples are also inspired by Magnus Vinther’s article and Kotlin’s own documentations.
Why Kotlin?
Kotlin brings the benefits of a modern programming language without the rigid rules and complexities. Kotlin is open-source and can be used for Android, iOS, web, server-side, back-end, and data science-related software or app development. Here’s an overview of Kotlin’s advantages:
Compatibility with Java. Programs written in Kotlin can be compiled into Java Bytecode, i.e., running the code on the Java Virtual Machine (JVM) just like Java.
Flexible support. There are options available if using Java and JVM is not possible (e.g., iOS, embedded devices) or not favored (i.e., slow to use). Kotlin/Native, for example, can be used to compile Kotlin code to native binaries.
Intuitive and concise syntax. Kotlin has the advantages of a modern programming language without unnecessarily adding new restrictions. Kotlin code blocks are easy to understand. Below are some examples:
/*
Create a POJO with getters, setters, `equals()`, `hashCode()`, `toString()` and `copy()` /in a single line:
*/
data class Customer(val name: String, val email: String, val company: String)
// Or filter a list using a lambda expression:
val positiveNumbers = list.filter { it > 0 }
// Want a singleton? Create an object:
object ThisIsASingleton {
val companyName: String = "JetBrains"
}
Snippet of code that shows what makes Kotlin easy to understand
build("PacMan", 400, 300) // equivalent
build(title = "PacMan", width = 400, height = 300) // equivalent
build(width = 400, height = 300, title = "PacMan") // equivalent
Snippet of code showing flexible named arguments via Kotlin
when (x) {
1 -> print("x is 1")
2 -> print("x is 2")
3, 4 -> print("x is 3 or 4")
in 5..10 -> print("x is 5, 6, 7, 8, 9, or 10")
else -> print("x is out of range")
}
val res: Boolean = when {
obj == null -> false
obj is String -> true
else -> throw IllegalStateException()
}
Snippet of code showing how versatile ‘when’ expression is in Kotlin
Convenience in expressing lambdas. According to Kotlin, lambda expressions are “function literals” — they are not declared but passed immediately as an expression. You can think of them as an expression whose value is a function; you don’t have to name them. Here’s an example of passing a lambda expression into a function with max as a parameter:
max(strings, { a, b -> a.length < b.length })
Lambda expressions also allow you to create a function like this:
val sum = { x: Int, y: Int -> x + y } // type: (Int, Int) -> Int
val res = sum(1,2) // res == 3
With lambdas, Kotlin allows you to write code in a more straightforward manner. Here are some related Lambda-related features you can look over when delving into Kotlin:
- Last expression in lambda as return value. Here’s a snippet of code showing the sum function, with last expression in lambda as the return value (x + y):
- Trailing lambdas. If the last parameter of a function is a function, a lambda can be placed outside the parentheses as the corresponding argument. This results in reduced syntactic noise, as shown below:
val product = items.fold(1) { acc, e -> acc * e }
- If the function has only one parameter (and the last at the same time) where it’s a function, you can completely remove the parentheses:
val positiveNumbers = list.filter { x -> x > 0 }
- Implicit name of single parameter (it). An example is the positiveNumbers function, where it can replace a manually named parameter x with it:
val positiveNumbers = list.filter { it > 0 }
Using Kotlin for Back-end Development
With Kotlin’s capabilities, we explored how we can use it for back-end development. Our journey into this project led us to delving into domain-specific languages (DSL) and coroutines for Kotlin.
Domain-Specific Languages (DSL)
In a nutshell, DSL is a language written to address a specific domain or set of concerns. DSL abstracts the complex, underlying logic so developers, programmers, and its users can focus on writing intuitive commands or instructions. Examples of DSL are: Hypertext Markup Language (HTML), Structured Query Language (SQL), YAML Ain’t Markup Language (YAML), Extensible Markup Language (XML), Cascading Style Sheets (CSS), and Gradle build language.
When you see them, you can instinctively understand their purpose. DSLs are so intuitive that most of the time you don’t realize you’re coding with DSLs. There are quite a lot of Kotlin libraries and frameworks that have DSL interfaces, such as Ktor and Koin.
Why DSL? Developers at Oursky often switch between several projects. We believe that implementing DSLs in Gesprek would improve the expressiveness of code, and it will be easier for them to understand and pick up on Gesprek’s context, even those with less technical know-how. This is just one of our forays into easing our workflow and development processes.
DSL in Action: Class Instantiating DSLs on User and Company
Let’s take Gesprek as an example to see how DSL works. In Gesprek, a User is a customer support staff who responds to enquiries. Each User belongs to a Company. Here are the DSLs that create instances of them:
user {
id = "some-uuid"
name = "elliot"
company {
id = "some-other-uuid"
name = "oursky"
}
Note that there are, in fact, two classes instantiating DSLs: user { … } and the one nested within company { … }. Behind the scenes, there are only four blocks of code. Here’s a look at the data classes modelling User and Company:
data class User(
var id: String? = null,
var name: String? = null,
var company: Company? = null
)
data class Company(
var id: String? = null,
var name: String? = null
)
For the sake of clearer presentation, the code above is a bit different from our actual implementation. All val were substituted with var. This is not the recommended way to write data classes. Immutable val should be used in the constructors, while variables and mutation should be delegated to a separate build class instead. A property called role that reflects a User’s access right has also been removed to keep the code blocks simpler.
Here’s an overview of the implementations of the DSLs:
// fun user(lambda: User.() -> Unit): User {
// val u = User()
// u.lambda()
// return u
// }
fun user(lambda: User.() -> Unit): User = User().apply(lambda)
fun User.company(lambda: Company.() -> Unit) {
company = Company().apply(lambda)
}
The commented user function works exactly like it’s intended to, only that the Kotlin apply function is used to shorten it. You might be wondering what the first argument in the lambda function represent — the one with .().
Take lambda: User.() -> Unit as an example. User is the receiver type, while the remaining part of notation after the dot () -> Unit denotes the type of the function called lambda that can be called on the receiver. Obviously, the lambda function is scoped within the user function, so it works like an extension function on the data class User just within the function. Receiver plays a crucial role in keeping our DSL simple.
As seen above, there is no need for it, as lambda doesn’t have that parameter. It works like an extension function, but have the this expression implicitly implied when called. So the this expression, i.e., the properties of the receiver object that can be directly accessed when passing a lambda expression as lambda.
In the example below, I included a full version (no removed noise) to further explain:
user {
id = "some-uuid"
name = "elliot"
company {
id = "some-other-uuid"
name = "oursky"
}
user(lambda = {
this.id = "some-uuid"
this.name = "elliot"
this.company {
this.id = "some-other-uuid"
this.name = "oursky"
}
})
In the user function, a new User instance is created. After the lambda function is executed on the newly created instance, the properties of the instance are mutated. It is then returned. Because there is only one parameter, which happens to be a function, we can call user function like: user { … }.
The other function is a mix of Kotlin’s extension function and receiver object. As you have seen in the final goal of our DSLs, there is a nested company { … } in there. First, to make the nested call possible, which is on the User data class, we have to actually declare an extension function on User first, which is what we have done with User.company(…). Then once again to reduce as much syntactic noise as possible, the receiver type comes into play, where this time the type before the dot is Company.
To summarize, it is pretty easy to write DSLs in Kotlin without parentheses and receiver function through the help of lambda expression. The DSLs are more human-readable where even those with less technical background can grasp what’s going on.
Coroutine’s Implementation for Kotlin
Coroutines are lightweight threads. They have pre-defined thread pools and smart scheduling to ensure they efficiently utilize computational resources. Consider a task that the threads can execute. It can be suspended at points you can define, freeing up the thread to work on something else, such as another coroutine. You can later resume control of the suspended coroutine. Basic task waiting and cancelling mechanisms are also supported with intuitive syntax.
Here are some of the advantages of implementing coroutines for Kotlin:
Feature-rich and flexible application program interfaces (APIs). Kotlin coroutines were made to enable you to write asynchronous code in a sequential manner. They have features to tackle the most asynchronous problems like async & await and cancellation mechanism.
The high-level APIs are well-designed and flexible, allowing you to code and accurately solve the problems. A quick example would be the CoroutineDispatcher, which decides what thread a coroutine will be launched on. You get to pick from a list of default dispatcher with pre-defined behaviors or create one of your own and gain full control of its lifecycle. Kotlin has an official documenation about it.
Cheaper and more convenient than threads. Kotlin’s coroutines are convenient and cheap to create, which give them an edge over threads that are normally considered as rare and expensive resources that must be handled carefully. The code below, for example, creates 1,000 coroutines and will not run out of memory. Try to create the same amount of threads — most likely you will get the ran-out-of-memory error message.
Unlike threads, coroutines are language abstractions which aren’t tied to native resources. Switching between them doesn’t involve the OS kernel, which is why creating a thousand of them won’t break like threads do.
import kotlinx.coroutines.*
fun main() = runBlocking {
for (i in 0..999) {
val job = async {
delay(1)
print("$i ")
}
job.await()
}
}
Simple management of code. The design of structured concurrency in Kotlin coroutines ensures every single coroutine is tracked and handled to ensure code quality and keep things managed. I wrote a separate section for this below, along with the various aspects of coroutines, which you can play around with. It includes how to launch coroutines, suspend functions, and enforce structured concurrency, which are all based on Kotlin’s official documentation. Kotlin-based coroutines have addressed the issues in asynchronus programming well, with features and designs that address them.
Here is additional information on implementing coroutines for Kotlin:
launch and join
Let’s see how a coroutine can be launched first.
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = GlobalScope.launch { // launch a new coroutine and keep a reference to its Job
delay(1000L)
println("World!")
}
println("Hello,")
job.join() // wait until child coroutine completes
}
In the example above, launch is a coroutine builder that returns a Job, which is assigned to the local constant job. Try take out the join function, and you will see the World! message is no longer printing. This is because by calling join() the caller coroutine gets suspended until job is complete. A Job can also be cancelled.
async and await
This pair works like the launch and join, but async returns a Deferred while await waits for it to complete with a value. Below is an example:
import kotlinx.coroutines.*
import kotlin.system.*
fun main() = runBlocking<Unit> {
val time = measureTimeMillis {
val one = doSomethingUsefulOne()
val two = doSomethingUsefulTwo()
println("The answer is ${one + two}")
}
println("Completed in $time ms")
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // pretend we are doing something useful here
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // pretend we are doing something useful here, too
return 29
}
Use the first pair when you don’t need the result or just need it to be finished. Otherwise, go with async and await to get the computed value.
suspend Functions
Suspending functions are marked with the keyword suspend, like the doSomethingUseful
functions or delay() we have just seen. They suspend a coroutine from executing and can only be called from a coroutine or another suspended function.
Structured Concurrency
This pattern is enforced in Kotlin coroutines where every coroutine must have a CoroutineScope
wrapping. Below is an example:
// won't work
suspend fun coroutineWithoutScope() {
val deferred = async { doSomething }
}
// works
suspend fun coroutineWithScope() {
return coroutineScope {
val deferred = async { doSomething }
}
}
Wrapping them with a scope marks the boundary of the code, and more importantly, clearly defines the behaviors upon completion, cancellation, or error encounter. How so? The code below explains this:
import kotlinx.coroutines.*
fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
fun main() = runBlocking() {
log("Main coroutine starts.")
val deferred = async() {
// addNums()
// addNumsThrowsV1()
// addNumsThrowsV2()
}
val job = launch() {
sayHi()
}
deferred.await()
job.join()
}
suspend fun addNums(): Int {
return coroutineScope {
val deferred1 = async { returnOne() }
val deferred2 = async { returnTwo() }
deferred1.await() + deferred2.await()
}
}
suspend fun addNumsThrowsV1(): Int {
return coroutineScope {
try {
val deferred1 = async { returnOne(throws = true) }
val deferred2 = async { returnTwo() }
deferred1.await() + deferred2.await()
} catch (e: Throwable) {
log("addNumThrowsV1 caught: $e")
-1
}
}
}
suspend fun addNumsThrowsV2(): Int {
return supervisorScope {
try {
val deferred1 = async { returnOne(throws = true) }
val deferred2 = async { returnTwo() }
deferred1.await() + deferred2.await()
} catch (e: Throwable) {
log("addNumThrowsV2 caught: $e")
-1
}
}
}
suspend fun sayHi() {
delay(500)
log("Hello friend.")
}
suspend fun returnOne(throws: Boolean = false): Int {
delay(100)
if (!throws) {
log("returnOne returning.")
return 1
} else {
log("returnOne throwing an exception.")
throw Throwable("Something went wrong!")
}
}
suspend fun returnTwo(): Int {
delay(100)
log("returnTwo returning.")
return 2
}
You will have to interact with the code above a bit. There are three calls in lines 8-10; uncomment them one by one and you will see three different outcomes:
- Uncomment addNums() and see the tasks complete successfully.
- Uncomment addNumsThrowsV1() and keep the other two commented. An exception will be thrown from returnOne and caught in addNumThrowsV1(). However, the entire coroutine tree will still be cancelled so that you won’t see the “Hello friend.” message.
- Uncomment addNumsThrowsV2 and keep the other two commented. An exception will be thrown from returnOne and caught in addNumThrowsV2(). This time the exception will not propagate to the parent and cancel the other child sayHi().
You may have already noticed that addNumThrowsV1 and addNumThrowsV2 only have one difference, the scope wrapper. With coroutineScope, when any child fails, the scope follows, so that all the rest of the children are cancelled. To keep the other children alive when one fails, use supervisorScope and catch the throwable. This enforces you to define the behavior of a structured coroutine tree clearly.
From Kotlin to Gesprek and Skygear
We designed Gesprek as a real-time chat and social commerce platform. Low latency is a must, and extra caution is needed to implement its core functionalities, especially the payment feature. We also had to ensure that notifications are not lost in the transaction, as this would impair communication with the customers. Kotlin coroutines fit the bill — we were able to develop a system that safely addresses these requirements.
User authentication was also a key feature, as we had two different types of users: the customers, and the staff who communicate with them. For this, we used our own Skygear, which provides an authentication solution called AuthGear and intuitive microservice deployment. AuthGear has all the modern authentication features and has gone through rounds of security audits, so we directly integrated it with Gesprek.
Gesprek’s integration with Skygear include decorated interfaces, such as a web portal to customize AuthGear’s behaviors (e.g., how complex a user’s password must be, the type of authentication to be implemented, etc.). Gesprek’s infrastructure is also powered by Skygear through the use of commands from Skygear’s command-line interace, skycli, to our CI/CD pipeline.
Throughout our journey we are delighted to have explored new things that turned out well in the end. As Gesprek exemplifies, Kotlin and Kotlin coroutines are a force to be reckoned with: They are a refreshing take on brevity, open source, and interoperability. We’ll continue working on more projects so stay tuned for more of our explorations into Kotlin!
1 comment