Course outline

Android development / Basic: Android and Compose foundations / Lesson 2

Kotlin values, nullability, and domain models

Model reading items with explicit invariants so UI code can make safe decisions without guessing what missing data means.

About 4 min readContent reviewed
On this page

Lesson goals

  • Use immutable values, data classes, and nullable types deliberately.
  • Transform collections while preserving identity and validation rules.

Choose values that express a contract

Kotlin distinguishes val, which cannot be reassigned, from var, which can. A val holding a mutable list does not make that list immutable; the reference and the object have different responsibilities. Prefer immutable-looking domain values and replace them through explicit transformations. Define units and valid ranges near the model rather than scattering checks through buttons. For a reading item, a stable identifier, nonblank title, and nonnegative page count are clearer than a map of arbitrary strings whose keys callers must remember.

Treat missing input as a modeled case

A String? may be null, so callers must decide how absence affects behavior. Safe calls propagate absence; the Elvis operator supplies a fallback; an early return rejects invalid input. These choices carry different product meanings. A missing title should not silently become a successful record named Unknown unless that is a documented rule. Parse external strings with toIntOrNull and validate the resulting number. The non-null assertion operator removes a compiler check but does not establish that a value actually exists at runtime.

kotlin Validate a boundary before constructing a model
data class ReadingItem(val id: String, val title: String, val pages: Int)

fun parseItem(id: String, title: String?, pages: String): ReadingItem? {
    val cleanTitle = title?.trim()?.takeIf { it.isNotEmpty() } ?: return null
    val count = pages.toIntOrNull()?.takeIf { it >= 0 } ?: return null
    return ReadingItem(id, cleanTitle, count)
}

This pure Kotlin excerpt needs no Android APIs. A null result means validation failed; a real form should map the failing field to a specific error instead of reporting every failure identically. The identifier is supplied by a trusted caller here.

Separate identity from display position

A data class generates useful equality and copy operations from constructor properties. Equality compares values, whereas a database identifier establishes continuity across updates. A list index does neither reliably because filtering and sorting move rows. Update the item matching its stable id, produce a new list, and let the UI render that result. copy is shallow: if a property contains a mutable collection, both copies can still refer to the same collection. Prefer immutable elements and avoid exposing mutable internals through a repository interface.

kotlin Replace the matching value without mutating the input
data class ShelfItem(val id: String, val title: String, val finished: Boolean)

fun finish(items: List<ShelfItem>, id: String): List<ShelfItem> =
    items.map { item ->
        if (item.id == id) item.copy(finished = true) else item
    }

The transformation preserves ordering and every existing identifier. If no item matches, the values remain unchanged. Callers must publish the returned list; discarding it does not update existing state. Imports and an application entry point are intentionally omitted.

Make outcomes explicit

Use a sealed interface when an operation has several meaningful outcomes, such as Loading, Ready, and Failed. A when expression can then require handling every known case, reducing combinations of unrelated booleans. Exceptions are appropriate for exceptional failures, while expected validation results often deserve ordinary values. Keep functions small enough to describe in one sentence and avoid hiding mutation inside collection transformations. Lambda syntax is concise, but naming intermediate results can make validation, cancellation, and error handling easier to review than a long chain of clever operators.

Apply Kotlin rules at Android boundaries

Intent extras, JSON responses, database rows, and saved state are all boundaries where values may be missing, stale, or malformed. Validate at entry and convert into a domain type once. Java platform types deserve special care because their nullability may not be fully expressed to Kotlin. Do not use lateinit merely to avoid deciding ownership: accessing it before initialization raises an exception. Constructor injection or a nullable property with a clear lifecycle often expresses the relationship more honestly and makes a local unit test straightforward.

Common errors and fixes

NullPointerException after using !!

Why it happens
An external value was asserted present without validation.
How to fix it
Handle absence with an explicit validation result or early return.
Prevent it next time
Keep nullable types at boundaries and test missing fields.

A supposedly immutable value changes elsewhere

Why it happens
A val references a shared mutable collection.
How to fix it
Stop exposing mutation and return copied immutable elements.
Prevent it next time
Review object mutability separately from variable reassignment.

The wrong row changes after sorting

Why it happens
A display index was used as persistent identity.
How to fix it
Pass the stable item id to the update operation.
Prevent it next time
Test updates after filtering, insertion, and reordering.

Check your understanding

1. Trace parseItem with title containing spaces and pages equal to -3, then with a valid title and pages equal to 12.

Show worked solution

The first call returns null because trimming removes the title; even with a title, the negative count is rejected. The second call returns a ReadingItem with the cleaned title and twelve pages. Neither branch throws for an ordinary malformed numeric string because parsing uses toIntOrNull.

2. A screen has isLoading, hasError, and hasData booleans. Explain an impossible combination and redesign the contract.

Show worked solution

All three can accidentally become true, leaving rendering ambiguous. Replace them with a sealed UI state when the states are mutually exclusive. If stale content may coexist with a refresh error, model that deliberately as content plus refresh status instead of pretending every situation is exclusive.

Key takeaways

  • Nullability describes a contract, not an inconvenience to suppress.
  • Stable identity survives transformations and screen reordering.
  • Explicit result types prevent contradictory UI states.

Official references