Skip to content
ExponaBLOG
MODERN ANDROID & KOTLIN · DAY 004 / 100

Kotlin null safety: a practical Android & Kotlin guide

Stop NullPointerExceptions before they happen.

Expona Admin8 min read

Introduction

NullPointerExceptions have crashed more Android apps than we care to admit. After years of Java's null everywhere, Kotlin's null safety system feels like a breath of fresh air—but only if you understand how to use it. This Kotlin null safety: a practical Android & Kotlin guide walks you through the language's built-in protections, from nullable types to safe calls to the Elvis operator, so you can write code that fails fast at compile time rather than silently at runtime.

Why null safety matters in Android

Android runs on millions of devices with varying memory constraints and network conditions. A single uncaught null reference can cascade into an ANR (Application Not Responding), a crash loop in production, or worse—silent data loss. We've all spent hours digging through crash logs only to find NullPointerException at line X. The traditional Java approach of sprinkling if (x != null) checks everywhere is error-prone; developers inevitably miss one.

Kotlin's type system makes nullability explicit and compile-time checked. You declare whether something can be null when you write the code, not when you discover it in a stack trace at 2 AM. This shift in philosophy, combined with practical operators like safe calls and the Elvis operator, lets us write cleaner, safer code without constant defensive checks.

Nullable types: the foundation

In Kotlin, the type system distinguishes between values that can be null and values that cannot. By default, any type is non-nullable.

kotlin
val name: String = "Alice"  // Non-nullable; cannot be null
val nickname: String? = null  // Nullable; can be null

The question mark (?) is the key. It transforms a type into its nullable variant. If you try to assign null to a non-nullable variable, the compiler rejects it:

kotlin
val age: Int = null  // Compile error: Type mismatch
val age: Int? = null  // OK: nullable Int

This seems simple, but it's powerful. The compiler now knows which values might be null and which cannot. Consider a real-world scenario: fetching a user profile from an API. The user object might exist, but optional fields like a bio or phone number could be missing:

kotlin
data class User(
    val id: String,
    val email: String,
    val bio: String?,
    val phoneNumber: String?,
    val avatar: String?
)

If you call a function expecting a non-null String and pass bio directly, the compiler stops you. You must explicitly handle the possibility of null. This forces intentionality into your code.

Safe calls: navigating optional values

The safe call operator (?.) lets you call methods or access properties on a nullable type without crashing if the value is null. If the receiver is null, the entire expression returns null; otherwise, it proceeds normally.

kotlin
val user: User? = fetchUserFromApi()
val emailLength = user?.email?.length  // Returns null if user is null or email is null

Compare this to Java-style defensive coding:

kotlin
// Java-style (verbose)
val emailLength: Int? = if (user != null && user.email != null) {
    user.email.length
} else {
    null
}

// Kotlin safe call (concise)
val emailLength: Int? = user?.email?.length

Safe calls chain elegantly. In the example above, if user is null, we short-circuit and return null immediately. If user exists but email is null, we also return null. Only if both are present do we compute the length.

A practical example: displaying user information in a profile screen. Rather than nesting if-statements, safe calls keep the code readable:

kotlin
fun displayUserProfile(user: User?) {
    val displayName = user?.name ?: "No name"
    val displayBio = user?.bio ?: "No bio provided"
    val phoneText = user?.phoneNumber?.let { "Call: $it" } ?: "No phone on file"
    
    println("$displayName\n$displayBio\n$phoneText")
}

Notice how the intent is clear: if these fields exist, use them; otherwise, fall back to sensible defaults. No nested nullability checks needed.

The Elvis operator: providing defaults

The Elvis operator (?:) offers a clean way to supply a default value when the left-hand side is null. It's named for Elvis Presley's quiff, which resembles the operator when you tilt your head.

kotlin
val name = user?.name ?: "Guest"

This reads naturally: "use user.name if it exists, otherwise use 'Guest'". The right-hand side is only evaluated if the left side is null, so there's no unnecessary computation.

The Elvis operator works with any expression, not just literals:

kotlin
val displayName = user?.name ?: generateDefaultName()
val apiUrl = config?.endpoint ?: buildDefaultEndpoint()

This is especially useful when dealing with optional API responses. Consider an e-commerce scenario where product descriptions might be missing:

kotlin
data class Product(
    val id: String,
    val title: String,
    val description: String?
)

fun formatProductCard(product: Product): String {
    val desc = product.description ?: "No description available"
    return "${product.title}\n$desc"
}

You can also chain Elvis operators for multiple fallbacks:

kotlin
val finalValue = primaryOption ?: secondaryOption ?: tertiaryOption ?: "default"

This is cleaner than nested if-else statements and reads left to right, matching how developers naturally think about priority.

The not-null assertion (!!): use sparingly

The not-null assertion operator (!!) tells the compiler, "I guarantee this is not null." If you're wrong, you get a NullPointerException immediately. It defeats Kotlin's null safety, so use it only when you are absolutely certain.

kotlin
val user: User? = fetchUser()
val email = user!!.email  // Crashes if user is null

When might this be appropriate? Few cases warrant it. One legitimate scenario: you've just checked for null moments before, and you're in a narrow scope:

kotlin
val user: User? = fetchUser()
if (user != null) {
    val email = user.email  // user is smart-cast to User (non-null)
    println(email)
}

Actually, Kotlin's smart-casting makes the !! unnecessary here. After the null check, user is automatically treated as non-null within that block.

A more dangerous use of !! emerges in callbacks or coroutines where you've lost that context:

kotlin
// Anti-pattern
viewModel.user.observe(this) { user ->
    val email = user!!.email  // Risky; what if emission is null?
}

Better: handle nullability explicitly:

kotlin
viewModel.user.observe(this) { user ->
    val email = user?.email ?: "Unknown"
}

Save !! for places where you're parsing known-good data or interacting with legacy APIs that guarantee non-null returns. Even then, consider wrapping the call and handling the exception.

Common mistakes

Mixing up safe calls and null checks: Remember that user?.name returns a nullable type String?, not a non-nullable String. You still need to handle the result:

kotlin
val name: String? = user?.name  // Correct: result is nullable
val name: String = user?.name  // Compile error: cannot assign nullable to non-nullable

Forgetting that !! can throw: Developers sometimes use !! thinking it's a no-op. It's not; it's an exception waiting to happen. Avoid it unless you have a guard clause immediately before.

Over-defensive Elvis chaining: While chaining Elvis operators is readable, extremely long chains can obscure intent. Keep them to three or four levels max, then consider extracting a helper function:

kotlin
// Hard to read
val value = a ?: b ?: c ?: d ?: e ?: f ?: g

// Better: extract logic
fun resolveValue(): String = a ?: b ?: c ?: "default"

Ignoring context in coroutines: Inside a coroutine or async block, a safe call doesn't automatically handle threading issues. Nullability and concurrency are orthogonal concerns. Always consider where a value comes from and whether it might race.

Real-world example: API response handling

Let's tie this together with a practical scenario. You're building a social media app and fetching a user's profile with optional enriched data:

kotlin
data class ApiUserResponse(
    val id: String,
    val username: String,
    val bio: String?,
    val profilePicUrl: String?,
    val followerCount: Int?
)

class UserRepository {
    suspend fun getUser(id: String): User? {
        return try {
            val response = apiService.fetchUser(id)
            response?.let { 
                User(
                    id = it.id,
                    name = it.username,
                    bio = it.bio ?: "No bio",
                    avatar = it.profilePicUrl,
                    followers = it.followerCount ?: 0
                )
            }
        } catch (e: Exception) {
            null
        }
    }
}

fun renderUserProfile(user: User?) {
    val name = user?.name ?: "Unknown User"
    val bio = user?.bio ?: "No bio provided"
    val avatar = user?.avatar ?: getDefaultAvatarUrl()
    val followerText = user?.followers?.let { "$it followers" } ?: "No followers"
    
    displayProfile(name, bio, avatar, followerText)
}

Notice how nullability is handled at each layer: the API response might be null, fields might be null, and we provide sensible defaults. No !!, no crashes, no defensive nesting.

Key takeaways

  • Declare nullability explicitly in your type system: String? for nullable, String for non-nullable.
  • Use safe calls (?.) to navigate optional values without risk of crashes.
  • Leverage the Elvis operator (?:) to provide defaults cleanly and readably.
  • Avoid the not-null assertion (!!) except in narrow, guarded scopes where you're certain of non-nullability.
  • Kotlin's null safety is a compile-time feature; use it to shift errors from runtime to development.
  • Combine null-safe operators with smart-casting and scoping for code that reads naturally and handles errors gracefully.
  • Test edge cases where APIs return unexpected nulls; null safety helps, but it doesn't replace good test coverage.
---
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles