Kotlin: combat experience

    Alexander Karjagin, a member of the ISDEF Association , leads marketing at Devexperts. We gave him and his colleagues a platform to talk about fairly recent development experience. We are still Independent Developers :)

    Recently, the growth of interest in the Kotlin programming language is approximately the same as the growth of the Bitcoin exchange rate. The increased attention is also due to the fact that in May 2017 Kotlin was declared the official development language for Android. Of course, we could not help but join in the study of this topic, and decided to experiment with Kotlin, using it in one of the new Android projects.

    Kotlin is a statically typed programming language that runs on top of the JVM and is developed by JetBrains. Kotlin combines the principles of an object-oriented and functional programming language. According to the developers, it possesses such qualities as pragmatism, conciseness and interoperability (pragmatic, concise, interoperable). Programs written on it can be run on the JVM or compiled in JavaScript, native compilation support is just around the corner. It is important to note that the language was created simultaneously with the development tools and was originally sharpened for them.

    To date, many articles have been devoted to Kotlin and many reports have been made. We will try to focus not so much on the distinguishing features of the language, to praise or scold it, but rather on our practical experience in using the declared advantages.

    So, first things first ...

    Best tooling


    The developer of the Kotlin language is JetBrains, which has developed perhaps the best IDE for Java and many other programming languages. Despite the verbosity of the Java language, the writing speed remains very high: the environment “writes code” for you.

    With Kotlin, you get the feeling that you bought a new keyboard and still you can’t get used to it and you can’t type blindly. IntelliSense often just does not keep up with the speed of typing, where for the Java IDE generates a whole class, for Kotlin you will look at the progress bar. And the problem is not only for new files: when actively navigating the project, the IDE just starts to freeze and it saves only by restarting it.

    It is disappointing that many of the tricks you are used to just stop working. For example, Live Templates. Android Studio - (A version of IntelliJ IDEA sharpened for Android development) comes with a set of convenient templates for frequently used operations, such as logging. The combination of logm + Tab will insert code for you, which will write a message to the log about which method and with what parameters was called:

    Log.d(TAG, "method() called with: param = [" + param + "]");

    Moreover, this template “knows how” to correctly determine the method and parameters, depending on where you applied it.

    However, this does not work for Kotlin, moreover, you will have to create a separate template (for example klogd + Tab) for Kotlin and use it depending on the programming language. The reason why for languages ​​that are 100% compatible with the IDE has to be tweaked twice remains a mystery to us.

    Easy to learn


    Kotlin, despite the possibility of compilation in JavaScript and potentially in native code (using Kotlin.Native), is primarily a language for the JVM and aims to save Java developers from unnecessary, potentially dangerous (in the sense of introducing bugs) boilerplate. However, it is a mistake to assume that you will write in Kotlin from the first lines in Kotlin. If you draw an analogy with languages, then at first you will write in a “ruglish” with a strong Java accent. This effect is confirmed by the review of your code, after some time, as well as by observing the code of colleagues who are just starting to learn the language. This is most noticeable in working with null and nonNull types, as well as excessive “verbosity” of expressions - a habit that is most difficult to deal with. Moreover, the presence of just a huge number of new features like extension methods opens the Pandora’s Box for writing black magic, adding extra complexity where it is not needed, and also making the code more confusing, i.e. less fit for review. What is the overload of the invoke () method [more details ], which allows you to disguise its call as a constructor call so that visually creating an object of type Dog you get anything:

    class Dog private constructor() {
      companion object {
          operator fun invoke(): String = "MAGIC"
      }
    }
    object DogPrinter {
      @JvmStatic
      fun main(args: Array) {
          println(Dog()) // MAGIC
      }

    Thus, in spite of the fact that it takes no more than a week to master the syntax, it may take more than one month to learn how to correctly apply the language features. In some places, a more detailed study of the working principles of a particular syntactic sugar will be required, including the study of the byte code received. When using Java, you can always refer to sources like Effective Java in order to avoid many troubles. Despite the fact that Kotlin was designed with the “troubles” introduced by Java, the “troubles” introduced by Kotlin have yet to be learned.

    Null safety


    The Kotlin language has an elegant type system. It allows in most cases to avoid the most popular problem in Java - NullPointerException. Each type has two options, depending on whether a variable of this type can be null. If the variable can be set to null, a question mark is added to the type. Example:

    val nullable: String? = null
    val notNull: String = ""

    Methods of a nullable variable are called using a statement.? If such a method is called on a variable that is null, the result of the entire expression will also be null, while the method will not be called and a NullPointerException will not occur. Of course, the language developers left a way to call the method on a nullable variable, no matter what, and get a NullPointerException. For this instead? have to write !!:

    nullable!!.subSequence(start, end)

    This line immediately cuts the eye and makes the code less comfortable. Two consecutive exclamation marks increase the likelihood that such code will be written exclusively consciously. However, it is difficult to think of a situation in which you would need to use the operator !! ..

    Everything looks good as long as all the code is written in Kotlin. If Kotlin is used in an existing Java project, everything becomes much more complicated. The compiler cannot track in which variables null will come to us, and, accordingly, correctly determine the type. For variables that come from Java, there are no null checks at the time of compilation. Responsibility for choosing the right type rests with the developer. In this case, in order for automatic conversion from Java to Kotlin to work correctly, @ Nullable / @ Nonnull annotations must be affixed in the Java code. A complete list of supported annotations can be found here .

    If null from Java code got into Kotlin, a crash will occur with the exception of the following:

    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 find the place where the exception was thrown from: The fact is that for all parameters of non-private methods the compiler adds a special check: the standard library method is called

    ALOAD 1
    LDC "param"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull(Ljava/lang/Object;Ljava/lang/String;)V



    kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull(param, "param")

    If desired, you can disable it using the compiler directive.
    -Xno-param-assertions

    Use this directive only as a last resort, because it gives only a slight increase in performance to the detriment of the likely loss of reliability.

    For all classes that have a get () method in Kotlin, the [] operator can be applied. It is very comfortable. For instance:

    val str = “my string”
    val ch = str[2]

    However, the index access operator can only be used for non-null types. A nullable version does not exist, and in this case you will have to explicitly call the get () method:

    var str: String? = null
    val ch = str?.get(2)

    Properties


    Kotlin makes it easy to work with class fields. The fields can be accessed as usual variables, and the getter or setter of the required field will be called up.

    // 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 want to override the getter of a Java class in a class in Kotlin. At first glance it seems that indicationHelper is a full-fledged property compatible with Kotlin. This is actually not the case. If we try to redefine it “forehead”, we get a compilation error:

    class GlobalAccountInfoViewController(context: Context) :  IndicationViewController(context) {
        protected open val indicationHelper = IndicationHelper(context, this)
    }



    Everything was done correctly: a property was declared in the successor class, the getter of which has an absolutely identical signature to the getter of the superclass. What is wrong? The compiler takes care of us and believes that the redefinition occurred by mistake. There is even a discussion on this topic on the Kotlin forum . From here we learn two important things:
    1. “Java getters are not seen as property accessors from Kotlin” - Getters in Java code are not visible from Kotlin as a property.
    2. “This may be enhanced in the future, though” - there is a hope that in the future this will change for the better.

    It also gives the only correct way to achieve our goal: create a private variable and at the same time redefine the getter.

    class GlobalAccountInfoViewController(context: Context) : IndicationViewController(context) {
        private val indicationHelper = IndicationHelper(context, this)
        override fun getIndicationHelper() = indicationHelper
    }

    100% Java-interop


    Perhaps it was worth putting this item in first place, because it was Java-interop that allowed the new language to gain such popularity so quickly that even Google announced official support for the language for Android development. Unfortunately, there were some surprises here as well.

    Consider such a simple and well-known thing to all Java developers as access modifiers or visibility modifiers. There are four things in Java: public, private, protected, and package-private. Package-private is used by default unless you specify otherwise. Kotlin uses the public modifier by default, and it, like protected and private, is called and works just like in Java. But the package-private modifier in Kotlin is called internal and it works a little differently.

    The language designers wanted to solve the problem with the potential to break encapsulation when using the package-private modifier by creating a package with the same name in the client code as in the library code and predefining the desired method. Such a trick is often used when writing unit tests so as not to open the “out” method only for testing needs. This is how the internal modifier appeared, which makes the object visible inside the module.

    The module is called:
    • Module in IntelliJ Idea Project
    • Project at Maven
    • Source set in Gradle
    • A set of sources compiled by a single run of an ant-script

    The problem is that in fact internal is public final. Thus, when compiling at the byte level of the code, it may happen that you accidentally override a method that you did not want to override. Because of this, the compiler will rename your method so that this 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 an internal modifier and add it as a dependency to your Java project, in which case you can call this method where the protected modifier would not allow you to do this, i.e. get access to the private API outside the package ( because the method is de facto public), although you cannot override it. One gets the impression that the internal modifier was not conceived as part of the “Pragmatic Language”, but rather as an IDE feature. Despite the fact that such a behavior could be done, for example, using annotations. Against the backdrop of claims that very few keywords are reserved in Kotlin, for example, for Coroutines, internal actually nails your Kotlin project to the JetBrains IDE. If you are developing a complex project consisting of a large number of modules, some of which can be used as dependency by colleagues,

    Data classes


    The next, perhaps one of the most famous features of the language is data classes. Data classes allow you to quickly and simply write POJO objects, equals, hashCode, toString and other methods for which the compiler will write for you.

    This is really a convenient thing, however, traps can wait for you in compatibility with the libraries used in the project. In one of our projects, we used Jackson to serialize / deserialize JSON. At that moment, when we decided to rewrite some POJOs on Kotlin, it turned out that Jackson annotations did not work correctly with Kotlin and it was necessary to additionally connect a separate jackson-module-kotlin module for compatibility.

    In conclusion


    Summing up, I would like to say that despite the fact that the article may seem to you criticizing Kotlin, we like it! Especially on Android, where Java is stuck on version 1.6 - this was a real salvation. We understand that Kotlin.Native, coroutines and other new features of the language are very important and correct things, however, they are not useful to everyone. While support for the IDE is something every developer uses, and the slow work of the IDE eliminates all the speed benefits from switching from verbose Java to Kotlin. Whether to switch to the new Kotlin or stay in Java for now is the choice of each individual team, we just wanted to share the problems that we had to face, in the hope that this could save someone time.

    Authors:
    Timur Valeev, Software EngineerDevexperts
    Alexander Vereshchagin, Software Engineer Devexperts

    Also popular now: