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.
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 NullPointerException
s 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:
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:
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:
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:
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:
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:
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:KOTLINfun getLength(subject: String?) = if (subject != null) subject.length else -1You can use the safe call operator,
?.
, which is especially useful in call chains:KOTLINfun hasPlayerReachedTop100(player: Player?): Boolean? {return player?.ranking?.reachedTop100}Here, if both
player
andranking
are not null, the function returns aBoolean
; 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?
.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:
KOTLINfun getLength(subject: String?) = subject?.length ?: -1Kotlin 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 anif/else
null guard that can arguably improve readability when you need to execute a multi-line code block. For example, instead of:KOTLINif (user != null) {user.groups.add(group)user.wasInvited = true}you could go with
KOTLINuser?.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:
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()
, andtoString()
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()
, andtoString()
: 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:
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:
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:
@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
:
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:
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:
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:
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:
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:
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:
val isEmpty: Boolean get() = this.size == 0
A custom setter could look like this:
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:
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:
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:
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:
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:
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:
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:
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:
if (!(x is String) || x.length == 0) { ... }
if (x !is String || x.length == 0) { ... }
You can use the regular as
operator for casting:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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 ofuser.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 thetrimMargin()
extension function does. You can omit both the prefix and thetrimMargin()
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:
- 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.
- 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:
- Start using Kotlin in your test code while keeping production code written in Java.
- 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!