Skip to Main Content
Engineers meeting about Kotlin language features

Our 5 Favorite Kotlin Features

When Swift was released by Apple for iOS developers, Android developers were in awe and envious of this modern programming language that provides null-safety and an expressive yet concise syntax. Thankfully, the Android Studio 3.0 release in 2017 saw Kotlin introduced as an alternative programming language to Java for Android development. In 2019, Google then announced that Android development will increasingly be Kotlin-first. 

Given all these developments and Kotlin’s surge in popularity, we thought we’d share a few features we love about our Android programming language of choice.

Null Safety

java.lang.NullPointerException — or NPE for short — has long been a source of headache for Java programmers, and a lot of Java code are littered with null checks to ensure something isn’t null before they’re interacted with. This isn’t ideal because the null checking is prone to human error, and it makes code look unnecessarily cluttered with additional levels of indentation. 

Kotlin introduces nullable types by giving developers the ability to designate variables as potentially containing ‘null’ as their values. This is often called optional types in other languages that offer similar null safety features like Swift or JavaScript. By forcing developers to explicitly state if a reference can be ‘null’ or not, the IDE can then help mitigate against unprotected calls to nullable references by throwing a compile-time error. 

For example:

val nullableString: String? = null
val stringLength = nullableString.length // This will cause a compile-time error

Safe Calls

The most powerful feature of null safety comes in the form of safe calls, also known as optional chaining in other similar languages. This use case surfaces when we need to access nested fields/properties of a nullable object which may themselves be nullable. 

Imagine if Kotlin works like Java, and there is no concept of nullable types, our code to access a string’s length may look like this:

val stringLength: Int
if (nullableString != null && nullableString.length != null) {
    stringLength = nullableString.length
} else {
    stringLength = 0
}

With null safety and the Kotlin elvis (“?:”) operator, our code above is instead written the following way:

val stringLength = nullableString?.length ?: 0

Another practical use of safe calls is to only perform certain method calls on a referenced object if it’s not null to begin with. If any of the calls along a nullable chain results in a null value, the final variable will become null itself.

For example:

val gatt: BluetoothGatt? = ConnectionManager.activeGatt
val hrService = gatt?.services?.firstOrNull {
    it.uuid.toString() == "0000180d-0000-1000-8000-00805f9b34fb"
}

Note: it’s possible to completely bypass the null safety system by appending “!!” to a nullable call. We DO NOT recommend doing this as it completely defeats the purpose of using Kotlin in the first place. Any “!!” usage in production code should be treated as code smell, except in the most bizarre of circumstances.

Lateinit Var and Lazy Properties

Even with nullable types, there may be situations where we simply want variables to be treated as not null, but we cannot supply values to them when we declare them as a property of a class.

// Class property
val gatt: BluetoothGatt

In the example above (which is completely valid in Java), the IDE will complain that the gatt variable must be initialized or be declared abstract. While we can make the gatt variable a nullable reference and set it to null to begin with, we’ll then have to deal with using safe calls or unwrapping the value constantly to get to the underlying value. This can be inconvenient if done at scale, and Kotlin has a special keyword of ‘lateinit var’ just for this use case.

// Class property
private lateinit var gatt: BluetoothGatt

We can declare ‘lateinit var’ properties that are treated as a regular, nonnull type. However, a runtime exception will be thrown if we attempt to access a ‘lateinit var’ property that hasn’t been initialized yet.

One can work around this by checking if it has been initialized yet before accessing it:

if (::gatt.isInitialized) { /* Do something with it */  }

While using a ‘lateinit var’ is fine for most cases, there’s a major caveat to using ‘lateinit var’: we’d lose the immutability guarantee that comes with a ‘val’ type variable. This is where lazy initialization shines, and using lazy initialization also enforces immutability if you so choose.

private val gatt: BluetoothGatt by lazy {
    ConnectionManager.servicesOnDevice(device)
}

Lazy initialization for a property means that a property is only initialized when its value is needed. This means that we can potentially have multiple lazy properties, all of which may be dependent on one another. As long as any dependencies in the lambdas that produce these values along the dependency graph are all available when the lazy properties are initialized, no error will be thrown.

A very common use case for lazy properties in the context of Android development is to get an object out of an Intent in an Activity’s onCreate(), or to get access to system services objects that cannot be obtained before onCreate() is called:

private val bluetoothAdapter: BluetoothAdapter by lazy {
    val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
    bluetoothManager.adapter
}

Platform Types

Kotlin-Java interop is seamless and great, with the usage of @Nullable and @NonNull annotations on the Java side that translates directly to a method call being nullable or non-nullable over on the Kotlin side. Despite this and all the guardrails in Kotlin that make it significantly harder to commit the Billion Dollar Mistake, we have to remember that the Android SDK is still Java-based. 

Not all Java methods in the Android SDK are annotated with @Nullable or @NonNull, and this makes it impossible for the Kotlin compiler to infer if a BluetoothGatt’s getDevice() should return a “BluetoothDevice” or a “BluetoothDevice?”, for instance. In these cases, Kotlin uses the “!” notation to indicate a value of type “T!” could be either “T” or “T?”. This is known as the platform type notation.

At Punch Through, we usually treat a platform type as a nullable reference just to be safe. If a platform type is accessed in an unsafe manner, your app will crash if the underlying value is found to be null.

val gatt: BluetoothGatt // Provided somewhere
val deviceAddress = gatt.device?.address ?: "" // Recommended
val deviceAddress = gatt.device.address // Not recommended

Scope Functions

There are five Kotlin scope functions — let, run, with, apply, and also — each of them rescoping the object on which they’re called on to either ‘it’ or ‘this’ to perform some operation on. Realistically, we’ve found that with this many tools in the kit, we usually stick to a couple that we’re familiar with in order to get things done. 

Here are two use cases where we like to use ‘let’ and ‘apply’ on.

Unwrap Nullable Types Using ‘let’

It’s very common when writing Kotlin code to only perform certain operations if a given nullable reference is not null, and ‘let’ is great for this use case:

val gatt: BluetoothGatt? // Provided somewhere
gatt?.device?.let {
    val deviceAddress = it.address
    // ...
}

Perform Multiple Operations on an Object Using ‘apply’

The ‘apply’ function is the perfect way to call multiple functions or set multiple properties on an object — nullable or not. Note that ‘apply’ rescopes the object that it’s called on as ‘this’ inside the code block, instead of ‘it’ like what ‘let’ does.

// A common use case in Activity’s onCreate()
supportActionBar?.apply {
    setDisplayHomeAsUpEnabled(true)
    setDisplayShowTitleEnabled(true)
    title = getString(R.string.title)
}

The fact that ‘apply’ returns the object from which it’s called on also means that we can instantiate an object before calling ‘apply’ as well.

// A convenient way of instantiating an object and settings its properties
val someRect = Rect().apply {
    left = 0
    right = 0
    top = 0
    bottom = 0
}
 
// As opposed to this
val someOtherRect() = Rect()
someOtherRect.left = 0
someOtherRect.right = 0
someOtherRect.top = 0
someOtherRect.bottom = 0

Other Scope Functions

We sometimes also use ‘run’ right after an elvis operator (“?:”) to do some logging if something unexpectedly returns null. The ‘with’ function can also be useful when we want to call multiple methods or access multiple properties on an object. 

There are so many use cases to explore here, and interested readers can check out the official Kotlin documentation on scope functions to learn more.

Extension Functions and Properties

Ever wish all your Activity classes can have a copyToClipboard() function? Kotlin’s extensions feature solves pain points like this by allowing you to extend a class’ functionality, essentially adding a function or property to a class.

Extension functions and properties are accessed as if they’re present on the extended class itself, and is a neat way of abstracting away class-specific logic from call sites that don’t require knowledge of that implementation logic. 

We usually keep our general-purpose extension functions in separate files, named after the class they’re extending. For example, the Context extension functions below would belong in a Context.kt file. 

One important thing to note is that in your implementation of an extension function, ‘this’ is the object on which your extension function is called on.

fun Context.copyToClipboard(plainText: String, label: String = "") {
    val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
    val clip = ClipData.newPlainText(label, plainText)
    clipboardManager.setPrimaryClip(clip)
}
 
fun Context.hasPermission(permissionType: String): Boolean {
    return ContextCompat.checkSelfPermission(this, permissionType) ==
        PackageManager.PERMISSION_GRANTED
}

With the example shown above, as long as the extension functions are visible to the calling file, we can call them on a Context object. For example, we can call copyToClipboard(…) directly from an AppCompatActivity class, which is a subclass of Context.

Specialized Class Types

There is no shortage of specialized class types in Kotlin — each with their own purposes — but in this post we’ll cover our two favorites: data classes and sealed classes.

Data Classes

In OOP (Object-Oriented Programming), we were often taught to create abstractions that hold data as a unit, like an Employee class that contains an employee’s name, age, salary, and other pieces of information. Working in Java, the class itself is easy to implement, but we were left with the arduous task of having to override equals() and hashCode() regularly. Kotlin attempts to alleviate this pain point with the introduction of data classes, whose sole purpose is to hold model type objects.

Kotlin data classes contain their main properties in their primary constructors. These properties will be used to generate the equals() and hashCode() overrides, unless the developer chooses to override these on their own.

data class Employee(
    val name: String,
    val age: Int,
    val salary: Double
)

With the class declaration as shown above, an employee whose name, age, and salary are the same will be deemed the same employee where equality and hashing are concerned.

Sealed Classes

Sealed classes are essentially enum classes on steroids. Enum classes are like basic enumeration structures in most programming languages; they encapsulate a collection of related types or values. Sealed classes go one step further and allow their subclasses to have different properties in them, thus being able to store state.

Sealed classes benefit from Kotlin’s smart type inference, and a common way of using sealed classes after declaring them and their subclasses is to have a function take in a type matching the sealed class. Once inside the function, the developer then uses a ‘when’ expression to determine which concrete subclass the object actually is, and performs some meaningful operation based on that knowledge. 

Here’s an example of a class hierarchy of BLE operations:

/** Abstract sealed class representing a type of BLE operation */
sealed class BleOperationType {
    abstract val device: BluetoothDevice
}
 
/** Connect to [device] and perform service discovery */
data class Connect(
    override val device: BluetoothDevice,
    val context: Context
) : BleOperationType()
 
/** Disconnect from [device] and release all connection resources */
data class Disconnect(override val device: BluetoothDevice) : BleOperationType()
 
/** Read the value of a characteristic represented by [characteristicUuid] */
data class CharacteristicRead(
    override val device: BluetoothDevice,
    val characteristicUuid: UUID
) : BleOperationType()

With this sealed class and its subclasses, we can then have a function that decides what to do based on the concrete subclass of BleOperationType. This function is written as a single-expression function, another one of Kotlin’s features that we love.

fun performOperation(operation: BleOperationType) = when (operation) {
    is Connect -> operation.device.connectGatt(operation.context, false, callback)
    is Disconnect -> disconnect(operation.device)
    is CharacteristicRead -> read(operation.device, operation.characteristicUuid)
}

Note that inside the ‘is’ block for each operation type, we have access to properties specific to those concrete subclasses — that is the beauty of a restricted class hierarchy that sealed classes offer.

QoL Improvements

Kotlin has a ton of quality-of-life (QoL) improvements over Java, but here are our favorite ones.

Default Function Arguments

It’s common practice in Java to overload methods and constructors, which can become tedious as the number of arguments increases. Kotlin allows us to specify default arguments in functions so we no longer need to account for the various combinations of overloaded functions and constructors. A default argument is specified using the equals sign (“=”) right after specifying the type for an argument. 

Here’s a very simple example:

fun saySomething(greeting: String = "Hello") {
    println(greeting)
}

With this, we can either do saySomething() or saySomething(“Hi”), the former of which will result in “Hello” being printed and the latter would print “Hi” instead.

String Templates

Kotlin’s introduction of string templates means we no longer need to use String.format or manual string concatenation when composing strings with values stored in variables. We use a single dollar sign followed by either a variable, or a more complex expression surrounded by curly braces. 

The example below shows both these variations in action:

val device: BluetoothDevice // Provided somewhere
println("Device: $device, address: ${ device.address }.")

Kotlin is ❤

There are so many things to love about Kotlin. Kotlin is updated frequently with improvements and new features, which is the complete opposite of Java 8 in the context of Android development. We strongly feel that code written in Kotlin is a lot more concise and readable, which is a huge contributor towards code maintainability. Developers are also protected against common pitfalls like NullPointerExceptions, and can work quickly and more efficiently using Kotlin’s expressive syntax.

Kotlin alleviates a lot of Java’s pain points, and is truly an impressive programming language in its own right. We’re beyond excited about Kotlin’s future, knowing that Google is doubling down on it, and that other developers love it, and we hope you are too!

Interested in Learning More?

Chee Yi is just one of our talented engineers and writers. Discover information about the people behind the brand, how we work, and why we do what we do.