On this page
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
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!! last, if at all.The operators
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:
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
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
@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-nullvarinitialised after construction, such as a View Binding inonCreateor an injected field. Accessing it before initialisation throwsUninitializedPropertyAccessException.by lazy: a read-only value computed on first access, then cached. It's thread-safe by default.
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.
// 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
-1as 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.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
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.