Mobile development: Android

Sefism.

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
val is a read-only reference, var can be reassigned, and the compiler infers types when they are obvious. const val is a compile-time constant.
Why
Defaulting to val makes code easier to reason about and matches how Compose and ViewModels expect state to change: through new values, not hidden mutation.
Kotlin
val name = "Ana"
var score = 0
score += 10
val scores: List<Int> = listOf(90, 72)
const val MAX_RETRIES = 3

Watch 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 ?: and let handle 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.
Kotlin
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.
Kotlin
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 class generates equals, hashCode, toString, copy and destructuring from its constructor properties.
Why
UI state and API models are data classes, and copy is how you produce a new state with one field changed.
Kotlin
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) = b

Watch 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 sealed class or interface has a fixed set of subtypes known at compile time. A when expression 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.
Kotlin
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, Set and Map from MutableList and friends, and offers functions such as map, filter, groupBy, associateBy and sumOf.
Why
Transforming API data for display is mostly collection work, and read-only types stop other code from changing lists behind your back.
Kotlin
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 suspend function can pause without blocking a thread. Coroutines are started in a scope with launch or async, and cancelling the scope cancels everything started in it.
Why
Network and database calls are suspend functions. Structured concurrency, via viewModelScope or lifecycleScope, stops work automatically when the screen goes away.
Kotlin
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.Main for UI, Dispatchers.IO for blocking I/O and Dispatchers.Default for CPU work. withContext switches for a block.
Why
Blocking the main thread freezes the UI and, after about five seconds, triggers an "App Not Responding" dialog.
Kotlin
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 Flow is a cold stream of values produced by a suspend block and read with collect. A StateFlow is 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.
Kotlin
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 catalog gradle/libs.versions.toml keeps 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.
Kotlin
dependencies {
  implementation(platform(libs.androidx.compose.bom))
  implementation(libs.androidx.material3)
  implementation(libs.androidx.room.runtime)
  ksp(libs.androidx.room.compiler)
  testImplementation(libs.junit)
}
01 · Kotlin essentials1 / 8