Null Safety & Scope Functions

If you ask any Kotlin developer what they'd miss most going back to Java, most will say the same thing: null safety. It is Kotlin's single most celebrated feature, and for good reason — a huge share of Android crashes historically came down to one root cause, a NullPointerException thrown at runtime because some reference the code assumed would always be present turned out to be null. Kotlin doesn't just help you handle that case more gracefully; it makes the entire category of bug visible to the compiler before the app ever runs. This lesson covers how that works, and introduces Kotlin's scope functions — a small family of functions (let, apply, also, run, with) that make working with objects, including nullable ones, noticeably more readable.

Nullable vs. Non-Nullable Types

In Kotlin, every type comes in two flavors: a plain type like String, which can never hold null, and a nullable version written with a trailing question mark, String?, which explicitly can. This isn't a runtime check or a convention — it's baked into the type system, so the compiler tracks it the same way it tracks whether a variable is an Int or a String.

val trailName: String = "Bear Ridge Loop"   // can never be null
val photoUri: String? = null               // explicitly allowed to be null

// trailName = null        // compiler error — won't even compile
// photoUri.length         // compiler error — must handle null first

For an app like TrailLog, this distinction maps naturally onto the data itself. A Trail's name is always going to be there — every logged hike has one. Its photoUri, on the other hand, is genuinely optional: not every hike gets a photo attached. Modeling photoUri as String? means the compiler forces every piece of code that touches it to consider the "no photo" case — you cannot forget, because the code won't compile until you do.

The Safe Call Operator: ?.

The safe call operator lets you call a method or read a property on a nullable reference without manually checking for null first. If the value on the left is null, the whole expression short-circuits to null instead of throwing — nothing crashes.

val trail: Trail? = repository.findTrailById(42)

// If trail is null, this whole line evaluates to null instead of crashing
val nameLength: Int? = trail?.name?.length

Chaining ?. works across as many nullable steps as needed — as shown above, trail?.name?.length checks trail for null, then (only if it wasn't null) checks name for null, before finally reading length. If anything along the chain is null, the whole expression is null, safely, with no exception thrown.

The Elvis Operator: ?:

Often a null result isn't good enough on its own — you want a sensible default instead. That's what the Elvis operator (?:) is for: "use the value on the left, but if that's null, use the value on the right instead."

val trail: Trail? = repository.findTrailById(42)

val displayName: String = trail?.name ?: "Unnamed Trail"

If trail is null, or if trail.name itself happens to be null, displayName falls back to "Unnamed Trail" instead. Note the result type: displayName is a plain, non-nullable String — the Elvis operator's whole job is to take you from "might be null" back to "definitely not null."

The Not-Null Assertion: !! — Use Sparingly

Kotlin also gives you an escape hatch: the not-null assertion operator, !!, which tells the compiler "trust me, this is never actually null" and unwraps a nullable type into its non-nullable form.

val trail: Trail? = repository.findTrailById(42)
val name: String = trail!!.name  // crashes with NPE if trail is actually null
Honest warning: !! should be used sparingly, and treated as a small red flag when you see it in code review. Every other tool in this lesson exists specifically to let you handle a null case safely — reaching for !! instead re-introduces the exact crash risk that Kotlin's null safety was designed to eliminate. If trail really is null when that line runs, !! throws a NullPointerException at runtime, exactly like Java would have. A handful of situations genuinely justify it (a value you've just checked for null two lines above, or Android framework interop where you know more than the compiler does), but reaching for ?. or ?: first should be the default habit.

Scope Functions: let, apply, also, run, with

Kotlin's standard library includes five small functions whose entire job is to run a block of code "in the scope of" an object, so you can read and write clearer code around that object. They differ in two ways: whether the object is referred to as it or this inside the block, and what the whole expression returns.

FunctionObject asReturnsTypical use
letitlambda resultrun a block only if a nullable value is non-null
applythisthe object itselfconfigure an object's properties, then keep using it
alsoitthe object itselfa side effect (like logging) while passing the original object along
runthislambda resultexecute a block against an object and return a computed result
withthislambda resultsame as run, but called as a top-level function: with(obj) { ... }

Worked Example: Safely Handling a Nullable Photo URI with ?.let { }

let combines beautifully with the safe call operator. value?.let { } reads as "if value is not null, run this block with it" — the block simply never executes for a null value, so there's no separate if check needed:

fun showTrailPhoto(trail: Trail) {
    trail.photoUri?.let { uri ->
        // This block only runs if photoUri is non-null
        imageView.setImageURI(Uri.parse(uri))
        imageView.visibility = View.VISIBLE
    } ?: run {
        // Runs instead, if photoUri was null
        imageView.visibility = View.GONE
    }
}

This single expression replaces what would otherwise be an if (trail.photoUri != null) { ... } else { ... } block, while keeping the non-null value conveniently named (uri here, instead of the default it) right where it's used.

Worked Example: Configuring a New Trail with apply

apply is ideal when you're setting several properties on a freshly created object and want the whole block to read as "configure this, then hand it back":

val newTrail = Trail(
    id = 0,
    name = "",
    notes = "",
    photoUri = null,
    latitude = 0.0,
    longitude = 0.0,
    distanceKm = 0.0,
    date = System.currentTimeMillis()
).apply {
    name = "Bear Ridge Loop"
    notes = "Muddy after the rain, but great views at the top."
    distanceKm = 6.4
    latitude = 37.4275
    longitude = -122.1697
}

Because apply refers to the object as this and returns the object itself, every line inside the block reads as a plain property assignment, and newTrail ends up holding the fully configured Trail — no separate variable, no repeated newTrail.something = ... on every line.

Next up: Null safety keeps individual values honest about whether they can be missing. In Data Classes & Sealed Classes, we look at how Kotlin models entire objects and entire sets of possible outcomes just as rigorously.