12 Kotlin tips for Android developers

10/10/2021

I write technical content targeted at developers. Normally, whatever I have written is published by a client and lives happily ever after. Sometimes though, a ready-to-post article doesn't go public for one reason or another. Whenever I'm sure that an article hasn't been pushed live within a year from the date of delivery, I may post it here on my personal blog. Here's one such piece.

Kotlin for Android developers

What is Kotlin?

Kotlin is a modern statically typed, general-purpose programming language. It is best known as the recommended language for Android application development.

Kotlin has come a long way from gaining first proponents in the Android community, through official support by Google, to recently becoming the preferred language for Android development. As of late 2019, nearly 60% of the top 1000 Android apps contained Kotlin code

Google's official Android development environment, Android Studio, is based on IntelliJ IDEA, the leading Java IDE by JetBrains. Coincidentally, Kotlin is also developed by JetBrains. Google has invested heavily into adopting JetBrains technology, which means Kotlin is here to stay for years. If you are an Android developer and you haven't started the transition to Kotlin, you definitely should.

What’s special about Kotlin?

What do developers love about Kotlin? What made Google pick Kotlin as the preferred programming language for Android development?

Kotlin is known to improve coding efficiency: we tend to write substantially less code in Kotlin. This is due to Kotlin's concise syntax and design that helps reduce boilerplate code and focus on business logic.

For example, here's how Duolingo describes the results of migrating their codebase from Java to Kotlin:

We found that converting a Java file to Kotlin reduced its line count by around 30% on average and by as much as 90% in some cases!

The size benefit from migration to Kotlin isn't just a one-off thing: you can expect Kotlin to help control the growth of your codebase going forward:

Our Android codebase’s line count was growing 46% year over year until we introduced Kotlin in early 2018. Two years, many new product features, and more than twice the number of active contributors later, our codebase is almost exactly the same size now as it was back then!

Kotlin also helps reduce runtime errors because its compiler is great at identifying error-prone code. Kotlin puts emphasis on null safety, requiring you to be very explicit when you expect values in your code to be nullable.

Kotlin is 100% compatible with Java:

  • You can have both Java code and Kotlin code in a single Android project.
  • You can use all Java libraries from your Kotlin code, including all available Android libraries.
  • Android Studio (or the Kotlin plugin in IntelliJ IDEA) can automatically convert existing Java code to Kotlin code for you.
  • Android applications written in Kotlin use the same distribution format as Android applications written in Java. They're installed on user devices same as Java, without requiring any extra effort from the user.

Let's now take a look at a selection of Kotlin features that help enforce null safety and cut down on boilerplate code.

Null safety

When you use Java (and most other programming languages), arguably the most common problem that occurs with your applications at runtime is null references. In Java, when you access a member of a null reference, a NullPointerException (NPE) is thrown. Java doesn't provide any sophisticated tools to avoid null references and is not expected to tackle the nullability issue any time soon.

In contrast, Kotlin was designed from day one to minimize NullPointerExceptions in your applications. Kotlin's type system makes a distinction between nullable references (references that can hold null) and non-nullable references (references that cannot hold null).

By default, all variables and properties in Kotlin are considered non-nullable. If you try to assign a null value to a non-nullable variable, Kotlin won't let you compile:

KOTLIN
var name: String = "My name"
name = null // This won't compile

Using non-nullable types helps you eliminate lots of null checks. For example, if your function accepts non-nullable parameters, you don't need to check for null in that function's body:

KOTLIN
fun fullNameToUpperCase(firstName: String, lastName: String): String {
if (firstName != null && lastName != null) {
return "$firstName $lastName".toUpperCase()
}
else return "Incomplete name"
}

In fact, Android Studio will see that the null checks in the code above are redundant, and suggest that you remove them. As soon as you remove both of them, you can safely get rid of the else branch as well, and end up with code that is both concise and null safe:

KOTLIN
fun fullNameToUpperCase(firstName: String, lastName: String): String {
return "$firstName $lastName".toUpperCase()
}

It could get even more compact if you change the return statement in the function body with expression return and choose to infer the return type:

KOTLIN
fun fullNameToUpperCase(firstName: String, lastName: String) = "$firstName $lastName".toUpperCase()

If you want to allow null values in a variable or property, you have to explicitly declare it with a nullable type. Nullable types have a question mark after their names:

KOTLIN
var name: String?

In this case, Kotlin will expect some kind of null handling logic.

Tools for null handling

In fact, Kotlin provides an array of tools to make null handling efficient and elegant:

  1. You can go with traditional null-guard conditions using if/else. In Kotlin, if/else is an expression, which means it can be used in a single-line expression function, much like the ternary operator in other languages:

    KOTLIN
    fun getLength(subject: String?) = if (subject != null) subject.length else -1
  2. You can use the safe call operator, ?., which is especially useful in call chains:

    KOTLIN
    fun hasPlayerReachedTop100(player: Player?): Boolean? {
    return player?.ranking?.reachedTop100
    }

    Here, if both player and ranking are not null, the function returns a Boolean; however, if either of these are null, the function returns null. This is why the return type of the function is the nullable bool -- Boolean?.

  3. You can choose to use the Elvis operator that is an alternative syntax to return either the value of a nullable reference or, if it's null, a default value:

    KOTLIN
    fun getLength(subject: String?) = subject?.length ?: -1
  4. Kotlin provides a function, let, that is often used together with the safe call operator to only execute a code block if a non-null value has been provided. This is another alternative to an if/else null guard that can arguably improve readability when you need to execute a multi-line code block. For example, instead of:

    KOTLIN
    if (user != null) {
    user.groups.add(group)
    user.wasInvited = true
    }

    you could go with

    KOTLIN
    user?.let {
    it.groups.add(group)
    it.wasInvited = true
    }

If for any reason you don't want to bother with null safety, there's the not-null assertion operator, !!, for the bold and adventurous. It converts a value to a non-nullable type, but if the value turns out to be null, it throws a NullPointerException. Yes, you can have a NPE in Kotlin, but you have to ask for it explicitly.

Data classes

Kotlin provides many ways to reduce bloat and write short, expressive code, and none of these ways is more evident than data classes.

Data classes serve one purpose: hold data. Customer, Person or Note would be your typical data class: it has a few fields for describing relevant details about data, but no operations to go with it. Java classes like this tend to take dozens to hundreds lines of code, but in Kotlin, a simple data class can easily take just a single line:

KOTLIN
data class Person(val firstName: String, val lastName: String)

If you declare a class like this as a data class, Kotlin will:

  • Automatically generate equals(), hashCode(), copy(), and toString() methods. Yes, you don't need to write these methods by hand anymore: the compiler will do that for you, behind the scenes.
  • Even better, Kotlin will update implementations of these methods for you automatically. For example, if you add a new property to your data class or remove an existing property, you don't need to worry about manually updating equals(), hashCode(), and toString(): Kotlin will take care of this.

If you're unhappy with one of the generated methods, you can override it explicitly, and Kotlin will use your implementation. For example, by default, Kotlin generates equals() in the form of Person(firstName=Name, lastName=LastName). You can provide a prettier implementation with a one-liner like this:

KOTLIN
data class Person(val firstName: String, val lastName: String) {
override fun toString() = "$firstName $lastName"
}

As a nice bonus, Kotlin will also auto-generate component functions that you can use for destructuring:

KOTLIN
val person = Person("Jack", "Smith")
val (jack, smith) = person // destructuring
val display = people.map { (name, age) -> "$name is $age years old" } // destructuring in lambdas

You can add annotations to your data class if you need to -- for example, when using it as an entity class with a persistence library:

KOTLIN
@Entity(tableName = "people")
data class Person(
@PrimaryKey @ColumnInfo(name = "id") val personId: String,
val firstName: String,
val lastName: String
) {
override fun toString() = "$firstName $lastName"
}

Lambda expressions

Kotlin's lambda expressions (a.k.a. lambdas) are a great way to define callbacks in a clearly readable way. Kotlin provides lambdas out of the box for Android developers: you don't need to configure your build process or hook additional libraries to use them.

Lambdas are probably the most compact syntax to pass a function to another function, and in Android development, this comes super handy when setting listeners.

Here's a traditional, Java-style way to set a click listener to an ImageView:

KOTLIN
galleryNav.setOnClickListener(object : View.OnClickListener {
override fun onClick(view: View) {
navigateToGallery()
}
})

You can change this code to use a lambda, and get a one-liner as a result:

KOTLIN
galleryNav.setOnClickListener({ view: View? -> navigateToGallery() })

The lambda takes a nullable View as a parameter and returns a Unit (which is Kotlin's way to call void). Since in this case the parameter isn't used inside the lambda, you can actually get rid of it. When a lambda is the only parameter of a method, you can even remove method parentheses. Applying these two changes lets you cut down to this code that is as clear as it gets:

KOTLIN
galleryNav.setOnClickListener { navigateToGallery() }

Android Studio will detect code that you can safely convert into lambdas. It will even do the conversion for you if you press Alt+Enter when standing on a piece of code that can be converted.

Properties

With properties, Kotlin helps lose most of the ceremony related to declaring data and data accessors in a class.

Instead of fields and convention-based getter and setter methods in Java classes, Kotlin lets you get by with only properties. Fields cannot be declared directly in Kotlin classes, but when a property needs a backing field, Kotlin provides it automatically.

In Java, you would write the following class:

JAVA
public class Person {
private String name;
private Integer age;
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

In Kotlin, the same code can be expressed as:

KOTLIN
class Person {
var name: String? = null
var age: Int? = null
}

Even more, as you may recall from reading about data classes, you can move both properties to the primary constructor instead of explicitly declaring them in the body of the class. In addition, you can make the properties non-nullable and get rid of initializers:

KOTLIN
class Person(var name: String, var age: Int)

Custom getters and setters

If you're not happy with just returning a property value when reading and/or setting a new value when writing, you can define custom accessors for a property. Here's an example of a custom getter:

KOTLIN
val isEmpty: Boolean
get() = this.size == 0

A custom setter could look like this:

KOTLIN
var data: List<Course> = emptyList()
set(value) {
currentQuery = value
updateSearchResults()
}

Extension functions

Kotlin's extension functions help add functionality to any class without having control over it, and then access that functionality as if it's available in the class natively.

Thinking of introducing a utility method for String or Android SDK's View? Go for an extension function! Here's how it looks:

KOTLIN
fun ImageView.loadUrl(url: String) {
Picasso.with(context).load(url).into(this)
}

Note how the name of the extension function is preceded by the class it's extending. When you call the extension function, it looks you're calling a regular instance method:

KOTLIN
imageView.loadUrl(url)

In Kotlin, a function doesn't need to be a member of a class, and most of the extension functions are defined on the top level, directly under packages. Extension functions let you eliminate the need to have utility classes with static methods, reduce static invocations, and improve the overall readability of your code.

Object declarations for singletons

Kotlin's objects are first-class citizens: object declarations implement the Singleton pattern natively, on the language level. The object keyword declares a class and initializes a single instance of that class in a thread-safe manner.

Here's how you usually define a Java class for a singleton:

JAVA
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
private synchronized static void createInstance() {
if (instance == null) instance = new Singleton();
}
public static Singleton getInstance() {
if (instance == null) createInstance();
return instance;
}
}

In Kotlin, what you can do instead is this:

KOTLIN
object Singleton

Primary constructors

In Kotlin, you can have one primary constructor and as many secondary constructors as you need.

What's special about the primary constructor is that you can inline it with the class declaration, and it can be used to define and initialize properties:

KOTLIN
class Project(val projectId: String)

This line of code declares a class, adds an immutable String property projectId to that class, and defines a primary constructor that takes the projectId property. All this takes a single line of code! Compare this to the traditional way of defining constructors where you declare a property explicitly, and the constructor simply assigns its parameter to the property:

KOTLIN
class Project {
val projectId: String
constructor(requestId: String) {
this.projectId = requestId
}
}

If you need to include annotations or visibility modifiers into your primary constructor, just use the explicit constructor modifier:

KOTLIN
class Customer public @Inject constructor(name: String)

Smart and safe casts

To check if an object is of a given type, Kotlin provides the is operator, which is quite common. What's less common is that there's a negated version of the operator, !is. You can use it instead of negating the entire expression that you're checking against and getting lost in parentheses. For example, the following two lines are equivalent:

KOTLIN
if (!(x is String) || x.length == 0) { ... }
if (x !is String || x.length == 0) { ... }

You can use the regular as operator for casting:

KOTLIN
val x: String = y as String

This is considered unsafe casting, as it will throw an exception if the cast is not possible.

To prevent this, Kotlin provides a safe cast operator, as?, that returns null instead of throwing an exception:

TEXT
val x: String = y as? String

In many cases, you don't even need to cast explicitly, because the compiler keeps track of is and as operator usages, and inserts safe casts automatically.

For example, if you check a variable for a specific type and provide an operation in case the cast succeeds, you may be tempted to err on the side of caution and write something like this:

KOTLIN
if (x is String) print((x as String).length)

However, you don't need the as cast here: Kotlin will add it for you behind the scenes. What you need to write is simply this:

KOTLIN
if (x is String) print(x.length)

You can even skip an explicit cast if it's preceded by a negative type check that leads to a return:

KOTLIN
if (x !is String) return
print(x.length) // You don't need to cast x here

When expression

when is Kotlin's power version of the switch statement available in many other languages:

KOTLIN
when (x) {
1 -> print("x == 1")
2 -> print("x == 2")
else -> print("otherwise")
}

If many cases are handled in the same way, you can combine branch conditions with a comma:

KOTLIN
when (x) {
1, 2 -> print("x == 1 or x == 2")
else -> print("otherwise")
}

Branch conditions don't need to be constants: instead, you can use any expression:

KOTLIN
when (x) {
"Word".length -> print("x is the length of a word")
else -> print("x is not 1")
}

You can use when branches for type checks with the is operator:

KOTLIN
when(file) {
is Directory -> processDirectory()
is Document -> processDocument()
}

Finally, you can use when branches to check if a value is part (or not part) of a range or collection:

KOTLIN
fun getZoneForLatitude(latitude: Double): Int = when (abs(latitude)) {
in 0.0..7.0 -> 13
in 7.0..14.0 -> 12
in 14.0..21.0 -> 11
in 21.0..28.0 -> 10
in 28.0..35.0 -> 9
in 35.0..42.0 -> 8
in 42.0..49.0 -> 7
in 49.0..56.0 -> 6
in 56.0..63.0 -> 5
in 63.0..70.0 -> 4
in 70.0..77.0 -> 3
in 77.0..84.0 -> 2
else -> 1
}

String templates and multiline strings

In addition to regular strings, Kotlin provides:

  • Raw strings that need no escaping, can contain newlines and arbitrary text. A raw string is delimited by a triple quote (""").
  • String templates that may contain template expressions -- pieces of code that are evaluated and concatenated into the string. A template expression starts with a dollar sign ($) and is usually wrapped into curly braces, although you can omit early braces with simple identifiers.

When you're dealing with multiline strings that vary based on incoming parameter values, these string features in Kotlin can make your code a lot more readable. If you add destructuring declarations into the mix, you can improve clarity even more. Consider this code sample:

KOTLIN
fun introduceUser(user: User): String {
val (firstName, lastName, age, profession, hobby, married, gender) = user
return """
|Hey, my name is $firstName $lastName and I'm $age year${(if (age == 1) "" else "s")} old.
|I'm a $profession by day and a $hobby by night.
|I'm $gender and ${if (married) "married" else "not married" }.
""".trimMargin()
}

There are a few things to note about this code:

  • The destructuring declaration in the first line of the function body helps you refer to user properties without prefixing with the user object every time: firstName instead of user.firstName.
  • In the raw string, whenever a template expression contains an if/else expression, it's wrapped in curly braces, but in other cases it's OK to get by with only a dollar sign.
  • Each new line in the raw string starts with a prefix (|). The prefix allows Kotlin to trim the whitespace before it, which is what the trimMargin() extension function does. You can omit both the prefix and the trimMargin() call, but this would mean all the leading whitespace will be preserved.

Kotlin is worth a try

The language features described above are just a glimpse of what Kotlin provides. Inline functions, collection filtering, ranges, lazy loading, lateinit, companion objects, coroutines -- these are all useful and impressive features that we haven't had a chance to touch upon.

Hopefully, you can now see that Kotlin is a capable, modern, expressive language that guards you from a lot of nullability issues while helping maintain your codebase clean and readable. Kotlin is also actively developed, both by JetBrains and community contributors, and backed by Google for Android development.

Kotlin is 100% interoperable with Java, and you can inject little pieces of Kotlin into your projects along with existing Java code. You don't need to commit to migrate all code or not migrate at all.

On top of that, IntelliJ IDEA and Android Studio both help you automatically convert Java to Kotlin code, which makes learning the language and actual codebase migration a breeze.

In recent years, Java has been trying to catch up with Kotlin in terms of language features, but this has a limited effect for two main reasons:

  1. Android developers are often forced to use older Java versions for maximum device compatibility. Even to use all Java 8 features, you have to set the minimum SDK to API 24, which isn’t practical for many developers. With Java 9 and beyond, it's even more complicated.
  2. Some things that Kotlin supports Java simply doesn't plan to address any time soon. This includes destructuring, string interpolation, and, most importantly, nullability.

Contrary to popular belief, Kotlin's applicability is not limited to Android development:

  • Kotlin is great for developing server-side applications where it ensures full compatibility with existing Java-based technology stacks.
  • Kotlin can be transpiled to JavaScript. This allows building rich client-server applications while reusing code and expertise between the backend and the frontend.
  • Kotlin Native allows to compile Kotlin code to native libraries and make use of existing libraries written in C.
  • Kotlin can be used for data science, thanks to integration with Jupyter Notebook and Apache Zeppelin, and a rapidly expanding ecosystem of data manipulation libraries.

If you're just getting ready for a switch to Kotlin, bear in mind a popular migration strategy that many development teams have used:

  1. Start using Kotlin in your test code while keeping production code written in Java.
  2. As you get comfortable with Kotlin, start iteratively converting production code from Java to Kotlin.

If you want to learn more about Kotlin, check out the official documentation that is almost as clear as the language itself, and use the online playground for experimentation. Finally, if you have IntelliJ IDEA or Android Studio open with a Java project, right-click a Java file, select Convert Java File to Kotlin File, and see what happens!

WRITTEN BY

Jura Gorohovsky

Tennis fan, occasional Product Manager, amateur software developer, impostor.