Adopt Kotlin for large teams

Moving to a new language can be challenging. To set your team up for success, start slow, migrate in small chunks, and test frequently. Kotlin compiles to JVM bytecode and it's fully interoperable with Java, which makes migration easier.

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. You can usually migrate these methods and unit test them 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 in the following example:

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

You can then unit test this code against your current test suite. Start small with one model at a time, and migrate classes that are mostly state rather than behavior. Test often along the way.

Migrate tests

Another place to start is your tests: convert existing tests and write new tests in Kotlin. This gives your team time to get comfortable with the language before you write code that ships with your app.

Move utility methods to extension functions

You can represent static utility classes, such as StringUtils, IntegerUtils, DateUtils, or YourCustomTypeUtils, as Kotlin extension functions and use them from 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 also offering a more succinct API for your growing Kotlin codebase.

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 = "hello"
val fooString = myString.foo()

// ...

Java

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

Complete the migration

After your team is comfortable with Kotlin and you've migrated smaller areas, you can move on to larger components such as fragments, activities, ViewModel objects, and other classes related to business logic.

Considerations

Just as Java has a specific style, Kotlin has its own idiomatic style that contributes to its succinctness. At first, though, you might find that the Kotlin code your team writes looks more like the Java code it's replacing. This changes as your team gains Kotlin experience. Gradual change is the key to success.

Here are a few things you can do to keep your Kotlin codebase consistent as it 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. ktlint, 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 keep the following points in mind.

Nullability

Kotlin relies on nullability annotations in compiled code to infer nullability on the Kotlin side. If annotations aren't provided, Kotlin defaults to a platform type that can be treated as either nullable or non-nullable. If you don't handle platform types carefully, this can lead to runtime NullPointerException issues.

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 common trap that developers new to Kotlin run into. Consider the following Kotlin code:

val nullableFoo: Foo? = 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 isn't null, thus avoiding a NullPointerException. While this code works as expected, it's less intuitive to read than a 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

In Kotlin, classes and their functions are closed for extension by default. You must explicitly open the classes and functions that you want to subclass. Kotlin makes this choice to promote composition over inheritance. To help simplify composition, Kotlin has built-in support for implementing behavior through delegation.

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 lets you 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 about using Kotlin, see the following links: