๐ŸŸฃ Kotlin Programming

Kotlin Null Safety Explained with Examples

NullPointerException was once one of the most common causes of Android crashes. Kotlin moves nullability into the type system so the compiler catches most of these bugs before they ship. Here's how it works and how to use it well in Android code.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Beginner
On this page
  1. Nullable and non-null types
  2. The operators
  3. Smart casts and their limits
  4. Null safety in Android code
  5. lateinit and lazy
  6. Java interop and platform types
  7. When is !! acceptable?
  8. Common mistakes
  9. Frequently asked questions
  10. Conclusion

In Java, any object reference might be null, and you only find out at runtime, often in a user's hands. Kotlin flips this: a type is non-null by default, and you must explicitly say when something can be null. The compiler then makes you handle that case.

This article covers the operators, the patterns that keep Android code clean, the edge cases (Java interop, lateinit, platform types), and the habits that keep !! out of your codebase.

Nullable and non-null types

BasicsKotlin
var title: String = "Inbox"
// title = null            // โŒ compile error: String can't hold null

var subtitle: String? = null   // โœ… nullable type
// subtitle.length          // โŒ compile error: must handle the null case
Ways to handle a nullable value A String? value can be handled with a safe call returning null, the Elvis operator providing a default, let to run code only when non-null, an if check enabling a smart cast, or the not-null assertion operator which throws if null. name: String? ?.safe call โ†’ null ?:Elvis default ?.let { }run if present if (x != null)smart cast !!throws if null
Four safe options and one escape hatch. Reach for !! last, if at all.

The operators

Operators in practiceKotlin
data class Address(val city: String?)
data class User(val name: String, val address: Address?)

fun cityLabel(user: User?): String =
    user?.address?.city ?: "City not set"          // safe-call chain + Elvis default

fun greet(user: User?) {
    user?.let { println("Welcome back, ${it.name}") }   // runs only when user != null
}

fun requireUser(user: User?): User =
    user ?: throw IllegalStateException("User must be signed in")   // Elvis can throw or return

fun firstChar(text: String?): Char? {
    if (text.isNullOrEmpty()) return null           // early return
    return text[0]                                   // smart cast: text is String here
}

Smart casts and their limits

After a null check, the compiler treats a local val as non-null. It can't do this for mutable properties (var) or values from other modules, because another thread or code path could change them between the check and the use. Copy them into a local val first:

Smart cast on a var propertyKotlin
class PlayerScreen {
    var currentTrack: Track? = null

    fun showTitle() {
        val track = currentTrack ?: return       // capture once, then it's non-null
        titleView.text = track.title
    }
}

Null safety in Android code

Intent extras and arguments

Reading navigation arguments safelyKotlin
class ArticleViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    // Fail fast with a clear message if a required argument is missing.
    private val articleId: String = checkNotNull(savedStateHandle["articleId"]) {
        "ArticleViewModel requires an articleId argument"
    }
}

Model absence explicitly in UI state

Nullable fields are fine for genuinely optional data, but don't use null to mean "loading", "error" and "empty". Use a sealed type so every state is explicit (see sealed classes).

Compose

Optional UI piecesKotlin
@Composable
fun ProfileHeader(name: String, bio: String?) {
    Column {
        Text(name, style = MaterialTheme.typography.titleLarge)
        bio?.takeIf { it.isNotBlank() }?.let { Text(it) }   // render only when present
    }
}

lateinit and lazy

  • lateinit var: a non-null var initialised after construction, such as a View Binding in onCreate or an injected field. Accessing it before initialisation throws UninitializedPropertyAccessException.
  • by lazy: a read-only value computed on first access, then cached. It's thread-safe by default.
lateinit vs lazyKotlin
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding            // set in onCreate
    private val dateFormatter by lazy { DateTimeFormatter.ofPattern("d MMM yyyy") }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }
}

Java interop and platform types

Kotlin can't know whether an unannotated Java method returns null, so the result is a platform type (shown as String!). Kotlin lets you treat it either way, and if you choose non-null and it's null, you crash at that line.

Defend at the boundaryKotlin
// Legacy Java: public String getDisplayName() { ... may return null ... }
val displayName: String? = legacyUser.displayName     // declare it nullable deliberately
val label = displayName ?: "Guest"

Better still, add @Nullable or @NonNull annotations to the Java code. The Android SDK and AndroidX are annotated, so Kotlin sees accurate types for them.

When is !! acceptable?

Rarely: in tests, or when an invariant is guaranteed and a crash is the correct outcome if it's violated. Even then, checkNotNull(x) { "reason" } or requireNotNull gives a much better error message than a bare !!.

Common mistakes

  • Using !! to make red underlines disappear.
  • Making everything nullable "to be safe". This pushes null checks everywhere and hides real bugs.
  • Using empty strings or -1 as fake "no value" markers instead of nullable or sealed types.
  • Trusting platform types from Java or JSON without handling nulls.

Frequently asked questions

Can a Kotlin app still crash with NullPointerException?

Yes: through !!, platform types from Java, uninitialised lateinit properties, or reflection and serialisation that bypass constructors. They're much rarer, but possible.

How does JSON parsing interact with null safety?

kotlinx.serialization respects nullability and default values in your data classes, so mark fields nullable or give defaults when the API may omit them.

Is ?.let always better than if?

No. For simple checks, if (x != null) with a smart cast is often clearer, especially when there's an else branch.

Conclusion

Kotlin's null safety turns a whole class of runtime crashes into compile-time errors. Use non-null types by default, handle optional values with ?., ?:, let and smart casts, defend at Java boundaries, and treat !! as a code smell that needs justification.

Next step: model your screen states with data, sealed and enum classes.

Official references

๐Ÿ“ฑ Apps built by Yunok Tech

Real Android apps on Google Play, free to download.

See all our apps โ†’

Ready to build real Android applications?

Kotlin fluency makes everything else in Android easier, from coroutines and Flow to Compose and clean architecture. The fastest way to build that fluency is to write real Kotlin every day and get feedback on it.

Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.

Explore the Android course and take your next step as an Android developer.

Share: LinkedIn X / Twitter WhatsApp