Lesson 2 of 83 beginner

Collections, Lambdas, Scope Functions & Higher-Order Functions

Functional Kotlin patterns that senior Android devs use daily

Open interactive version (quiz + challenge)

Real-world analogy

Collections are your data containers, lambdas are mini-functions you can pass around like notes, scope functions are context-switchers that let you 'be inside' an object temporarily, and higher-order functions are managers who accept other functions as their instructions.

What is it?

Kotlin's collection API and functional operators let you write concise, readable data transformations. Scope functions (let/run/with/apply/also) are syntactic sugar that organize code around objects. Higher-order functions that accept lambdas form the backbone of coroutines, Flow, and modern Android APIs like setContent {} in Compose.

Real-world relevance

In a school management platform with Firebase + Room, you fetch a List from Room, use map to convert to List for display, filter for only enrolled students, groupBy for class section tabs, and fold to compute total attendance percentage. apply sets up the RecyclerView adapter, let handles nullable selected student in edit screen.

Key points

Code example

// Collection transformations — school management scenario
data class Student(
    val id: String,
    val name: String,
    val section: String,
    val attendanceRate: Double,
    val isEnrolled: Boolean
)

fun buildSectionMap(students: List<Student>): Map<String, List<Student>> =
    students
        .filter { it.isEnrolled }
        .groupBy { it.section }

fun averageAttendance(students: List<Student>): Double =
    if (students.isEmpty()) 0.0
    else students.fold(0.0) { acc, s -> acc + s.attendanceRate } / students.size

// Scope functions
fun setupAdapter(recyclerView: RecyclerView, students: List<Student>) {
    recyclerView.apply {
        layoutManager = LinearLayoutManager(context)
        adapter = StudentAdapter(students)
        setHasFixedSize(true)
    }
}

// let for null-safe navigation
fun editStudent(selectedStudent: Student?) {
    selectedStudent?.let { student ->
        val intent = Intent(context, EditStudentActivity::class.java).apply {
            putExtra("student_id", student.id)
        }
        startActivity(intent)
    }
}

// also for side-effect logging in a chain
fun processEnrollment(students: List<Student>): List<Student> =
    students
        .filter { it.isEnrolled }
        .also { enrolled -> Timber.d("Processing ${enrolled.size} enrolled students") }
        .sortedBy { it.name }

Line-by-line walkthrough

  1. 1. students.filter { it.isEnrolled } — creates a new list with only enrolled students; original list untouched
  2. 2. .groupBy { it.section } — transforms the filtered list into Map> keyed by section
  3. 3. students.fold(0.0) { acc, s -> acc + s.attendanceRate } — starts at 0.0, adds each student's rate; safe on empty lists
  4. 4. recyclerView.apply { ... } — 'this' inside is the recyclerView; returns recyclerView itself; perfect for initialization
  5. 5. layoutManager = LinearLayoutManager(context) — no 'recyclerView.' prefix needed inside apply; 'this' is implicit
  6. 6. selectedStudent?.let { student -> } — block only runs if selectedStudent is non-null; student is non-null inside
  7. 7. Intent(...).apply { putExtra(...) } — creates Intent and configures it in one expression; no temp variable needed
  8. 8. .also { enrolled -> Timber.d(...) } — side effect (logging) without interrupting the chain; returns the same list
  9. 9. .sortedBy { it.name } — terminal sort operation; returns a new sorted list; chain is now complete

Spot the bug

fun getTotalAmount(orders: List<Order>): Double {
    return orders.reduce { acc, order -> acc + order.amount }
}

fun getActiveUserNames(users: List<User>?): List<String> {
    return users.map { it.name }.filter { it.isActive }
}
Need a hint?
Bug 1: reduce throws on empty list. Bug 2: two issues — nullable list not handled, and filter condition is wrong
Show answer
Bug 1: Use fold(0.0) { acc, o -> acc + o.amount } — reduce throws EmptyCollectionException on empty list. Bug 2: users.map { } on a nullable List? won't compile — fix with users?.map { it.name } ?: emptyList(). Also, isActive is a property of User not String — filter must happen BEFORE map: users?.filter { it.isActive }?.map { it.name } ?: emptyList()

Explain like I'm 5

Imagine you have a bag of colored balls (your collection). map is painting each ball a new color. filter keeps only the balls you like. fold counts up the total weight of all the balls. The scope functions (let/apply/run/with/also) are like saying 'I'm going to stand inside this ball for a moment and work on it' — each one lets you do that in slightly different ways.

Fun fact

Kotlin's scope functions are so idiomatic that Android's own Jetpack libraries are built around them — ViewBinding uses apply{}, coroutine builders use trailing lambdas, and Compose's DSL is entirely built on trailing lambda / receiver lambda patterns.

Hands-on challenge

Given a List with fields: id, agentId, amount (Double), status (PENDING/APPROVED/REJECTED), write functions using only collection operators to: 1) Get total approved amount per agent as Map. 2) Partition into (needsReview: List, processed: List). 3) Find the top 3 agents by approved claim count. Use no loops — only map/filter/groupBy/fold/sortedByDescending/take.

More resources

Open interactive version (quiz + challenge) ← Back to course: Android Interview Mastery