To get the most out of Kotlin for android development, we'll need to go through some of the best practices we learned in Java again. Many of them can be substituted with better Kotlin-provided alternatives. Let's look at how to write idiomatic Kotlin android code and how to do stuff Kotlin-style.
First, we will see features and practices which you can try in Kotlin 1.4.30, but they are planned for a stable release in Kotlin 1.5.
Save your time. Learn this in as little as 1 minute.
We’ve got 500+ bite-sized content to help you learn the smarter way.
Download the appKotlin 1.5 - Upcoming features and practices:
- Sealed interfaces and sealed classes
- Valhalla optimization
- Stabilization of inline value classes
- Changing JVM name for Java calls
- Support for JVM records
- Value classes
- Inline Value classes
- Init blocks
1. Sealed interfaces and sealed classes
If a class is sealed, it restricts the hierarchy to specified subclasses, allowing for exhaustive branch checks. The enclosed class hierarchy in Kotlin 1.4 has two constraints.
First, the top class must be a class, not a sealed GUI.
Second, all of the subclasses must be in the same file.
Both restrictions are eliminated in Kotlin 1.5: we can now build a sealed GUI. The subclasses (for both sealed classes and sealed interfaces) should be in the same compilation unit and package as the superclass, but they can now be in separate directories.
sealed interface Expr
data class Const(val number: Double) : Expr
data class Sum(val e1: Expr, val e2: Expr) : Expr
object NotANumber : Expr
fun eval(expr: Expr): Double = when(expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
}
For defining abstract data type (ADT) hierarchies, sealed classes and now interfaces are useful. Closing an interface for inheritance and implementation outside the library is another critical use case that can now be handled nicely with sealed interfaces.
When an interface is defined as sealed, it can only be implemented within the same compilation unit and package, which makes it difficult to implement outside of the library in the case of a library. The Job interface from the kotlinx.coroutines package, for example, is only meant to be used inside the kotlinx.coroutines library. This goal is made clear by sealing it:
package kotlinx.coroutines
sealed interface Job { ... }
You are no longer permitted to create your Job subclass as a library user. This was once “implied,” but now that interfaces are sealed, the programmer will explicitly prohibit it.
2. Valhalla optimization
Project Valhalla is an OpenJDK experiment aimed at developing significant new language features for Java 10 and beyond. It introduces primitive groups, a new idea in Java and the JVM. Primitive classes' main purpose is to merge the performance of primitives with the object-oriented advantages of standard JVM classes.
Primitive classes are data holders whose instances can be stored in variables and worked on directly, without the use of headers or pointers, on the computation stack. In this way, they're close to primitive values like int, long, and so on (you don't deal with primitive types directly in Kotlin; instead, the compiler generates them).
Primitive classes have the advantage of allowing for a smooth and dense memory layout of objects.<ArrayPoint> is currently an array of references. With Valhalla support, the JVM can optimize and store an array of Points in a "flat" layout, as an array of many xs and ys directly, rather than as an array of references, when defining Point as a primitive class (in Java terminology) or as a value class with the underlying optimization (in Kotlin terminology). The new JVM optimizations will work for value classes when compiling the code to the JVM with Valhalla's help.
3. Stabilization of inline value classes
Since Kotlin 1.3, inline classes have been available in Alpha, and in 1.4.30, they have been promoted to Beta. Inline classes are stabilized in Kotlin 1.5, but they are now part of a more general function called value classes, which we will discuss later in this article. Let's start with an overview of how inline classes function. You can skip this portion and go straight to the latest updates if you're already familiar with inline classes. An inline class removes the need for a wrapper around a value:
inline class Color(val rgb: Int)
An inline class can wrap a primitive type as well as any reference type, such as String. Where necessary, the compiler replaces inline class instances (in our case, the Color instance) in the bytecode with the underlying form (Int):
fun changeBackground(color: Color)
val blue = Color(255)
changeBackground(blue)
The compiler generates the changebackground function with a mangled name that takes Int as a parameter and passes the 255 constant directly to the call site without generating a wrapper:
fun changeBackground-euwHqFQ(color: Int)
changeBackground-euwHqFQ(255) // no extra object is allocated!
The name has been mangled to allow for smooth overloading of functions that take instances of different inline classes and to avoid unintended invocations from Java code that could violate an inline class's internal constraints.
In certain cases, the wrapper is not removed from the bytecode. This occurs only when it's possible, and it functions in a similar way to primitive forms that are built-in. When you specify a Color type variable or transfer it directly into a function, the underlying value is replaced:
val color = Color(0) // primitive
changeBackground(color) // primitive
The colour variable in this example is of type Color during compilation, but it is replaced with Int in the bytecode. However, if you save it in a set or transfer it to a generic feature, it will be boxed into a standard Color object:
genericFunc(color) // boxed
val list = listOf(color) // boxed
val first = list.first() // unboxed back to primitive
The compiler does the boxing and unboxing for you. You don't have to do anything about it, but learning how things work in Kotlin is always beneficial.
4. Changing JVM name for Java calls
To render an inline class available from Java, modify the JVM name of the function that takes it as a parameter. By default, such names are mangled to prevent Java from unintentionally using them or overloading them (like changebackground-euwHqFQ in the example above).
When you use the @JvmName annotation on a function, it changes the name of the function in the bytecode and allows you to call it from Java and transfer a value directly:
// Kotlin declarations
inline class Timeout(val millis: Long)
val Int.millis get() = Timeout(this.toLong())
val Int.seconds get() = Timeout(this * 1000L)
@JvmName("greetAfterTimeoutMillis")
fun greetAfterTimeout(timeout: Timeout)
// Kotlin usage
greetAfterTimeout(2.seconds)
// Java usage
greetAfterTimeoutMillis(2000);
As with any feature annotated with @JvmName, you call it by its Kotlin name from Kotlin. Since you can only pass a value of the Timeout kind as an argument in Kotlin, and the units are obvious from the use, it is type-safe.
A long value can be passed directly from Java. Since it is no longer type-safe, it does not function by design. It's not instantly clear if greetAfterTimeout(2) is 2 seconds, 2 milliseconds, or 2 years when you see it in the code. By adding the annotation, you're saying that you want to call this feature from Java. A descriptive name helps to prevent ambiguity: the “Millis” suffix applied to the JVM name clarifies the units for Java users
5. Support for JVM records
JVM records are close to Kotlin data groups in that they're primarily data holders. Java records do not obey the JavaBeans convention, and instead of the familiar getX() and getY() methods, they have x() and y() methods ().
Kotlin has always prioritized Java interoperability and continues to do so. As a result, Kotlin code interprets new Java records as classes with Kotlin properties. This functions in the same way as it does for standard Java classes that obey the JavaBeans convention:
// Java
record Point(int x, int y) { }
// Kotlin
fun foo(point: Point) {
point.x // seen as property
point.x() // also works
}
You can annotate your data class with @JvmRecord to have new JVM record methods created, primarily for interoperability reasons:
@JvmRecord
data class Point(val x: Int, val y: Int)
Instead of the standard getX() and getY() methods, the @JvmRecord annotation causes the compiler to generate x() and y() methods.We believe you'll need to use this annotation when converting a Java class to Kotlin to maintain the class's API. In all other instances, Kotlin's familiar data classes can be used without difficulty.
6. Value classes
An immutable object with data is represented by a value class. To support the use-case of "old" inline classes, a value class may currently only have one property. Value classes with several properties will be possible to define in future Kotlin versions with full support for this function. Read-only vals should be used for all values:
value class Point(val x: Int, val y: Int)
Value groups have no identity: they are entirely identified by the data they hold, and === identity checks are not permitted. The == equality search compares the underlying data automatically. Value classes' "identitylessness" allows for major potential optimizations: with Project Valhalla's introduction on the JVM, value classes will be able to be implemented as JVM primitive classes under the hood. Value classes are distinguished from data classes by their immutability restriction and, as a result, the possibility of Valhalla optimizations.
7. Inline Value classes
Inline classes are stabilized in Kotlin 1.5 and become part of a more general function called value classes. Previously, “inline” classes were a separate language function, but they are now a JVM optimization for a value class with only one parameter. Value classes are a broader term that will support a variety of optimizations, including inline classes for now and Valhalla primitive classes in the future when project Valhalla is published (more about this below).
For the time being, the only thing that changes for you is the syntax. Since an inline class is a value class that has been optimized, you must declare it differently than usual:
@JvmInline
value class Color(val rgb: Int)
You build a value class with a single constructor parameter and a @JvmInline annotation.
8. Init blocks
You can specify initialization logic in the init block, which is a good practise for inline classes:
inline class Name(val s: String) {
init {
require(s.isNotEmpty())
}
}
Previously, this was prohibited.
There is one more practice of writing mutating methods. What is a mutating method? When a member function or property setter returns a new instance rather than updating an existing one, it is called a mutating method.
This feature is yet to be prototyped into the Kotlin language so I don’t have a code example yet. This is why it is not on the list. But you can see an example of Swift for mutating methods.
Now we will learn about some basic practices of Kotlin Android development in the Latest Kotlin version 1.4.30.
Kotlin 1.4.30 - Features and practices:
- Use Sealed Classes Rather Than Exceptions
- Don’t use Fluent Setters, Use Named Arguments instead
- Ad-Hoc Structs
- Don't use if-type checks
- Make use of let()
- Destructuring
- For Grouping Object Initialization, use apply().
- Stay away from not-null statements!!
- No Overload of Default Arguments
- Make Use of Value Objects
- Deal with nullability succinctly-avoid if-null checks
- Implementation of Stateless Interfaces (Object 13)
- Look up Constructor Parameters in Property Initializers
- Single Expression Functions with Concise Mapping
- Extension Functions for Utility Function
- Functional Programming
- Use of Expressions
1. Use Sealed Classes Rather Than Exceptions
The use of a dedicated result class hierarchy, particularly for remote calls (such as HTTP requests), can improve the code's protection, readability, and traceability.
// Definition
sealed class UserProfileResult {
data class Success(val userProfile: UserProfileDTO) : UserProfileResult()
data class Error(val message: String, val cause: Exception? = null) : UserProfileResult()
}
// Usage
val avatarUrl = when (val result = client.requestUserProfile(userId)) {
is UserProfileResult.Success -> result.userProfile.avatarUrl
is UserProfileResult.Error -> "http://domain.com/defaultAvatar.png"
}
Unlike exceptions (which in Kotlin are often unchecked), the compiler helps you manage error cases. The compiler also forces you to manage the error case if you use when as an expression.
2. Don’t use Fluent Setters. Use Named Arguments instead
Fluent setters (also known as "Wither") were once common in Java for simulating named and default arguments and making long parameter lists more readable and error-prone:
//Don't
val config = SearchConfig()
.setRoot("~/folder")
.setTerm("game of thrones")
.setRecursive(true)
.setFollowSymlinks(true)
Named and default arguments serve the same function in Kotlin, but they are built into the language:
//Do
val config2 = SearchConfig2(
root = "~/folder",
term = "game of thrones",
recursive = true,
followSymlinks = true
)
3. Ad-Hoc Structs
listOf, mapOf, and the infix function can all be used to quickly construct structs (like JSON). It's not as lightweight as Python or JavaScript, but it's a lot better than Java.
//Do
val customer = mapOf(
"name" to "Clair Grube",
"age" to 30,
"languages" to listOf("german", "english"),
"address" to mapOf(
"city" to "Leipzig",
"street" to "Karl-Liebknecht-Straße 1",
"zipCode" to "04107"
)
)
To build JSON, however, we can typically use data classes and object mapping. However, there are occasions when this is useful (for example, in tests).
4. Don't use if-type checks
The Java approach to type checks is inconvenient and easy to forget.
//Don't
if (service !is CustomerService) {
throw IllegalArgumentException("No CustomerService")
}
service.getCustomer()
We can check the form, (smart-)cast it, and throw an exception if the type is not the intended one by using as? And? All in one expression!
//Do
service as? CustomerService ?: throw IllegalArgumentException("No CustomerService")
service.getCustomer()
5. Make use of let()
If you don't want to use if, you can use let() instead. However, to prevent unreadable "train wrecks," you must use it with caution. Nonetheless, I strongly urge you to think about using let ().
val order: Order? = findOrder()
if (order != null){
dun(order.customer)
}
There is no need for an extra variable when using let(). As a result, we've settled on a single term.
findOrder()?.let { dun(it.customer) }
//or
findOrder()?.customer?.let(::dun)
6. Destructuring
Destructuring is useful for returning multiple values from a function. We can either create our data class (which is the preferred method) or use Pair (which is less expressive due to the lack of semantics in Pair).
//Do
data class ServiceConfig(val host: String, val port: Int)
fun createServiceConfig(): ServiceConfig {
return ServiceConfig("api.domain.io", 9389)
}
//destructuring in action:
val (host, port) = createServiceConfig()
Destructuring, on the other hand, can be used to iterate over a map quickly:
//Do
val map = mapOf("api.domain.io" to 9389, "localhost" to 8080)
for ((host, port) in map){
//...
}
7. For Grouping Object Initialization, use apply()
//Don't
val dataSource = BasicDataSource()
dataSource.driverClassName = "com.mysql.jdbc.Driver"
dataSource.url = "jdbc:mysql://domain:3309/db"
dataSource.username = "username"
dataSource.password = "password"
dataSource.maxTotal = 40
dataSource.maxIdle = 40
dataSource.minIdle = 4
The extension function apply() aids in the grouping and centralization of an object's initialization code. Besides, we won't have to keep repeating the variable's name.
//Do
val dataSource = BasicDataSource().apply {
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://domain:3309/db"
username = "username"
password = "password"
maxTotal = 40
maxIdle = 40
minIdle = 4
}
8. Stay away from not-null statements!!
//Don't
order!!.customer!!.address!!.city
“You may notice that the double exclamation mark looks a bit rude: it’s almost like you’re yelling at the compiler. This is intentional. The designers of Kotlin are trying to nudge you toward a better solution that doesn’t involve making assertions that can’t be verified by the compiler." Kotlin in Action by Dmitry Jemerov and Svetlana Isakova.
9. No Overload of Default Arguments
To achieve default arguments, avoid overloading methods and constructors (also known as "method chaining" or "constructor chaining").
//Don't
fun find(name: String){
find(name, true)
}
fun find(name: String, recursive: Boolean){
}
That is a crutch block. Kotlin has called the following arguments for this purpose:
//Do
fun find(name: String, recursive: Boolean = true){
}
10. Make Use of Value Objects
Writing immutable value objects is a breeze with data classes. Including when there is only one property in a value object. So there's no longer any reason for not using value objects!
// Don't
fun send(target: String){}
// Do
fun send(target: EmailAddress){}
// expressive, readable, type-safe
data class EmailAddress(val value: String)
// Even better (Kotlin 1.3):
inline class EmailAddress(val value: String)
For value properties, we can use inline classes because Kotlin 1.3. Since the compiler eliminates the wrapping inline class and uses the wrapped property directly, we avoid the overhead of additional object development. As a result, it's a free abstraction.
11. Deal with nullability succinctly-avoid if-null checks
//Don't
if (order == null || order.customer == null || order.customer.address == null){
throw IllegalArgumentException("Invalid Order")
}
val city = order.customer.address.city
Hold on any time you write an if-null search. Nulls are handled even more effectively in Kotlin. In certain cases, a null-safe call?. or the elvis operator?: can be used instead.
//Do
val city = order?.customer?.address?.city ?: throw IllegalArgumentException("Invalid Order")
12. Implementation of Stateless Interfaces using Object
When we need to build a system interface with no state, Kotlin's object comes in handy. Consider the Converter interface in Vaadin 8.
//Do
object StringToInstantConverter : Converter<String, Instant> {
private val DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss Z")
.withLocale(Locale.UK)
.withZone(ZoneOffset.UTC)
override fun convertToModel(value: String?, context: ValueContext?) = try {
Result.ok(Instant.from(DATE_FORMATTER.parse(value)))
} catch (ex: DateTimeParseException) {
Result.error<Instant>(ex.message)
}
override fun convertToPresentation(value: Instant?, context: ValueContext?) =
DATE_FORMATTER.format(value)
}
13. Look up Constructor Parameters in Property Initializers
Consider if you need to define a constructor body (init block) just to initialize assets.
// Don't
class UsersClient(baseUrl: String, appName: String) {
private val usersUrl: String
private val httpClient: HttpClient
init {
usersUrl = "$baseUrl/users"
val builder = HttpClientBuilder.create()
builder.setUserAgent(appName)
builder.setConnectionTimeToLive(10, TimeUnit.SECONDS)
httpClient = builder.build()
}
fun getUsers(){
//call service using httpClient and usersUrl
}
}
In property initializers, we may refer to the primary constructor parameters (and not only in the init block). Apply () can be used to merge initialization code into a single expression.
// Do
class UsersClient(baseUrl: String, appName: String) {
private val usersUrl = "$baseUrl/users"
private val httpClient = HttpClientBuilder.create().apply {
setUserAgent(appName)
setConnectionTimeToLive(10, TimeUnit.SECONDS)
}.build()
fun getUsers(){
//call service using httpClient and usersUrl
}
}
14. Single Expression Functions with Concise Mapping
// Don't
fun mapToDTO(entity: SnippetEntity): SnippetDTO {
val dto = SnippetDTO(
code = entity.code,
date = entity.date,
author = "${entity.author.firstName} ${entity.author.lastName}"
)
return dto
}
We can write easy, succinct, and readable mappings between objects using single expression functions and named arguments.
// Do
fun mapToDTO(entity: SnippetEntity) = SnippetDTO(
code = entity.code,
date = entity.date,
author = "${entity.author.firstName} ${entity.author.lastName}"
)
val dto = mapToDTO(entity)
If you want, you can use extension functions to make the feature description and usage even shorter and more readable. At the same time, the mapping logic does not pollute our value object.
// Do
fun SnippetEntity.toDTO() = SnippetDTO(
code = code,
date = date,
author = "${author.firstName} ${author.lastName}"
)
val dto = entity.toDTO()
15 .Extension Functions for Utility Function
In Java, static util methods are often generated in util classes. The following is a direct Kotlin translation of this pattern:
//Don't
object StringUtil {
fun countAmountOfX(string: String): Int{
return string.length - string.replace("x", "").length
}
}
StringUtil.countAmountOfX("xFunxWithxKotlinx")
Top-level functions can be used instead of the redundant wrapping util class in Kotlin. We may often make use of extension functions to improve readability. Our code would feel more like "telling a story" this way.
//Do
fun String.countAmountOfX(): Int {
return length - replace("x", "").length
}
"xFunxWithxKotlinx".countAmountOfX()
16. Functional Programming
Functional programming, among other things, helps us to minimize side effects, which makes our code...
- Thread-safe,
- less error-prone,
- easier to grasp,
- easier to evaluate.
In comparison to Java 8, Kotlin has much better functional programming support:
- Immutability: variables and properties with values, immutable data types, and copy ()
- Expressions: Single expression functions are called expressions. Expressions include if, when, and try-catch. These control structures can be combined with other phrases in a succinct manner.
- Types of Functions
- Lambda Expressions in a Nutshell
- The Collection API in Kotlin
These features make it possible to write usable code that is safe, succinct, and expressive. As a consequence, we can more easily construct pure functions (functions with no side effects).
17. Use of Expressions
// Don't
fun getDefaultLocale(deliveryArea: String): Locale {
val deliverAreaLower = deliveryArea.toLowerCase()
if (deliverAreaLower == "germany" || deliverAreaLower == "austria") {
return Locale.GERMAN
}
if (deliverAreaLower == "usa" || deliverAreaLower == "great britain") {
return Locale.ENGLISH
}
if (deliverAreaLower == "france") {
return Locale.FRENCH
}
return Locale.ENGLISH
}
// Do
fun getDefaultLocale2(deliveryArea: String) = when (deliveryArea.toLowerCase()) {
"germany", "austria" -> Locale.GERMAN
"usa", "great britain" -> Locale.ENGLISH
"france" -> Locale.FRENCH
else -> Locale.ENGLISH
}
As a general rule, if you write an if, consider if it could be replaced with a more succinct when expression. A useful expression is try-catch:
val json = """{"message":"HELLO"}"""
val message = try {
JSONObject(json).getString("message")
} catch (ex: JSONException) {
json
}
That was an exhaustive list, wasn’t it? And if you have made it this far, I am sure you will be having a better idea of what is coming and what best practices to use for Kotlin android programming. While learning Kotlin, slogging code is not a very good idea. Coding concisely with better readability is also required and this is what Kotlin provides you over java. So keep the above points in mind and improve your Kotlin android code.
Want to build your first blockchain program? Join the Kool community and discover a micro learning space- KoolStories!