Kotlin Basics: val/var, Null Safety, Smart Casts & Data Classes
The fundamentals asked in every Android interview — master these first
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Kotlin's type system and core syntax form the foundation of every Android interview. val/var control mutability, null safety eliminates NPE at compile time using the ?, ?., ?:, and !! operators, smart casts reduce boilerplate, when expressions replace switch with power, and data classes generate boilerplate-free value objects.
Real-world relevance
In a field operations enterprise app, you might model a WorkOrder as a data class with nullable assignedTo: Employee? — because orders can be unassigned. Using ?.let ensures you only send push notifications when there IS an assignee, while ?: provides default status text in the UI. Smart casts in a when expression dispatch different sync strategies based on the WorkOrder subtype.
Key points
- val vs var — val is read-only (like Java final) — the reference cannot be reassigned. var is mutable. Prefer val by default for immutability and thread safety. val does NOT mean the object itself is immutable — a val List can still be mutable.
- Nullable types with ? — In Kotlin, String is non-nullable (guaranteed non-null). String? is nullable. The compiler enforces null checks at compile time, eliminating NullPointerException at runtime when used correctly.
- Safe call operator ?. — user?.address?.city — chains null checks. If any part is null, the entire expression short-circuits to null instead of throwing NPE. Essential for deeply nested nullable objects in enterprise data models.
- Elvis operator ?: — val city = user?.address?.city ?: 'Unknown' — provides a default value when the left side is null. Can also throw: val id = userId ?: throw IllegalStateException('User ID required')
- Non-null assertion !! — Forces a nullable type to non-null — throws KotlinNullPointerException if null. Use ONLY when you are 100% certain the value is non-null and the compiler cannot infer it. In production code, this is usually a code smell.
- let scope function for null checks — user?.let { safeUser -> process(safeUser) } — executes the block only if user is non-null. Inside the block, safeUser is guaranteed non-null. Replaces verbose if (user != null) { } patterns.
- Smart casts with is — After an is check, Kotlin automatically casts the type — no explicit cast needed. if (shape is Circle) { shape.radius } works because the compiler knows shape is a Circle inside the if block.
- when expression — when replaces Java switch but is far more powerful — it's an expression (returns a value), supports arbitrary conditions, ranges, type checks, and can destructure. Exhaustive when sealed classes/enums forces all cases.
- Data classes — data class User(val id: Int, val name: String) auto-generates: equals(), hashCode(), toString(), copy(), componentN() functions for destructuring. Ideal for DTOs, API responses, UI state models.
- copy() function — val updated = user.copy(name = 'Alice') creates a new instance with only specified fields changed. All other fields are copied from the original. Critical for immutable state management in MVI/Compose architectures.
- Destructuring declarations — val (id, name) = user unpacks data class component functions. Works in for loops: for ((key, value) in map). Behind the scenes calls component1(), component2() etc.
- lateinit vs lazy — lateinit var is for var properties initialized later (not null, throws if accessed before init). lazy { } is for val properties initialized on first access (thread-safe by default). Use lateinit for dependency injection, lazy for expensive computed properties.
Code example
// val vs var — prefer val
val BASE_URL = "https://api.fieldops.com"
var retryCount = 0
// Null safety in a real model
data class WorkOrder(
val id: String,
val title: String,
val assignedTo: Employee?, // nullable — can be unassigned
val completedAt: Long? = null
)
data class Employee(val id: String, val name: String, val email: String)
// Safe call chain + Elvis
fun getAssigneeName(order: WorkOrder): String {
return order.assignedTo?.name ?: "Unassigned"
}
// let for null-safe block execution
fun notifyAssignee(order: WorkOrder) {
order.assignedTo?.let { employee ->
sendPushNotification(employee.email, order.title)
}
}
// Smart cast with when expression
fun describeOrder(order: Any): String = when (order) {
is WorkOrder -> "Work Order: ${order.title}"
is String -> "Raw ID: $order"
else -> "Unknown: $order"
}
// copy() for immutable state update
fun markComplete(order: WorkOrder, timestamp: Long): WorkOrder {
return order.copy(completedAt = timestamp)
}
// Destructuring
fun logOrder(order: WorkOrder) {
val (id, title) = order
println("Processing order $id: $title")
}Line-by-line walkthrough
- 1. val BASE_URL = ... — compiler enforces this reference can never be reassigned; use for constants and injected dependencies
- 2. data class WorkOrder(...) — compiler generates equals/hashCode based on all constructor properties; two WorkOrders with same id/title are equal
- 3. val assignedTo: Employee? — the ? makes this property nullable; compiler will reject direct access without a null check
- 4. order.assignedTo?.name — safe call: if assignedTo is null, this returns null instead of throwing; chains safely
- 5. ?: 'Unassigned' — Elvis: when the left side is null, return this default; right side can also be throw or return
- 6. order.assignedTo?.let { employee -> ... } — let block only executes when assignedTo is non-null; employee inside is guaranteed non-null String
- 7. is WorkOrder -> order.title — smart cast: compiler knows order is WorkOrder inside this branch, no cast needed
- 8. order.copy(completedAt = timestamp) — creates a NEW WorkOrder with all fields copied except completedAt; original is unchanged
- 9. val (id, title) = order — destructuring calls order.component1() and order.component2() automatically
- 10. fun notifyAssignee is unit-returning — no return type needed; Kotlin infers Unit (equivalent to Java void)
Spot the bug
data class User(var id: Int, var name: String)
fun processUser(input: Any) {
if (input is User) {
println(input.name)
}
input.name // smart cast here
}
fun getDisplayName(user: User?): String {
return user!!.name
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Kotlin Basic Syntax — Official Docs (kotlinlang.org)
- Null Safety in Kotlin (kotlinlang.org)
- Data Classes (kotlinlang.org)
- Control Flow: when expression (kotlinlang.org)
- Android Kotlin Style Guide (developer.android.com)