Effective migration to Kotlin on Android
--
This article was featured in Android Weekly, Issue #335 and Kotlin Weekly, Issue #120.
There are countless resources available to help you jumpstart on Kotlin: official documentation from Google and JetBrains, Google-IO talks, codelabs, and an endless supply of blogpost and tutorial provided by the community. All these resources can introduce you to Kotlin’s type system, null-safety, extension functions, data classes, high order functions and all the other cool features that Kotlin provides.
However, when you start applying all this to a real Android codebase, not everything turns out to be as simple as it appears. Although Kotlin’s syntax is easy to pick up for a Java developer, it takes some time and practice in order to be able to write idiomatic code and effectively translate some of the widely-used patterns on Android development to Kotlin. Furthermore, many libraries are made redundant or at least require some adaptations to be made on how they are used, due to the features that Kotlin offers out of the box.
This article presents a collection of practical advice and tricks to help you out in the transition to Kotlin.
Custom view constructors
When creating a Custom View in Java, we have to override the 4 constructors that the View
class defines. This is quite a bit of boilerplate.
One of the first things we learn about defining classes in Kotlin is that we can use the implicit constructor that is part of the class header and it also allows us to define the class’s fields in one line.
We then learn that it’s possible to define secondary constructors, using the constructor
keyword. With secondary constructors, MyCustomView
would look like this: (this is actually very similar to the code that Android Studio’s automatic conversion to Kotlin generates).
While this is valid Kotlin syntax, we can do better than that and reduce the boilerplate. By using the @JvmOverloads
annotation, we can just define one constructor with the full argument list and provide default values for the optional ones. The annotation instructs the Kotlin compiler to generate the overloaded constructors.
Static factory methods for Fragments and Activities
One very popular pattern on Android is the use of static factory method for creating Fragments and Activities (actually in the case of Activities, the factory methods generate the Intent that is used to launch the Activity). Since Activities and Fragments are created by the Android framework, which means that we can’t pass arguments to the constructors of these classes, with the use of factory methods, we can encapsulate the creation code inside the actual class that we are instantiating, so that on the creation site we don’t have to worry about the extras names or other details related to the creation of the Fragment or Activity.
Since Kotlin doesn’t have static methods, converting this code requires the use of the Activity’s companion object
.
There are articles that describe ways to try and make this more generic and extract it into a base activity, but I don’t think its worth the fuss.
Note that we can improve the code above by writing it in a more idiomatic way. First we can use the apply
method, that allows us to chain a constructor call with method calls on the newly-created object. Then we can also use the bundleOf
method of android-ktx
library to simplify the creation of the extras.
Dagger
Dagger is an annotation library written for Java, that generates Java code. Its API and usage patterns contain some idioms that do not translate well to Kotlin. Migrating dagger code to Kotlin sometimes requires extra effort, and actually the resulting code seems a bit unnatural. Here’s a small collection of gotchas and tricks that we are forced to follow when dealing with Dagger in Kotlin.
- Field injection
The first struggle when introducing Dagger in a Kotlin project is how to define injected fields, complying with Kotlin’s nullability system. The solution is to define the field with lateinit var
. So this (in Java):
becomes:
- Injecting named fields
Injecting a named field is quite easy in Java, you just annotate the field and the provision (@Provides
or @Binds
) method and you are done.
In Kotlin simply annotating the field like this will not work. You need to set the correct use-site target for the annotation:
If the injected type is a primitive, an extra step is required: you also need to add the @JvmField
annotation to instruct the Kotlin compiler to create a public field, so that Dagger can inject it.
Note that since we cannot use lateinit
with primitives, we have to initialize the field with a default value.
- Static
@Provides
methods
It is a recommended practice to use static @Provides
methods whenever possible. But in Kotlin static methods do not exist. The replacement for static methods is to define a method in the class’s companion object, but in the case of a @Provides
method, just doing so isn’t enough.
As shown in the complete solution below, we also need to add the @JvmStatic
annotation to the method, in order to indicate to the compiler that it has to generate a static method that dagger can locate. Apart from that, we also need to annotate not only the module, but also the companion object with @Module
.
Butterknife
Butterknife is a very convenient and popular library that was used in the Java days to reduce the boilerplate required to get references to the Views from an inflated layout.
The Android extensions plugin leaves us with no good reason to use Butterknife any more on Kotlin. The plugin generates extension properties for Activities, Fragments and other targets (including custom targets — check the documentation), and Android Studio directly imports them, so you can seamlessly access the Views by just using their ids.
With the use of the plugin, this code:
can be simplified to this:
Note that in order to be able to use the feature, you need to add the kotlin-android-extension
plugin to your module’s build.gradle
file, and for certain features, you also need to enable the experimental
flag.
AutoValue
AutoValue is very useful for defining value-classes in Java: it helps eliminate all the boilerplate related to overriding the equals
, hashCode
and toString
methods.
However, Kotlin’s out-of-the-box support for data classes renders it redundant. And it does so by also offering a lower maintenance cost: when adding/removing a field to an AutoValue class, you have to manually update the factory method or the Builder, and although the IDE can help you with that it’s still an extra step that you need to follow. In Kotlin you just add the property in the constructor, and the compiler takes care of the rest.
Note that AutoValue builders are also made obsolete with Kotlin, as they can be replaced by constructors with optional and named arguments.
Parcelable
Another source of boilerplate for Android is implementing the Parcelable
interface. In Java-land there are numerous options available to help you out with the generation of this boilerplate, from Android Studio plugins, to AutoValue extensions and various libraries. There are actually so many options available, that even articles that compare their advantages and disadvantages are available.
In Kotlin, the situation is simpler: the Kotlin android extension offers a zero-maintenance and low-overhead solution. You can just annotate your class with @Parcelize
and the Kotlin compiler will generate the Parcelable implementation.
Note that for the Parcelize extension to work, you need to set the experimental
flag in your gradle file as explained above (in the Butterknife section).
Testing/Mockito
In Kotlin all classes are final/closed
to inheritance by default, and this causes problems with Mockito, which creates mocks by subclassing the class to be mocked. There are three main solutions for this:
- Use interfaces, and define your mocks to be of the interface type — then Mockito can create the mocks without problems. Extracting an interface for all classes, though, might be a bit of an overkill for certain projects.
- Mockito does offers a mechanism for allowing mocks of final classes. In order to enable it you just need to create the file
src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
containing the single line:mock-maker-inline
Note that this comes with a performance penalty of up to 3x according to the official documentation. - Finally, the kotlin compiler offers a plugin that treats all classes that are marked with a specific annotation as open by default. To enable it, you just need to add this to your gradle file
apply plugin: "kotlin-allopen"
and then specify the annotation that you will use to mark the classes that should be treated as open.
So, now that we’ve covered how we can empower mockito with the ability to create mocks, we will go through how we can actually create and use them in code. In Java, the standard Mockito usage would look like this:
If we want to translate directly this to Kotlin, we have two options:
- We can define the
collaborator
variable as nullable (val collaborator: Dependency?
) so that it can be assigned byMockitoAnnotations.initMocks
. Then we would have to always reference it with?.
or!!
- Or we could alternatively define it as
lateinit var collaborator: Collaborator
which is not perfect either.
The mockito-kotlin library offers a shorthand to create mocks, that at the same time doesn’t require to call initMocks
, so we can initialize our mocks in just one line:
Another annoying thing when starting writing tests in Kotlin is that since when
is a keyword, it causes a conflict with the method Mockito.when
which is necessary for setting up our mocks. You have to use backticks to signal the compiler that it’s the method that you want to use and not the keyword.
The mockito-kotlin library offers an alias for the Mockito.when
method, called whenever
to avoid the name clash.
Conclusions
Kotlin’s features and idiomatic syntax can help us remove large amounts of boilerplate. At the same time, these powerful feature have made some of the libraries that belonged until now to the standard toolset of android developers, look almost redundant nowadays. Other libraries (like Dagger) seem a bit awkward and require extra effort when used with Kotlin: these libraries will either have to provide more Kotlin-friendly APIs, or they will be replaced by new Kotlin-first libraries that have started popping up.