Kotlin - a pragmatic multiplatform language
In 2019, Google announced Jetbrain’s Kotlin as the first-class language for Android development. Since then, Kotlin has evolved to be more than just a language for Android. On kotlinlang.org, it claims to be safe, concise, expressive and cross-platform. In this blog, I cherry-pick some useful features (with practical examples) that I really appreciate when working with Kotlin.
Table of Contents
Handling Null Safety
NPE is one of the most frequently encountered bugs in JVM world. Since Java 8, Optional
was
introduced to deal with null reference, but Kotlin introduces a more concise approach. The interface
below has a method that can return an optional result.
interface UserRepository : AutoCloseable {
...
fun get(userId: String): User?
...
}
Any implementation of the interface must guarantee that userId
is not null. The syntax User?
means
that the function can return null or non-nullable object. A sample implementation is:
class InMemoryUserRepository : UserRepository {
private val userIdToUser: MutableMap<String, User> = mutableMapOf()
override fun get(userId: String): User? {
return userIdToUser[userId]
}
}
What the client code looks like in this case? You must handle the possible nullable case otherwise the code
won’t compile. You can also use safe call operator ?.
for chaining nested calls and Elvis operator ?:
to simplify null check statement.
fun onFollow(follower: User, otherUserId: String): FollowStatus {
return userRepository.get(otherUserId)?.let { userRepository.follow(follower, it) } ?: FollowStatus.INVALID_USER
}
More on Null-safety at https://kotlinlang.org/docs/reference/null-safety.html
Separation of Immutability and Mutability
Mutating objects in runtime is error-prone. Applying Immutability in programming can improve performance, stability
and predictability of software, it is especially important when working with functional / reactive programs. Fortunately, Kotlin
clearly separates mutable objects and immutable ones using val
, var
keywords for variable declarations. This class has both
mutable and immutable properties.
class BusinessRuleEngine(val facts: Facts) {
var actions: MutableList<Action> = mutableListOf()
fun addAction(action: Action) {
actions.add(action)
}
fun replaceActions(actions: MutableList<Action>) {
this.actions = actions
}
fun run() {
actions.forEach { it.execute(facts) }
}
}
In the class above we create a custom rule engine with unmodifiable facts val facts: Facts
while declaring a mutable list of actions, allowing the engine to dynamically add new actions or even
replace the whole actions
list. In my option, it is good practice to keep using val
for all variables
until you actually need var
keyword.
No checked exception handling
Checked exceptions (exceptions that inherit Exception class in Java such as FileNotFoundException) require client code to handle all possible exceptional cases. Same as C# and Ruby, Kotlin compiler doesn’t enforce client code to catch checked exceptions, so that developers are responsible for handling them. This approach has both pros and cons and it depends on situations. Having too many specific checked exceptions leads to code cluttering and impacting developer’s productivity (imagine you write Reactive/Stream based codes and have to handle a method which throws multiple exceptions). However, ignoring exception is a bad practice.
Useful data class
In Java you can use Lombok
library to annotate a class as data class. Kotlin supports this ootb and
the compiler automatically creates equals
, hashcode
, toString
and copy
methods. Data class is
suitable for implementing Value Object or classes without behavior.
data class SummaryStatistics(
val sum: Double,
val max: Double,
val min: Double,
val average: Double
)
Built-in Singleton pattern
Instead of reimplementing Singleton pattern, Kotlin provides an easy way to create a singleton and guarantee it is the only instance available at runtime. For example, to group global constants and make it available elsewhere.
object DocumentTypes {
const val INVOICE_TYPE = "invoice"
const val LETTER_TYPE = "letter"
const val REPORT_TYPE = "report"
const val IMAGE_TYPE = "jpg"
}
The object DocumentTypes is lazily initialised at the first access and it is thread-safe.
Its constants can be accessed as DocumentTypes.INVOICE_TYPE
Rich and concise Collections library
There are many flavours of collections for Java such as JDK, Eclipse, Guava and Apache Collections. Kotlin standard library provides easy to use and a few more features than native Java’s Collections. Collection types such as List, Set, Map are provided with both mutable and read-only versions. Manipulation is done through various extension functions, for examples:
- Transformation: map, mapKeys, mapValues, zip, unzip, associate, flatten, flatMap, join
- Filtering: filter, filterNot, partition, predicates (any, none, all)
- Grouping: groupBy, fold and reduce, eachCount, aggregate
- Ordering: sorted, sortedBy, reversed, shuffled
- Aggregation: sum, min, max, count, average, fold and reduce
In the example below, we filter transactions from a bank statement and group by month then description.
class BankStatementProcessor(val trans: List<BankTransaction>) {
fun getTransactionsGroupByMonthAndDesc(): List<BankTransaction> {
return trans.groupBy { it.date.month }
.mapValues { it.value.groupBy { t -> t.description }.values.flatten() }
.values.flatten()
}
}
Coroutines
Generally, asynchronous programming in JVM is done through native threads or third party libraries like RxJava or Project Reactor. Kotlin offers a new way to write concurrent programs: Coroutines. Using a special data structure called Continuations, Coroutines is co-operative multitasking instead of pre-emptive multitasked like native threads. From runtime perspective, the program benefits from leak-free, lock-free and more efficient use of computing resources. With coding style, the coroutine code is sequential (no callback hell), less pervasive, easy to understand and refactor, hence reduce cognitive overhead for coders.
In order to write concurrent code in Kotlin, developers need to identify suspendable functions so the runtime can suspend and resume them in the middle of its executions. Examples of suspendable functions are long-running, I/O intensive ones. This design enables the runtime to schedule the execution of multiple suspend
functions concurrently. In the example below, the function getTotalByCountrySlug
is a blocking one.
class CovidStatusClient: CovidStatusApi {
private val statusUrl = "https://api.covid19api.com/total/country/"
override fun getTotalByCountrySlug(code: String): CountryCovidStatus? {
return Klaxon().parseArray<CountryCovidStatus>(URL(statusUrl + code).readText())?.lastOrNull()
}
}
This code below creates an instance of the client and invoke the blocking method asynchronously. Notice the new language elements
runBlocking {}
, async() {}
and await()
fun main() = runBlocking {
val format = "%-30s%-20s%-20s%-20s"
println(String.format(format, "Country", "Confirmed", "Deaths", "Active"))
val client = CovidStatusClient()
val countryCodes = listOf<String>("united-states", "russia", "australia", "singapore", "thai-")
val covidStatuses: List<Deferred<CountryCovidStatus?>> = countryCodes.map { code ->
async(Dispatchers.IO + SupervisorJob()) {
client.getTotalByCountrySlug(code)
}
}
for (status in covidStatuses) {
try {
val info = status.await()
println(String.format(format, info?.country, info?.confirmed, info?.deaths, info?.active))
} catch (ex: Exception) {
println("Error: ${ex.message?.substring(0..30)}")
}
}
}
runBlocking
block force the main() function to block and wait for asynchronous executions to finished.async()
function executes the method concurrently. To run in parallel,async
accepts an optional parameterDispatchers.IO
as CoroutineContext which basically tells the runtime to execute the code in a separate IO thread pool. Optional parameterSupervisorJob
has a special role to stop the cancellation of parent coroutine in case child coroutine failed to execute.await()
is called on Deferred objectDeferred<CountryCovidStatus?>
to eventually get the actual result
There are a lot more about Coroutines that is explained at https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html
Type-safe builders for creating DSL
Type-safe builder is what make Kotlin DSL compatible (DSL - Domain-specific language). Great examples are TornadoFX for UI apps and Ktor framework for writing server-client apps. TornadoFX adopts Kotlin DSL on top of JavaFX, result in very concise and expressive language to build UI layouts.
class AnalyzerView: View() {
override val root = borderpane {
top<MenuView>()
center<TabpaneView>()
}
}
class MenuView: View() {
override val root = menubar {
menu("File") {
item("New")
item("Save As")
item("Quit")
}
}
}
class TabpaneView: View() {
val controller: UIController by inject()
override val root = tabpane {
tab("Monthly Statistics") {
linechart("Monthly Statistics", CategoryAxis(), NumberAxis()) {
multiseries("Income", "Expense") {
controller.getMonthlySummary().forEach {
data(it.month, it.income, it.expense)
}
}
}
}
...
}
}
I only scratch the surface here, there are more in Kotlin that worth discovering. Kotlin ecosystem is expanding rapidly. Popular frameworks support the language (Spring, Gradle, Spark, Vert.x…) and many have been created from scratch with Kotlin (Ktor, RxKotlin, MockK…).
Executable source code can be found at: https://github.com/thanhnamit/rwsd-in-kotlin