Adopt Kotlin for large teams

Moving to any new language can be a daunting task. The recipe for success is to start slow, move in chunks, and test frequently to align your team for success. Kotlin makes migrating easy, as it compiles down to JVM bytecode and is fully interoperable with Java.

Building the team

The first step before migrating is to build a common baseline understanding for your team. Here are a few tips that you might find useful to accelerate your team's learning.

Form study groups

Study groups are an effective way to facilitate learning and retention. Studies suggest that reciting what you've learned in a group setting helps to reinforce the material. Get a Kotlin book or other study material for each member of the group, and ask the group to go through a couple of chapters each week. During each meet, the group should compare what they've learned and discuss any questions or observations.

Build a culture of teaching

While not everyone considers themselves to be a teacher, everyone can teach. From a technology or team lead to an individual contributor, everyone can encourage a learning environment that can help to ensure success. One way to facilitate this is to hold periodic presentations where one person on the team is designated to talk about something they've learned or want to share. You can leverage your study group by asking for volunteers to present a new chapter each week until you get to a point where your team feels comfortable with the language.

Designate a champion

Finally, designate a champion to lead a learning effort. This person can act as a subject matter expert (SME) as you start the adoption process. It is important to include this person in all of your practice meetings related to Kotlin. Ideally, this person is already passionate about Kotlin and has some working knowledge.

Integrate slowly

Starting slowly and thinking strategically about what parts of your ecosystem to move first is key. It's often best to isolate this to a single app within your organization rather than a flagship app. In terms of migrating the chosen app, each situation is different, but here are a few common places to start.

Data model

Your data model likely consists of a lot of state information along with a few methods. The data model might also have common methods such as toString(), equals() and hashcode(). These methods can usually be transitioned and unit tested easily in isolation.

For example, assume the following snippet of Java:

public class Person {

   private String firstName;
   private String lastName;
   // ...

   public String getFirstName() {
       return firstName;
   }

   public void setFirstName(String firstName) {
       this.firstName = firstName;
   }

   public String getLastName() {
       return lastName;
   }

   public void setLastName(String lastName) {
       this.lastName = lastName;
   }

   @Override
   public boolean equals(Object o) {
       if (this == o) return true;
       if (o == null || getClass() != o.getClass()) return false;
       Person person = (Person) o;
       return Objects.equals(firstName, person.firstName) &&
               Objects.equals(lastName, person.lastName);
   }

   @Override
   public int hashCode() {
       return Objects.hash(firstName, lastName);
   }

   @Override
   public String toString() {
       return "Person{" +
               "firstName='" + firstName + '\'' +
               ", lastName='" + lastName + '\'' +
               '}';
   }
}

You can replace the Java class with a single line of Kotlin, as shown here:

data class Person(var firstName: String?, var lastName : String?)

This code can then be unit tested against your current test suite. The idea here is to start small with one model at a time and transition classes that are mostly state and not behavior. Be sure to test often along the way.

Migrate tests

Another starting path to consider is to convert existing tests and start writing new tests in Kotlin. This can give your team time to feel comfortable with the language before writing code that you plan to ship with your app.

Move utility methods to extension functions

Any static utility classes (StringUtils, IntegerUtils, DateUtils, YourCustomTypeUtils, and so on) can be represented as Kotlin extension functions and used by your existing Java codebase.

For example, consider you have a StringUtils class with a few methods:

package com.java.project;

public class StringUtils {

   public static String foo(String receiver) {
       return receiver...;  // Transform the receiver in some way
   }

   public static String bar(String receiver) {
       return receiver...;  // Transform the receiver in some way
   }

}

These methods might then be used elsewhere in your app, as shown in the following example:

...

String myString = ...
String fooString = StringUtils.foo(myString);

...

Using Kotlin extension functions, you can provide the same Utils interface to Java callers while at the same time offering a more succinct API for your growing Kotlin code base.

To do this, you could start by converting this Utils class to Kotlin using the automatic conversion provided by the IDE. Example output might look similar to the following:

package com.java.project

object StringUtils {

   fun foo(receiver: String): String {
       return receiver...;  // Transform the receiver in some way
   }

   fun bar(receiver: String): String {
       return receiver...;  // Transform the receiver in some way
   }

}

Next, remove the class or object definition, prefix each function name with the type on which this function should apply, and use this to reference the type inside the function, as shown in the following example:

package com.java.project

fun String.foo(): String {
    return this...;  // Transform the receiver in some way
}

fun String.bar(): String {
    return this...;  // Transform the receiver in some way
}

Finally, add a JvmName annotation to the top of the source file to make the compiled name compatible with the rest of your app, as shown in the following example:

@file:JvmName("StringUtils")
package com.java.project
...

The final version should look similar to the following:

@file:JvmName("StringUtils")
package com.java.project

fun String.foo(): String {
    return this...;  // Transform `this` string in some way
}

fun String.bar(): String {
    return this...;  // Transform `this` string in some way
}

Note that these functions can now be called using Java or Kotlin with conventions that match each language.

Kotlin

...
val myString: String = ...
val fooString = myString.foo()
...

Java

...
String myString = ...
String fooString = StringUtils.foo(myString);
...

Complete the migration

Once your team is comfortable with Kotlin and you have migrated smaller areas, you can move on to tackling larger components such as fragments, activities, ViewModel objects, and other class that are related to business logic.

Considerations

Much like Java has a specific style, Kotlin has its own idiomatic style that contributes to its succinctness. However, you might find initially that the Kotlin code your team produces looks more like the Java code it's replacing. This changes over time as your team's Kotlin experience grows. Remember, gradual change is the key to success.

Here are a few things you can do to attain consistency as your Kotlin code base grows:

Common coding standards

Be sure to define a standard set of coding conventions early on in your adoption process. You can diverge from the Android Kotlin style guide where it makes sense.

Static analysis tools

Enforce the coding standards set for your team by using Android lint and other static analysis tools. klint, a third-party Kotlin linter, also provides additional rules for Kotlin.

Continuous integration

Be sure to conform to common coding standards, and provide sufficient test coverage for your Kotlin code. Making this part of an automated build process can help to ensure consistency and adherence to these standards.

Interoperability

Kotlin interoperates with Java seamlessly for the most part, but note the following.

Nullability

Kotlin relies on nullability annotations in compiled code to infer nullability on the Kotlin side. If annotations are not provided, Kotlin defaults to a platform type which can be treated as the nullable or non-nullable type. This can lead to runtime NullPointerException issues, however, if not treated carefully.

Adopt New Features

Kotlin provides a lot of new libraries and syntactic sugar to reduce boilerplate, which helps to increase development speed. That said, be cautious and methodical when using Kotlin's standard library functions, such as collection functions, coroutines, and lambdas.

Here's a very common trap that newer Kotlin developers encounter. Assume the following Kotlin code:

val nullableFoo: Foo? = ...

// This lambda executes only if nullableFoo is not null
// and `foo` is of the non-nullable Foo type
nullableFoo?.let { foo ->
   foo.baz()
   foo.zap()
}

The intent in this example is to execute foo.baz() and foo.zap() if nullableFoo is not null, thus avoiding a NullPointerException. While this code works as expected, it's less intuitive to read than a simple null check and smart cast, as shown in the following example:

val nullableFoo: Foo? = null
if (nullableFoo != null) {
    nullableFoo.baz() // Using !! or ?. isn't required; the Kotlin compiler infers non-nullability
    nullableFoo.zap() // from guard condition; smart casts nullableFoo to Foo inside this block
}

Testing

Classes and their functions are closed for extension by default in Kotlin. You must explicitly open the classes and functions that you want to subclass. This behavior is a language design decision that was chosen to promote composition over inheritance. Kotlin has built-in support to implement behavior through delegation to help simplify composition.

This behavior poses a problem for mocking frameworks, such as Mockito, that rely on interface implementation or inheritance to override behaviors during testing. For unit tests, you can enable the use of the Mock Maker Inline feature of Mockito, which allows you to mock final classes and methods. Alternatively, you can use the All-Open compiler plugin to open any Kotlin class and its members that you want to test as part of the compilation process. The primary advantage to using this plugin is that it works with both unit and instrumented tests.

More information

For more information on using Kotlin, check out the following links: