Kotlin From The Trenches
In the last few years, the growth of hype around the Kotlin programming language has been about the same as that of the Bitcoin rate. This close attention was gingered up even more in May 2017, when Google declared official support of Kotlin for the development for Android. Of course, we couldn’t help joining the study of this topic and decided to experiment with Kotlin, using it in our new Android-based project.
Kotlin is a statically typed programming language that runs on top of the JVM; it is developed by JetBrains. Kotlin combines the principles of an object-oriented and functional programming language. According to the developers, it is pragmatic, concise, and interoperable. Applications written in it can be run on the JVM or compiled in JavaScript, and the native compilation support is around the corner. It is important to note that the language was created simultaneously with the development tools and fine-tuned to be used with them.
By now, many articles have been dedicated to Kotlin and many reports on it have been made. We’ll try to focus not so much on the distinctive features of the language, praising it or scolding it, as on our practical experience of benefiting from its features.
So, let’s contemplate each of the mentioned aspects…
The best tooling
The developer of the Kotlin language is JetBrains, a software development company that has produced probably the best IDE for Java and many other programming languages. Despite all the verbosity of the Java language, the writing speed remains very high: the environment “writes the code” for you.
With Kotlin, there is a feeling that you have purchased a new keyboard and still cannot get used to it and you aren’t able to touch-type despite trying. IntelliSense often just doesn’t keep up with the speed of typing; where IDE will generate a whole class for Java, for Kotlin you are going to be looking at the progress bar. And the problem is not only with new files: navigating actively through the project may result in IDE freezing so only restarting it will help you.
It’s upsetting that many of the tricks you’re used to just stop working. For example, Live Templates. Android Studio – (a version of IntelliJ IDEA for Android development) – comes with a set of convenient templates for frequently used operations, such as logging. The logm + Tab combination will insert a code that will record a message to the log about which method with which parameters has been called:
Log.d(TAG, 'method() called with: param = [' + param + = ']');
In doing so, this template “knows how” to correctly determine the method and parameters, depending on where you applied it.
However, it does not work in Kotlin. Moreover, you’ll have to create a separate template (for example, klogd + Tab) and use it based on the programming language. The reason why the 100% IDE-compatible languages require managing settings twice, remains a mystery to us.
Is it easy to learn?
Kotlin, despite the possibility of compiling into JavaScript and potentially into the native code (using Kotlin.Native), is primarily a language for JVM and its purpose is to spare Java developers the unnecessary and potentially dangerous (prone to bugs) boilerplate. However, it is a mistake to assume that writing in Kotlin from the very start will actually be Kotlin at all. To draw an analogy with languages, at first you’ll be writing in “Spanglish” with a strong Java accent. We saw this when reviewing our own code after some time, as well as when observing the code of colleagues who were just beginning to learn the language. This manifested the most in working with null and nonNull types, as well as in the excessive “verbosity” of expressions, a habit that’s hard to kick. In addition, the huge number of new features, e.g., extension methods open Pandora’s Box for writing black magic spells, adding excessive complexity where it wasn’t called for, and making the code more confusing and hard to review. A telling example is the invoke () method overloading, which allows masking its call under a constructor call so that visually creating a Dog type object, you can get pretty much anything:
class Dog private constructor() {
companion object {
operator fun invoke(): String = 'MAGIC'
}
}
object DogPrinter {
@JvmStatic
fun main(args: ArrayString) {
println(Dog()) // MAGIC
}
}
Thus, despite it won’t take you more than a week to master the syntax, it may take several months to learn to correctly apply the features of the language. In some cases, a more detailed study of the principles of the operation of certain syntactic sugar, including the study of the obtained bytecode, will be necessary. When using Java, you can always refer to such sources as Effective Java to avoid many troubles. Even though Kotlin was designed to eliminate “troubles” brought by Java, “troubles” brought by Kotlin are yet to be revealed.
Null safety
The Kotlin language has a sophisticated type system. It allows you, in most cases, to avoid the most popular problem in Java – NullPointerException. Each type has two options, based on whether a variable of this type can be null. If you can assign null to a variable, the question mark is added to the type. Example:
val nullable: String? = null
val notNull: String = ''
Nullable variable methods are called using the .? operator. If such method is called on a variable that is null, the result of the entire expression will also be null; however, the method will not be called and NullPointerException will not occur. Of course, the developers of the language left a way to call the method on a nullable variable, no matter what, and get a NullPointerException. To do this, instead of ? you will have to write !!:
nullable!!.subSequence(start, end)
This line is jarring and the code loses its neatness. Two consecutive exclamation are likely to indicate that such a code was deliberately written. On the other hand, it is difficult to think of a situation where using the !! operator would be necessary.
Everything looks good until the entire code is written in Kotlin. If, however, Kotlin is used in an existing Java-based project, everything becomes much more complicated. The compiler is unable to track which variables will be null, so to determine the type correctly is next to impossible. For variables from Java, null tests are not available at compile time. The developer will thus take the responsibility of choosing the right type. Besides, in order for the automatic conversion from Java to Kotlin to work correctly, the code in Java must contain @Nullable/@Nonnull annotations.
If, however, a null finds its way from the Java code into Kotlin, a crash with the exception of the following kind is inevitable:
FATAL EXCEPTION: main
Process: com.devexperts.dxmobile.global, PID: 16773
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.devexperts.dxmobile.global/com.devexperts.dxmarket.client.ui.generic.activity.GlbSideNavigationActivity}: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter savedInstanceState
Having disassembled the bytecode, we can find the spot from which the exception was thrown:
ALOAD 1
LDC "param"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull(Ljava/lang/Object;Ljava/lang/String;)V
The deal is that for all parameters of non-private methods, the compiler adds a special check: the standard library method is called
kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull(param, "param")
If necessary, you can disable it by using the compiler directive
-Xno-param-assertions
This directive should be reserved for extreme cases as it only gives a slight increase in performance at the expense of the likely loss of reliability.
To all classes that have the get () method, you can use the [] operator in Kotlin. It is very convenient. For example:
val str = 'my string'
val ch = str[2]
However, the index access operator can only be used with non-null types. A nullable version does not exist, and in a case like that you will have to explicitly call the get () method:
var str: String? = null
val ch = str?.get(2)
Properties
Kotlin makes it easier to work with class fields. You can access fields as normal variables, and a getter or a setter of the desired field will be called.
// Java code
public class IndicationViewController extends ViewController {
private IndicationHelper indicationHelper;
protected IndicationHelper getIndicationHelper() {
return indicationHelper;
}
}
// Kotlin code
val indicationViewController = IndicationViewController()
val indicationHelper = indicationViewController.indicationHelper
Things get complicated if you need to override a Java class getter in a Kotlin class. At the first glance, it seems that indicationHelper is a full-fledged property compatible with Kotlin. As a matter of fact, this is not so. If we try overriding it “head-on”, we’ll most definitely get a compilation error:
class GlobalAccountInfoViewController(context: Context) : IndicationViewController(context) {
protected open val indicationHelper = IndicationHelper(context, this)
}
All is done correctly: declared in the subclass is a property whose getter has a signature absolutely identical to the superclass getter. What is wrong then? The compiler takes care of us, suggesting that the overriding happened by mistake. There is even a discussion on this subject in the Kotlin forum. There we can learn two important things:
- “Java getters are not seen as property accessors from Kotlin”
- “This may be enhanced in the future, though”
And it seems that there’s only one correct way to achieve our goal (also from the forum): to create a private variable and at the same time override the getter.
class GlobalAccountInfoViewController(context: Context) : IndicationViewController(context) {
private val indicationHelper = IndicationHelper(context, this)
override fun getIndicationHelper() = indicationHelper
}
100% Java-interop
Perhaps we should have placed this paragraph first, because it was Java-interop that allowed the new language to gain such popularity so quickly that even Google declared official support of the language for the development for Android. Unfortunately, we did not avoid surprises here either.
Let’s consider a simple thing known to all Java developers: access modifiers or visibility modifiers. In Java, there are four of them: public, private, protected, and package-private. Package-private is used by default, unless you specify otherwise. In Kotlin, the modifier used by default is public, and it (like both the protected and the private) is called and works exactly as in Java. However, the package-private modifier in Kotlin is called “internal” and it works somewhat differently.
The language designers wanted to address the problem with the potential ability to disrupt encapsulation when applying the package-private modifier. The solution was to create a package in the client code with the same name as in both the library code and the method pre-defining. Such trick is often used when writing unit-tests in order not to open “to the outside” the method created for testing purposes. This is how the internal modifier appeared, which makes the object visible inside the module.
The name module applies to the following:
- Module in the IntelliJ Idea project
- Project in Maven
- Source set in Gradle
- A set of source codes compiled with a single ant-script launch
The problem is that internal is actually public final. Thus, when compiling at the bytecode level, you can accidentally override a method that you did not want to override. Because of this, the compiler will rename your method so that such a thing does not happen, which in turn will make it impossible to call this method from Java code. Even if the file with this code is in the same module, in the same package.
class SomeClass {
internal fun someMethod() {
println('')
}
}
public final someMethod$production_sources_for_module_test()V
You can compile your Kotlin code with the internal modifier and add it as a dependency to your Java project. In this case, you can call this method where the protected modifier would not allow you to do so, i.e., you will get access to the private API outside of the package (because the method is de facto public), although you will not be able to override. One may get the feeling that the internal modifier was not designed as part of the “Pragmatic Language”, but rather as a feature of the IDE. And such behavior could have been made by using annotations, for exampleю despite many statements that very few keywords are reserved in Kotlin, for example, for coroutines, internal actually chains your Kotlin-based project to the JetBrains IDE. If you are developing a complex project that consists of a large number of modules and some of these modules may be used as dependencies by colleagues in a pure Java-based project, think carefully before writing common parts in Kotlin.
Data Classes
The next, probably one of the most famous features of the language, is data classes. Data classes allow you to quickly and easily write POJO-objects, equals, hashCode, toString and other methods for which the compiler will write for you.
This is really a handy thing, however, beware of traps in compatibility with libraries used in the project. In one of our projects, we used Jackson to serialize/deserialize JSON. When we decided to rewrite some POJOs in Kotlin, it turned out that the Jackson annotations did not work correctly with Kotlin and it was necessary to additionally connect a separate jackson-module-kotlin for compatibility.
In conclusion
Even though this article may come off as criticizing, we do like Kotlin! Especially on Android, where Java got stuck on version 1.6, it became a true salvation. We understand that Kotlin.Native, coroutines and other new features of the language are very important and correct things, but they just may not be useful to everyone. At the same time, the IDE support is what each developer uses, and the slow work of the IDE negates all the speed benefits we get switching to Kotlin from the verbose Java. While the decision to switch to Kotlin or to stay on Java for the time being, is to be taken by each individual team, all we want is to share the problems that we had to face, which will hopefully save you precious time.