How do I use the Elvis operator to provide default values in Kotlin?

In Kotlin, the Elvis operator ?: provides a fallback value when the expression on its left is null.

val result = nullableValue ?: defaultValue

If nullableValue is not null, result gets that value.
If nullableValue is null, result gets defaultValue.

Example:

val name: String? = null

val displayName = name ?: "Guest"

println(displayName) // Guest

With a non-null value:

val name: String? = "Alice"

val displayName = name ?: "Guest"

println(displayName) // Alice

It’s often used with safe calls:

val name: String? = null

val nameLength = name?.length ?: 0

println(nameLength) // 0

Here, name?.length returns null if name is null, so ?: 0 supplies the default.

You can also use it with functions:

fun getUsername(): String? {
    return null
}

val username = getUsername() ?: "anonymous"

And because throw and return are expressions in Kotlin, they can be used on the right side:

fun printName(name: String?) {
    val actualName = name ?: return
    println(actualName)
}
fun requireName(name: String?) {
    val actualName = name ?: throw IllegalArgumentException("Name is required")
    println(actualName)
}

So the basic pattern is:

val value = somethingNullable ?: fallbackValue

How do I use the safe call operator `?.` in Kotlin?

In Kotlin, the safe call operator ?. lets you access a property or call a function only if the value is not null.

If the value is null, the expression simply returns null instead of throwing a NullPointerException.

val name: String? = null

val length = name?.length

println(length) // null

Here, name is nullable because its type is String?. Since name is null, name?.length does not try to access .length; it returns null.

Basic syntax

nullableValue?.property
nullableValue?.function()

Example:

val user: User? = getUser()

val email = user?.email

If user is not null, email gets user.email.

If user is null, email becomes null.

Chaining safe calls

You can chain multiple safe calls together:

val city = user?.address?.city

This means:

  • if user is null, return null
  • otherwise check address
  • if address is null, return null
  • otherwise return city

Using ?. with a default value

Often, you combine ?. with the Elvis operator ?::

val length = name?.length ?: 0

This means:

  • if name is not null, use name.length
  • if name is null, use 0

Using ?.let

Use ?.let when you want to run code only when a value is not null:

val name: String? = "Kotlin"

name?.let {
    println("Name is $it")
    println("Length is ${it.length}")
}

The block runs only if name is not null.

Safe call on assignment

Safe calls can also be used on the left side of an assignment:

person?.address?.city = "Paris"

If person or address is null, the assignment is skipped.

Summary

val result = nullableValue?.someProperty

Use ?. when:

  • a value might be null
  • you want to avoid NullPointerException
  • returning null is acceptable when the receiver is null

Common pattern:

val result = nullableValue?.someProperty ?: defaultValue

How do I declare nullable variables using `?` in Kotlin?

In Kotlin, you declare a nullable variable by adding ? after the type.

var name: String? = null

This means name can hold either a String value or null.

Examples:

var age: Int? = null
age = 25

val email: String? = "[email protected]"
val phone: String? = null

Without ?, Kotlin does not allow null:

var name: String = null // Error

With ?, you must handle possible null safely:

val name: String? = null

println(name?.length) // Safe call, prints null instead of crashing

You can also provide a default value with the Elvis operator ?::

val name: String? = null
val length = name?.length ?: 0

println(length) // 0

So the basic pattern is:

var variableName: Type? = null

How do I handle null values safely in Kotlin?

Kotlin handles null safety through its type system: a normal type like String cannot be null, while a nullable type like String? can be null.

1. Use nullable types when a value may be null

val name: String = "Alice"   // Cannot be null
val nickname: String? = null // Can be null

If a value is nullable, Kotlin will not let you access it directly without handling the null case.

val length = nickname.length // Compile error

2. Use the safe-call operator ?.

Use ?. to access a property or call a function only when the value is not null.

val name: String? = getName()

val length: Int? = name?.length

If name is null, length becomes null instead of throwing a NullPointerException.

3. Use the Elvis operator ?: for defaults

Use ?: to provide a fallback value when something is null.

val name: String? = getName()

val length: Int = name?.length ?: 0

You can also return early or throw an exception:

fun printName(name: String?) {
    val nonNullName = name ?: return
    println(nonNullName)
}
fun requireName(name: String?) {
    val nonNullName = name ?: error("Name is required")
    println(nonNullName)
}

4. Use if null checks and smart casts

After checking that a nullable value is not null, Kotlin smart-casts it to a non-null type.

fun printLength(text: String?) {
    if (text != null) {
        println(text.length)
    } else {
        println("Text is null")
    }
}

Inside the if block, text is treated as a non-null String.

5. Use let to run code only when non-null

?.let { ... } is useful when you want to execute a block only if the value exists.

val email: String? = getEmail()

email?.let {
    println("Sending email to $it")
}

You can combine it with Elvis for the null case:

email?.let {
    println("Sending email to $it")
} ?: println("No email available")

6. Avoid !! unless you truly mean “crash if null”

The not-null assertion operator !! converts a nullable value to a non-null value, but throws a NullPointerException if the value is null.

val name: String? = getName()
val length = name!!.length

Prefer safer alternatives:

val length = name?.length ?: 0

Use !! only when null would indicate a serious programmer error, and you want the program to fail immediately.

7. Prefer non-nullable function parameters when possible

If a function requires a value, declare it as non-nullable.

fun greet(name: String) {
    println("Hello, $name")
}

If null is a valid input, declare it explicitly:

fun greet(name: String?) {
    println("Hello, ${name ?: "Guest"}")
}

Quick guide

Situation Use
Access nullable value safely value?.property
Provide default if null value ?: default
Run code only if non-null value?.let { ... }
Check manually if (value != null)
Require non-null and crash if null value!!
Disallow null entirely Use non-null type, e.g. String

In general: prefer non-nullable types, use ?. and ?: for safe handling, and avoid !! unless absolutely necessary.

How do I combine Kotlin collections with coroutines and flows for asynchronous processing?

You typically combine Kotlin collections, coroutines, and Flow by using:

  • collections for in-memory data
  • coroutines for concurrency / async work
  • Flow for asynchronous streams of values

Basic idea

If you have a collection:

val ids = listOf(1, 2, 3, 4, 5)

You can turn it into a Flow:

val idFlow = ids.asFlow()

Then process each item asynchronously using Flow operators:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val ids = listOf(1, 2, 3, 4, 5)

    ids.asFlow()
        .map { id ->
            fetchUser(id)
        }
        .collect { user ->
            println(user)
        }
}

suspend fun fetchUser(id: Int): String {
    delay(500)
    return "User $id"
}

Here:

  • asFlow() converts the collection into a Flow
  • map { } applies a suspending transformation
  • collect { } starts the flow and consumes results

Sequential asynchronous processing

By default, Flow processes elements sequentially:

ids.asFlow()
    .map { id ->
        fetchUser(id)
    }
    .collect { user ->
        println(user)
    }

Even though fetchUser is suspending, each item is processed one after another.

Concurrent processing with flatMapMerge

If you want to process multiple items concurrently, use flatMapMerge:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val ids = listOf(1, 2, 3, 4, 5)

    ids.asFlow()
        .flatMapMerge(concurrency = 3) { id ->
            flow {
                emit(fetchUser(id))
            }
        }
        .collect { user ->
            println(user)
        }
}

suspend fun fetchUser(id: Int): String {
    delay(500)
    return "User $id"
}

This allows up to 3 items to be processed at the same time.

Note: flatMapMerge may emit results out of the original order.

Keeping order while doing concurrent work

If you need concurrency but want results in the original order, you can use async with a collection:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val ids = listOf(1, 2, 3, 4, 5)

    val users = ids.map { id ->
        async {
            fetchUser(id)
        }
    }.awaitAll()

    println(users)
}

suspend fun fetchUser(id: Int): String {
    delay(500)
    return "User $id"
}

awaitAll() returns results in the same order as the original list.

Filtering and transforming Flow values

You can use familiar collection-like operators:

ids.asFlow()
    .filter { id ->
        id % 2 == 0
    }
    .map { id ->
        fetchUser(id)
    }
    .collect { user ->
        println(user)
    }

This is similar to collection processing, but it supports suspending operations.

Collecting a Flow back into a collection

If you need a List again:

val users: List<String> = ids.asFlow()
    .map { id -> fetchUser(id) }
    .toList()

Because toList() collects the flow, it must be called from a coroutine or suspend function.

Using flowOn for background work

You can move upstream processing to a dispatcher:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val ids = listOf(1, 2, 3, 4, 5)

    ids.asFlow()
        .map { id ->
            fetchUser(id)
        }
        .flowOn(Dispatchers.IO)
        .collect { user ->
            println(user)
        }
}

This is useful for I/O-bound work such as network or database calls.

Handling errors

Use catch to handle exceptions from upstream operators:

ids.asFlow()
    .map { id ->
        fetchUser(id)
    }
    .catch { error ->
        emit("Fallback user because of: ${error.message}")
    }
    .collect { user ->
        println(user)
    }

Example: process URLs asynchronously

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val urls = listOf(
        "https://example.com/a",
        "https://example.com/b",
        "https://example.com/c"
    )

    val results = urls.asFlow()
        .flatMapMerge(concurrency = 2) { url ->
            flow {
                val content = download(url)
                emit(url to content.length)
            }
        }
        .toList()

    println(results)
}

suspend fun download(url: String): String {
    delay(1_000)
    return "Content from $url"
}

When to use what

Need Use
Simple in-memory transformation Collection operators: map, filter
Suspending work per item, sequential asFlow().map { suspendCall() }
Suspending work per item, concurrent flatMapMerge
Concurrent work while preserving order map { async { ... } }.awaitAll()
Continuous stream of values Flow
Convert Flow back to List toList()

In short:

val results = items.asFlow()
    .filter { shouldProcess(it) }
    .flatMapMerge(concurrency = 4) { item ->
        flow {
            emit(processAsync(item))
        }
    }
    .toList()

That pattern is a good starting point for asynchronous collection processing with Kotlin coroutines and flows.