Kotlin: Pitfalls, Tips & Tricks

A list of what to use and what not to use when working with Kotlin
24.08.2023
Tags

Introduction

In the realm of modern software development, the battle between Java and Kotlin continues to captivate programmers. We at kreuzwerker have written a blog post documenting the learning curve with Kotlin (coming from a Java based development background), and have had the chance to speak about it at a webinar dedicated for the same. The seasoned consultants that we are, we have been documenting this journey and decided to bring forth some more tips, tricks and pitfalls, which may help out fellow developers on their journeys to transition from Java to Kotlin. In this article we will dive into the Kotlin-versus-Java saga and unveil some insights into navigating this exciting shift.

Pitfalls

Compatibility with Jackson

Jackson is a popular JSON serialization/deserialization library used in Spring. Kotlin data classes, which are commonly used for modeling data in Kotlin, may have different default behavior with Jackson serialization compared to Java classes without the usage of the module 'jackson-module-kotlin’. For example, consider the following Kotlin data class:

data class MyData(val id: Int, val name: String)

By default, Kotlin Data classes generate equals, hashCode, and toString methods that consider all properties, including val properties. However, Jackson uses these generated methods during serialization, which may result in unexpected behavior. To resolve this issue, you can use Jackson’s annotations, such as @JsonIgnore or @JsonProperty, to customize the serialization behavior of Kotlin data classes:

data class MyData(
@JsonProperty("id") val id: Int,
@JsonProperty("name") val name: String
)

Static Methods in Kotlin

In Kotlin, static methods and fields are defined inside a companion object, which is similar to a Java static inner class. In the example below, we may expect that the Kotlin compiler will compile the ‘isThisStatic’ method defined inside a companion object as a static method under the hood, but this is not the case. The companion object is formed as a static inner class, but the methods inside are simple instance methods and not static themselves. This may cause issues when using a mixed Java and Kotlin codebase as method calls which are expected to be static won’t compile.

// Kotlin and Java
class MyClass {
    companion object {
        fun isThisStatic() { ... }
        val staticField = ...
    }
}

// Won't compile
MyClass.isThisStatic()

Kotlin provides the ‘@JvmStatic’ annotation to expose the methods and fields in the companion object as static methods and fields in Java. The ‘@JvmStatic’ annotation can only be applied to methods and not to properties. If you want to expose a property as a static field in Java, you need to define a getter method in the companion object. So the following will now compile:

// Kotlin and Java
class MyClass {
    companion object {
		@JvmStatic
        fun isThisStatic() { ... }
        val staticField = ...
    }
}

// Good to go
MyClass.isThisStatic()

IDE Converters

Use of IDE converters to convert Java code into Kotlin always needs double checks. The following snippet is an example from IntelliJ:

// Java
class Foobar{
    private final Foo foo = new Foo();;
    void doFoo(){
        if (foo != null)
            foo.bar()
    }
}

// Kotlin: Converted by IDE
class Foobar{
    private var foo: Foo? = null;
    fun doFoo(){
        if (foo != null)
        	foo!!.bar()
    }
}

// Kotlin: What would be ideal
class Foobar{
	private val foo: Foo;
    fun doFoo(){
		foo.bar()
	} 
}

Mocking Libraries

Classes and methods in Kotlin are final by default, and any one using Mocks during testing needs to know that Mockito used to have problems with that. Mockito was primarily built for Java testing and until recently you couldn’t use it for Kotlin fully as it would expect you to write your classes and methods as ‘open’. This has recently changed with the new feature introduced by Mockito, but for those using older versions when working with Kotlin, you would need to either use another mocking library OR skip mocking at all.

open class B {
  open fun provideValue() = "b"
}

In case you want to avoid this, we recommend using Mockk. It provides a pure Kotlin-mocking DSL for writing similar tests and is well suited as it provides better support for Kotlin features along with mocking capabilities for final classes and methods.

Null Safety for Platform Types

Despite boosting in-built null safety, Kotlin still is unable to identify null-related issues when you have a mixed code base of Java and Kotlin. For example, the following code will not throw any compile time issues in Kotlin:

fun main(){
	val aValue: Int = AJavaClass.generatedNullValue()
}

You would need to handle this as you would in Java:

fun main(){
	val aValue: Int = AJavaClass.generatedNullValue() ?: -1
 	val bValue: Int? = AJavaClass.generatedNullValue() 
}

Tips and Tricks

Smart Casting

Smart Casting in Kotlin allows you to NOT specify the type of a variable before accessing the property itself. (No more sonar lint warnings when doing type conversions)

// Java
Object givenObj = "Something";
if(givenObj instanceof String) {
    // Explicit type casting
    String str = (String) givenObj;
    System.out.println("length of String " + str.length());
}

// Kotlin
val givenObject: Any = "Something"
if(givenObject is String) {
	// Works automatically
	givenObject.substring(...)
}

Elvis Operator is your friend

The Elvis operator (?) provides a way to write more intuitive and stream based code which is more focused on the task at hand and aids readability.

// Java
val image = fetchById(1)
if(image != null) {
	...
} else {
	throw NotFoundException("Image doesn't exist")
}

// Kotlin
val image = fetchById(1)
image?.let{
	...
} ?: throw NotFoundException("Image doesn't exist")

Function Types and Interfaces

In Kotlin, functions are first-class citizens, which means they can be treated like any other data type, such as integers, strings, or objects. A function type represents the signature of a function, including its parameter types and return type. This allows you to pass functions as arguments to other functions, return them from functions, and store them in variables or data structures. This functional programming approach allows for more flexible and expressive code, making it easier to work with complex logic and operations.

// Function Types
val sayHello = {"Hello from Tom and Gopal"} // corresponds to function signature:  () -> String
println(sayHello())

println({Cheers}()) // Anonymous Function
println{Cheers}() // You can skip brackets in last argument

// Functional Interfaces
callFoo(object: Foo {
 override bar(s: String){
	println(s)
 }
})

callFoo{print(it)}

KTor

KTor is a library built from the ground up using Kotlin and co-routines that is very easy to utilize for building asynchronous client server applications. Getting a fully functional lightweight and flexible micro service up and running can be as simple as:

fun main() {
	embeddedServer(Netty, port = 8000) {
		routing {
			get ("/") {
				call.respondText("Hello, world!")
			}
		}
	}.start(wait = true)
}

Summary

“In the grand tapestry of programming languages, Kotlin stands as a vibrant thread reshaping the fabric of modern development.” - ChatGPT

In this post, we presented the wisdom gained from using Kotlin in live production grade applications. By embracing them, developers can harness Kotlin’s advantage over Java. So what’s next?

We would suggest first time users go through the official Kotlin documentation to familiarize themselves with the language and continue pushing to learn more.

If you found this article useful, do share it with your friends. If you have more tips, tricks and gotcha’s, please let us know. We’d be happy to learn more about it. If you want to hear more from us and about Kotlin, check out the recording of our webinar “Modernize Your Java Applications with Kotlin: Best Practices and Strategies”

Thanks for reading!