Chapter 01
Kotlin essentials
The Kotlin 2 that Android code relies on: null safety, data and sealed classes, lambdas, collections, coroutines and Flow, plus the Gradle build.
val, var and type inference
Also called type inference
- What
valis a read-only reference,varcan be reassigned, and the compiler infers types when they are obvious.const valis a compile-time constant.- Why
- Defaulting to
valmakes code easier to reason about and matches how Compose and ViewModels expect state to change: through new values, not hidden mutation.
val name = "Ana"
var score = 0
score += 10
val scores: List<Int> = listOf(90, 72)
const val MAX_RETRIES = 3Watch out
A val holding a MutableList can still be changed; read-only reference does not mean immutable contents.
Null safety
Also called elvis operator, safe call, smart cast
- What
- Types are non-null by default;
String?may be null. The safe call?., the Elvis operator?:andlethandle the null case, and!!throws if the value is null. - Why
- It removes most NullPointerExceptions at compile time, which were the top crash cause in Java Android apps.
var nickname: String? = null
val shown = nickname ?: "Guest"
val length = nickname?.length
nickname?.let { println("Hi, $it") }
fun requireUser(user: User?): User =
user ?: error("Not logged in")Watch out
Using !! to quiet the compiler turns a compile error into a crash; values from Java or platform APIs can also be null.
Functions, lambdas and extensions
Also called extension function, higher-order function, trailing lambda, scope function
- What
- Functions support default and named arguments. Lambdas are function values, and a lambda as the last argument can go outside the parentheses. Extension functions add methods to existing types.
- Why
- Compose is built on trailing lambdas (
Column { ... }), and extension functions keep helpers readable without utility classes.
fun greet(name: String, greeting: String = "Hello") =
"$greeting, $name"
greet("Ana", greeting = "Hi")
val isAdult: (Int) -> Boolean = { age -> age >= 18 }
fun String.initials() =
split(" ").mapNotNull { it.firstOrNull() }.joinToString("")Data classes
- What
- A
data classgeneratesequals,hashCode,toString,copyand destructuring from its constructor properties. - Why
- UI state and API models are data classes, and
copyis how you produce a new state with one field changed.
data class User(val id: String, val name: String)
val a = User("1", "Ana")
val b = a.copy(name = "Ana Li")
println(a == User("1", "Ana")) // true: compares values
val (id, name) = bWatch out
Properties declared in the class body, not the constructor, are ignored by equals and copy.
Sealed classes and when
Also called sealed interface, sealed class, when expression
- What
- A
sealedclass or interface has a fixed set of subtypes known at compile time. Awhenexpression over it must handle every subtype. - Why
- Screen states like Loading, Success and Error are modelled this way, so adding a new state makes the compiler point at every place that must handle it.
sealed interface UiState {
data object Loading : UiState
data class Success(val posts: List<Post>) : UiState
data class Error(val message: String) : UiState
}
fun title(state: UiState) = when (state) {
UiState.Loading -> "Loading"
is UiState.Success -> "${state.posts.size} posts"
is UiState.Error -> state.message
}Collections and their operations
- What
- Kotlin separates read-only
List,SetandMapfromMutableListand friends, and offers functions such asmap,filter,groupBy,associateByandsumOf. - Why
- Transforming API data for display is mostly collection work, and read-only types stop other code from changing lists behind your back.
val scores = mapOf("ana" to 90, "li" to 72)
val passed = scores.filterValues { it >= 75 }.keys
val names = mutableListOf("ana")
names += "li"
val byInitial = names.groupBy { it.first() }
val total = scores.values.sum()Coroutines and suspend functions
Also called structured concurrency, coroutine, suspend function
- What
- A
suspendfunction can pause without blocking a thread. Coroutines are started in a scope withlaunchorasync, and cancelling the scope cancels everything started in it. - Why
- Network and database calls are suspend functions. Structured concurrency, via
viewModelScopeorlifecycleScope, stops work automatically when the screen goes away.
suspend fun loadDashboard(api: Api): Dashboard =
coroutineScope {
val user = async { api.user() }
val feed = async { api.feed() }
Dashboard(user.await(), feed.await())
}Watch out
Using GlobalScope leaks work that outlives the screen; launch in viewModelScope or lifecycleScope.
Dispatchers and withContext
Also called Dispatchers, coroutine dispatcher, test dispatcher
- What
- A dispatcher decides which threads a coroutine runs on:
Dispatchers.Mainfor UI,Dispatchers.IOfor blocking I/O andDispatchers.Defaultfor CPU work.withContextswitches for a block. - Why
- Blocking the main thread freezes the UI and, after about five seconds, triggers an "App Not Responding" dialog.
class ProfileViewModel(
private val repo: UserRepository,
) : ViewModel() {
fun refresh() = viewModelScope.launch {
val user = withContext(Dispatchers.IO) { repo.readCache() }
_state.value = UiState.Loaded(user)
}
}Watch out
Retrofit and Room suspend functions are already main-safe, so wrapping them in Dispatchers.IO adds nothing.
Flow and StateFlow
Also called StateFlow, Kotlin Flow
- What
- A
Flowis a cold stream of values produced by a suspend block and read withcollect. AStateFlowis a hot flow that always holds a current value. - Why
- Room queries return Flows that emit on every change, and ViewModels expose UI state as a StateFlow that Compose collects.
fun ticker(): Flow<Int> = flow {
var n = 0
while (true) {
emit(n++)
delay(1_000)
}
}
scope.launch {
ticker().filter { it % 2 == 0 }.collect { println(it) }
}Gradle and version catalogs
Also called build.gradle.kts, libs.versions.toml, version catalog, Maven Central, Groovy
- What
- Gradle builds Android apps. Modules declare dependencies in
build.gradle.kts, and the version cataloggradle/libs.versions.tomlkeeps library versions in one place. - Why
- A single catalog stops modules drifting onto different versions, and a Compose BOM keeps all Compose libraries compatible with each other.
dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.material3)
implementation(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler)
testImplementation(libs.junit)
}