Techbase Academy
Android · Lesson 14

Collections and Higher-Order Functions

12 phasesClass Results AnalyserAndroid Studio
Phase 1 of 12
Lesson Introduction

Welcome back! So far your programs have held one value at a time: one name, one price, one score. Real apps never work like that. A messaging app holds hundreds of messages, a shop app holds a list of products, and a school app holds a register of students. Today you learn how Kotlin stores groups of values and, more importantly, how to ask questions of those groups in one or two lines: Which students passed? What is the total bill? Which items start with the letter B?

Think of a market woman arranging her goods. A list is the neat row of tins on her shelf, in order, where two tins can be the same. A set is the collection of different kinds of pepper she sells, with no repeats. A map is her price board: tomatoes: ₦800, onions: ₦500, a name that points to a value. Kotlin has all three built in.

By the end of this lesson you will be able to:

  • Create and read List, Set and Map collections.
  • Explain the difference between read-only and mutable collections, and choose between them.
  • Loop through collections with for, forEach, forEachIndexed and destructuring.
  • Transform data with map, filter, flatMap, distinct, take and drop.
  • Summarise data with sum, average, count, fold and reduce.
  • Organise data with groupBy, associate, sortedBy and partition.
  • Test collections with any, all, none, find and firstOrNull.
  • Explain what a sequence is and when it saves work.
  • Combine data classes with collections to solve a realistic problem.

How this fits the bigger picture: This lesson covers three items from Unit 3 of Google's Android Basics with Compose course: Use collections in Kotlin, Higher-order functions with collections and the Practice: Classes and Collections set. We go slower, explain the reasoning behind each function, and give you extra exercises. Official links are at the end.

Why collections matter for Android

In lesson 21 you will show a scrollable list of cards on screen. The list on screen is simply a Kotlin List that Compose turns into rows. Every real feature in this course, from a shopping cart to chat messages to database results, arrives as a collection. If you master the functions in this lesson, the rest of the course becomes much easier.

✏️ Your Task
Practise what you just learned about Lesson Introduction. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 2 of 12
Prerequisite Concepts

You should be comfortable with these ideas. Each has a short refresher.

1. Variables: val and var (lesson 07)

val creates a label that cannot be pointed at something else later. var creates a label that can. A collection stored in a val is still the same collection, but, as you will see, whether you can change its contents depends on the type of collection, not on val or var.

2. Functions and lambdas (lessons 08 and 12)

A lambda is a small function written in braces and passed around like a value: { x -> x * 2 }. When a lambda has exactly one parameter you may skip naming it and use it: { it * 2 }. When the lambda is the last argument of a function you may write it outside the parentheses (trailing lambda syntax), and if it is the only argument you may drop the parentheses entirely. This is why list.filter { it > 3 } looks the way it does. A function that takes another function as a parameter is called a higher-order function, which is the second half of this lesson's title.

3. Classes and data classes (lesson 11)

A data class is a class that mainly holds values, written like this:

kotlin
data class Student(val name: String, val score: Int)

Kotlin automatically gives data classes a readable toString(), so println(Student("Ade", 70)) prints Student(name=Ade, score=70). We use data classes throughout this lesson.

4. Nullability (lesson 10)

Some collection functions return a value that might not exist, such as the first item of an empty list. Kotlin represents "might not exist" with a nullable type such as Int?. You handle it with ?., ?: or a null check.

✏️ Your Task
Practise what you just learned about Prerequisite Concepts. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 3 of 12
Conceptual Understanding: Three Kinds of Collection

Kotlin's standard library has three main collection types. The big question to ask when choosing one is: "What do I need to do with the data?"

Type Order matters? Duplicates allowed? Look up by Everyday picture
List<T> Yes, each item has a position (index) starting at 0 Yes Position: list[2] A queue at the bank: Ada is 1st, Tunde is 2nd
Set<T> No guaranteed meaning (see note below) No, each item appears once Membership: "is it in there?" The set of states a learner has visited
Map<K, V> By key, not position Keys are unique; values may repeat Key: map["rice"] A price board or a phone contacts book

Note on set order: setOf and mutableSetOf in practice remember the order you added items (they use a LinkedHashSet behind the scenes). Do not depend on this in your design. Treat a set as "a bag of unique things".

Read-only versus mutable

Every collection type comes in two flavours.

Read-only Mutable (can change) Create with
List MutableList listOf(...) / mutableListOf(...)
Set MutableSet setOf(...) / mutableSetOf(...)
Map MutableMap mapOf(...) / mutableMapOf(...)

A read-only collection has no add or remove functions at all. You can look, but not touch. This is a safety feature: if you give a read-only list to another part of the program, you know it cannot secretly change it. (More precisely, List is a read-only view; if someone else holds a mutable reference to the same underlying list it can still change. For beginners the practical rule is: prefer read-only, switch to mutable only when you truly need to add, remove or replace items.)

text
        val shelf = listOf("tin A", "tin B")        val trolley = mutableListOf("tin A")
        +-------+-------+                           +-------+
        | tin A | tin B |   look only               | tin A |  add, remove, replace
        +-------+-------+                           +-------+

How the higher-order functions fit together

Almost every collection question is one of a few shapes. Learn the shapes and you can pick the right function every time:

text
 INPUT collection ──► FUNCTION ──► OUTPUT

 [a, b, c]   map { transform }    ──► [a', b', c']       same size, each item changed
 [a, b, c]   filter { test }      ──► [a, c]             fewer items, same items
 [a, b, c]   fold / sum / count   ──► one single value
 [a, b, c]   groupBy { key }      ──► {k1=[a, c], k2=[b]} a Map of Lists
 [a, b, c]   sortedBy { key }     ──► [c, a, b]          same items, new order
 [a, b, c]   any / all / none     ──► true or false

Think about it: You have a list of 40 exam scores and you want the average of the ones that passed. Which shapes do you need, and in what order? (First filter to keep the passes, then a single-value summary: average.)

✏️ Your Task
Practise what you just learned about Conceptual Understanding: Three Kinds of Collection. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 4 of 12
Worked Examples: Lists

Open the Kotlin Playground (play.kotlinlang.org) as you did in lesson 06 and type each example. We will predict the output before running it. That habit will make you a better programmer than any trick.

Example 1: A read-only list

kotlin
fun main() {
 val fruits = listOf("mango", "orange", "pawpaw", "banana")
 println(fruits)
 println("Count: ${fruits.size}")
 println("First item: ${fruits[0]}")
 println("Last item: ${fruits.last()}")
 println("Third item: ${fruits.getOrNull(2)}")
 println("Tenth item: ${fruits.getOrNull(9)}")
 println("Has orange? ${"orange" in fruits}")
 println("Position of pawpaw: ${fruits.indexOf("pawpaw")}")
}

Expected output:

[mango, orange, pawpaw, banana]
Count: 4
First item: mango
Last item: banana
Third item: pawpaw
Tenth item: null
Has orange? true
Position of pawpaw: 2

Explanation:

  • Positions are called indexes and they start at 0. So fruits[0] is the first item and the last valid index of a list of four is 3.
  • fruits[9] would crash with an IndexOutOfBoundsException because there is no item 9. getOrNull(9) is the safe version: it returns null instead of crashing. Use it whenever the index comes from user input.
  • in asks "is this value contained here?" and gives true or false.

Example 2: A mutable list

kotlin
fun main() {
 val cart = mutableListOf("rice", "beans")
 cart.add("garri")
 println(cart)
 cart.add(1, "yam")
 println(cart)
 cart.remove("beans")
 println(cart)
 cart.removeAt(0)
 println(cart)
 cart[0] = "plantain"
 println(cart)
 println("Is the cart empty? ${cart.isEmpty()}")
}

Expected output:

[rice, beans, garri]
[rice, yam, beans, garri]
[rice, yam, garri]
[yam, garri]
[plantain, garri]
Is the cart empty? false

Explanation: Follow the cart line by line. add("garri") puts the item at the end. add(1, "yam") inserts at index 1 and pushes the rest along. remove("beans") removes the first item equal to "beans". removeAt(0) removes whatever is at position 0 (rice). cart[0] = "plantain" replaces the item at index 0 (yam).

Notice that cart is declared with val, yet we changed its contents. val only stops you pointing cart at a different list. It does not freeze the list.

Example 3: Sets throw away duplicates

kotlin
fun main() {
 val scores = listOf(1, 2, 2, 3, 3, 3)
 val unique = scores.toSet()
 println(scores.size)
 println(unique)
 println(unique.size)
 val tags = mutableSetOf("kotlin", "android")
 println(tags.add("compose"))
 println(tags.add("kotlin"))
 println(tags)
 val a = setOf(1, 2, 3, 4)
 val b = setOf(3, 4, 5)
 println(a union b)
 println(a intersect b)
 println(a subtract b)
}

Expected output:

6
[1, 2, 3]
3
true
false
[kotlin, android, compose]
[1, 2, 3, 4, 5]
[3, 4]
[1, 2]

Explanation: Turning a list into a set with toSet() is the quickest way to remove duplicates. add on a set returns true if the item was new and false if it was already there, so adding "kotlin" a second time changes nothing. union combines two sets, intersect keeps only what both share, and subtract keeps what is in the first but not the second. Those three words are written between the two sets (this style is called infix notation).

Example 4: Maps hold key-value pairs

kotlin
fun main() {
 val prices = mapOf("rice" to 1500, "beans" to 1200, "garri" to 800)
 println(prices)
 println("Rice costs ₦${prices["rice"]}")
 println("Bread costs ${prices["bread"]}")
 println("Bread or default: ${prices.getOrDefault("bread", 0)}")
 println(prices.keys)
 println(prices.values)
 println("Has beans? ${"beans" in prices}")
 for ((item, price) in prices) {
 println("$item costs ₦$price")
 }
}

Expected output:

{rice=1500, beans=1200, garri=800}
Rice costs ₦1500
Bread costs null
Bread or default: 0
[rice, beans, garri]
[1500, 1200, 800]
Has beans? true
rice costs ₦1500
beans costs ₦1200
garri costs ₦800

Explanation:

  • "rice" to 1500 creates a pair. mapOf takes pairs.
  • prices["bread"] returns null because there is no such key. The result type is Int? (a nullable Int), which is why Kotlin forces you to think about the missing case.
  • for ((item, price) in prices) uses destructuring: each entry is split into its key and value so you can name them directly.

Changing a mutable map:

kotlin
fun main() {
 val stock = mutableMapOf("rice" to 10, "beans" to 4)
 stock["yam"] = 7
 stock["beans"] = 9
 stock.remove("rice")
 println(stock)
}

{beans=9, yam=7}

stock["yam"] = 7 adds a new key. stock["beans"] = 9 replaces the existing value because keys are unique. remove("rice") deletes the whole entry.

✏️ Your Task
Practise what you just learned about Worked Examples: Lists. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 5 of 12
Worked Examples: Looping Through Collections

You already know for loops from lesson 09. They work directly on collections.

kotlin
fun main() {
 val scores = listOf(55, 72, 38, 91, 64)
 for (s in scores) {
 println("Score: $s")
 }
 scores.forEach { println("Again: $it") }
 scores.forEachIndexed { index, s ->
 println("Student ${index + 1} scored $s")
 }
 for ((index, s) in scores.withIndex()) {
 if (s < 50) println("Student ${index + 1} needs help")
 }
}

Expected output:

Score: 55
Score: 72
Score: 38
Score: 91
Score: 64
Again: 55
Again: 72
Again: 38
Again: 91
Again: 64
Student 1 scored 55
Student 2 scored 72
Student 3 scored 38
Student 4 scored 91
Student 5 scored 64
Student 3 needs help

Explanation: forEach takes a lambda and runs it once per item. forEachIndexed also hands you the index. withIndex() gives you the index inside a normal for loop. Use whichever reads best; in a for loop you can use break and continue, which you cannot use inside a forEach lambda.

✏️ Your Task
Practise what you just learned about Worked Examples: Looping Through Collections. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 6 of 12
Higher-Order Functions: Transforming and Filtering

A higher-order function takes a function (a lambda) as input. Collections have dozens of them. Instead of writing a loop with a temporary list, you describe what you want.

map: change every item

kotlin
fun main() {
 val scores = listOf(55, 72, 38, 91, 64)
 val curved = scores.map { it + 5 }
 println(curved)
 val names = listOf("ade", "bola", "chidi")
 println(names.map { it.uppercase() })
 println(names.map { "Hello, $it!" })
 println(names.map { it.length })
}

Expected output:

[60, 77, 43, 96, 69]
[ADE, BOLA, CHIDI]
[Hello, ade!, Hello, bola!, Hello, chidi!]
[3, 4, 5]

map always returns a list with the same number of items. The original list is never changed; map builds a new one.

filter: keep only the items that pass a test

The lambda must return true (keep) or false (drop).

kotlin
fun main() {
 val scores = listOf(55, 72, 38, 91, 64)
 println(scores.filter { it >= 50 })
 println(scores.filterNot { it >= 50 })
 println(scores.filter { it % 2 == 0 })
 println(scores.filter { it >= 50 }.map { it / 10 })
}

Expected output:

[55, 72, 91, 64]
[38]
[72, 38, 64]
[5, 7, 9, 6]

The last line is a chain. First filter keeps [55, 72, 91, 64], then map divides each by 10 using whole-number division: 55/10 = 5, 72/10 = 7, 91/10 = 9, 64/10 = 6. Chaining is how you build real queries: read it left to right like a sentence.

Quick helpers: take, drop, distinct, flatMap, zip

kotlin
fun main() {
 val scores = listOf(55, 72, 38, 91, 64)
 println(scores.take(2))
 println(scores.drop(3))
 println(listOf(3, 1, 3, 2, 1).distinct())
 val classes = listOf(listOf("Ade", "Bola"), listOf("Chidi"), listOf("Dayo", "Efe"))
 println(classes.flatMap { it })
 println(classes.map { it.size })
 val names = listOf("Ade", "Bola", "Chidi")
 val marks = listOf(70, 85, 60)
 println(names.zip(marks))
}

Expected output:

[55, 72]
[91, 64]
[3, 1, 2]
[Ade, Bola, Chidi, Dayo, Efe]
[2, 1, 2]
[(Ade, 70), (Bola, 85), (Chidi, 60)]

  • take(n) keeps the first n; drop(n) skips the first n.
  • distinct() removes repeats, keeping the first occurrence.
  • flatMap flattens a list of lists into one list. map { it } would keep the nesting; flatMap removes one level.
  • zip pairs up items from two lists by position.
✏️ Your Task
Practise what you just learned about Higher-Order Functions: Transforming and Filtering. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 7 of 12
Higher-Order Functions: Summarising, Grouping and Sorting

Summaries: sum, average, count, max, min

kotlin
fun main() {
 val scores = listOf(55, 72, 38, 91, 64)
 println("Total: ${scores.sum()}")
 println("Average: ${scores.average()}")
 println("Highest: ${scores.max()}")
 println("Lowest: ${scores.min()}")
 println("Passed: ${scores.count { it >= 50 }}")
}

Expected output:

Total: 320
Average: 64.0
Highest: 91
Lowest: 38
Passed: 4

average() always gives a Double, so 320 / 5 shows as 64.0. On an empty list, max() and min() throw an exception in current Kotlin; use maxOrNull() and minOrNull() when the list might be empty. (Older tutorials used max() to return a nullable value; that changed in Kotlin 1.7, so check which one your tutorial means.)

fold and reduce: combine everything into one value

sum is really a special case of a more general tool. fold takes a starting value and a function that combines the running total (the accumulator) with each item in turn.

For listOf(1, 2, 3, 4).fold(10) { acc, n -> acc + n } trace it like this:

Step acc before item n acc after
1 10 1 11
2 11 2 13
3 13 3 16
4 16 4 20
kotlin
fun main() {
 val nums = listOf(1, 2, 3, 4)
 println(nums.fold(10) { acc, n -> acc + n })
 println(nums.reduce { acc, n -> acc * n })
 val prices = listOf(1500, 1200, 800)
 val receipt = prices.fold("Items:") { text, p -> "$text ₦$p" }
 println(receipt)
}

Expected output:

20
24
Items: ₦1500 ₦1200 ₦800

reduce is like fold but uses the first item as the starting value, so it has no separate initial value: 1 × 2 × 3 × 4 = 24. Because of that, reduce throws an exception on an empty list, while fold just returns its starting value. When in doubt, use fold. Notice also that fold can produce a different type from the items: here numbers go in and a String comes out.

groupBy: sort items into labelled boxes

kotlin
fun main() {
 val names = listOf("ade", "bola", "ayo", "bisi", "chidi", "bayo")
 val byLetter = names.groupBy { it.first() }
 println(byLetter)
 val sizes = names.groupBy { it.length }.mapValues { it.value.size }
 println(sizes)
}

Expected output:

{a=[ade, ayo], b=[bola, bisi, bayo], c=[chidi]}
{3=2, 4=3, 5=1}

groupBy returns a Map whose keys come from the lambda and whose values are lists of the items that produced that key. The second line turns each list into its size with mapValues: two names have 3 letters (ade, ayo), three have 4 (bola, bisi, bayo) and one has 5 (chidi).

associate, associateBy and associateWith: build a Map from a list

kotlin
fun main() {
 val names = listOf("ade", "bola", "chidi")
 println(names.associateWith { it.length })
 println(names.associateBy { it.first() })
 println(names.associate { it to it.uppercase() })
}

Expected output:

{ade=3, bola=4, chidi=5}
{a=ade, b=bola, c=chidi}
{ade=ADE, bola=BOLA, chidi=CHIDI}

  • associateWith : the item is the key, the lambda gives the value.
  • associateBy : the lambda gives the key, the item is the value.
  • associate : the lambda returns a full pair.

Keys must be unique. If two items produce the same key, the later one overwrites the earlier one, silently. Use groupBy when keys may repeat.

Sorting

kotlin
fun main() {
 val names = listOf("ade", "bola", "ayo", "bisi", "chidi", "bayo")
 println(names.sorted())
 println(names.sortedDescending())
 println(names.sortedBy { it.length })
 println(names.sortedByDescending { it.length })
}

Expected output:

[ade, ayo, bayo, bisi, bola, chidi]
[chidi, bola, bisi, bayo, ayo, ade]
[ade, ayo, bola, bisi, bayo, chidi]
[chidi, bola, bisi, bayo, ade, ayo]

sorted() uses the natural order (alphabetical for text, smallest-first for numbers). sortedBy sorts by whatever the lambda returns. The sort is stable: items that tie keep their original relative order. So for length 4, bola, bisi, bayo stay in the order they appeared. Sorting never changes the original list. It returns a new one. (The mutable list has sort() and sortBy {} that change the list in place, but you will rarely need them.)

partition: split in two

kotlin
fun main() {
 val scores = listOf(55, 72, 38, 91, 64)
 val (passed, failed) = scores.partition { it >= 50 }
 println("Passed: $passed")
 println("Failed: $failed")
}

Expected output:

Passed: [55, 72, 91, 64]
Failed: [38]

partition returns a pair of lists, and destructuring names them right away.

Questions that return true or false: any, all, none, and finding one item

kotlin
fun main() {
 val scores = listOf(55, 72, 38, 91, 64)
 println(scores.any { it > 90 })
 println(scores.all { it > 40 })
 println(scores.none { it > 100 })
 println(scores.find { it > 60 })
 println(scores.firstOrNull { it > 95 })
 println(scores.isNotEmpty())
}

Expected output:

true
false
true
72
null
true

any is "does at least one pass?", all is "do all pass?" and none is "does nobody pass?". find (a friendlier name for firstOrNull) returns the first matching item or null. Because it can be null the result type is Int?, so combine it with ?: when you need a fallback: scores.find { it > 95 } ?: 0.

Higher-order functions on Maps

kotlin
fun main() {
 val prices = mapOf("rice" to 1500, "beans" to 1200, "garri" to 800)
 println(prices.filter { it.value > 1000 })
 println(prices.mapValues { it.value + 100 })
 println(prices.values.sum())
 println(prices.maxByOrNull { it.value }?.key)
 println(prices.toList().sortedBy { it.second })
}

Expected output:

{rice=1500, beans=1200}
{rice=1600, beans=1300, garri=900}
3500
rice
[(garri, 800), (beans, 1200), (rice, 1500)]

On a map, each lambda item is an entry with .key and .value. maxByOrNull finds the entry whose value is largest; ?.key safely reads its key. toList() turns a map into a list of pairs, which has .first and .second, ready for sorting.

✏️ Your Task
Practise what you just learned about Higher-Order Functions: Summarising, Grouping and Sorting. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 8 of 12
Sequences: Doing Less Work

Every function above creates a brand new list at each step. For a chain of three operations on a small list that is perfectly fine. For huge data, building a full list at every step wastes time and memory.

A sequence works lazily: it processes one item at a time through the whole chain and only does the work when a final result is requested. Convert a collection with asSequence(). Let us watch the difference by printing inside the lambdas.

kotlin
fun main() {
 val nums = listOf(1, 2, 3, 4)
 println("--- List ---")
 val eager = nums
 .map { println("map $it"); it * 2 }
 .filter { println("filter $it"); it > 4 }
 println(eager)
 println("--- Sequence ---")
 val lazy = nums.asSequence()
 .map { println("map $it"); it * 2 }
 .filter { println("filter $it"); it > 4 }
 .toList()
 println(lazy)
 println("--- Sequence with first() ---")
 val firstHit = nums.asSequence()
 .map { println("map $it"); it * 2 }
 .filter { println("filter $it"); it > 4 }
 .first()
 println(firstHit)
}

Expected output:

--- List ---
map 1
map 2
map 3
map 4
filter 2
filter 4
filter 6
filter 8
[6, 8]
--- Sequence ---
map 1
filter 2
map 2
filter 4
map 3
filter 6
map 4
filter 8
[6, 8]
--- Sequence with first() ---
map 1
filter 2
map 2
filter 4
map 3
filter 6
6

What this shows: the list version finishes all the map work before any filter starts. The sequence passes each item through the whole chain before touching the next one. With first() the sequence stops as soon as it has an answer (after item 3), never even looking at item 4. Nothing runs at all until a terminal operation such as toList(), first() or sum() is called.

When to use sequences: very large collections, long chains, or when you only need the first few results. For short lists of the sort you see on a phone screen, plain lists are simpler and often just as fast. Start with lists and reach for asSequence() when you have a reason.

✏️ Your Task
Practise what you just learned about Sequences: Doing Less Work. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 9 of 12
Practice: Classes and Collections

The official Practice: Classes and Collections set asks you to combine classes with the collection functions you just learned. Here is an original set in the same spirit. We will use one data class throughout: a school register.

kotlin
data class Student(val name: String, val house: String, val score: Int)
val register = listOf(
 Student("Ade", "Blue", 72),
 Student("Bola", "Red", 55),
 Student("Chidi", "Blue", 38),
 Student("Dayo", "Green", 91),
 Student("Efe", "Red", 64),
 Student("Funmi", "Green", 49)
)

Try each problem on paper first, then check against the worked solution. Scores are out of 100 and the pass mark is 50.

  1. Print the names of all students who passed.
  2. Print the average score of the whole class.
  3. Print the name of the top student.
  4. Print the number of students in each house.
  5. Print the names, sorted from highest score to lowest.
  6. Print true or false: did every house have at least one student who passed?

The full solutions are in the first exercise below.

✏️ Your Task
Practise what you just learned about Practice: Classes and Collections. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 10 of 12
Guided Practice Exercises
🎯 Your Challenge

Exercise 1: The school register

Create Student and register exactly as shown in the practice set above, and write a main function that solves all six problems. Use one collection function per problem where you can.

kotlin
data class Student(val name: String, val house: String, val score: Int)
fun main() {
 val register = listOf(
 Student("Ade", "Blue", 72),
 Student("Bola", "Red", 55),
 Student("Chidi", "Blue", 38),
 Student("Dayo", "Green", 91),
 Student("Efe", "Red", 64),
 Student("Funmi", "Green", 49)
 )
 // 1. Names of students who passed
 println(register.filter { it.score >= 50 }.map { it.name })
 // 2. Average score
 println(register.map { it.score }.average())
 // 3. Top student
 println(register.maxByOrNull { it.score }?.name)
 // 4. Students per house
 println(register.groupBy { it.house }.mapValues { it.value.size })
 // 5. Names from highest score to lowest
 println(register.sortedByDescending { it.score }.map { it.name })
 // 6. Did every house have at least one student who passed?
 val everyHousePassed = register
 .groupBy { it.house }
 .all { (_, members) -> members.any { it.score >= 50 } }
 println(everyHousePassed)
}

Expected output:

[Ade, Bola, Dayo, Efe]
61.5
Dayo
{Blue=2, Red=2, Green=2}
[Dayo, Ade, Efe, Bola, Funmi, Chidi]
true

Reasoning through it: the passes are Ade 72, Bola 55, Dayo 91 and Efe 64 (Chidi 38 and Funmi 49 failed). The scores add up to 72 + 55 + 38 + 91 + 64 + 49 = 369, and 369 / 6 = 61.5. Sorted by score the order is 91, 72, 64, 55, 49, 38. For problem 6, Blue has Ade (72), Red has Bola and Efe, and Green has Dayo, so every house has a pass. In all { (_, members) -> ... } the underscore means "I do not need the key".

🎯 Your Challenge

Exercise 2: Shopping basket report

You have this basket, where each item has a name, a unit price in naira and a quantity:

kotlin
data class Item(val name: String, val price: Int, val quantity: Int)

with Item("rice", 1500, 2), Item("beans", 1200, 1), Item("garri", 800, 3), Item("yam", 2000, 1).

Print: (a) the total cost of the basket, (b) the names of items that cost more than ₦1000 per unit, sorted alphabetically, (c) a map from item name to its line total (price times quantity), and (d) the name of the most expensive line.

kotlin
data class Item(val name: String, val price: Int, val quantity: Int)
fun main() {
 val basket = listOf(
 Item("rice", 1500, 2),
 Item("beans", 1200, 1),
 Item("garri", 800, 3),
 Item("yam", 2000, 1)
 )
 val total = basket.sumOf { it.price * it.quantity }
 println("Total: ₦$total")
 val pricey = basket.filter { it.price > 1000 }.map { it.name }.sorted()
 println(pricey)
 val lineTotals = basket.associate { it.name to it.price * it.quantity }
 println(lineTotals)
 val biggest = basket.maxByOrNull { it.price * it.quantity }
 println("Biggest line: ${biggest?.name}")
}

Expected output:

Total: ₦8600
[beans, rice, yam]
{rice=3000, beans=1200, garri=2400, yam=2000}
Biggest line: rice

Line totals: rice 1500 x 2 = 3000, beans 1200, garri 800 x 3 = 2400, yam 2000. They add up to 3000 + 1200 + 2400 + 2000 = 8600. sumOf { } is the tidy way to add up a value computed from each item. The items above ₦1000 per unit are rice, beans and yam, and alphabetically that is beans, rice, yam.

🎯 Your Challenge

Exercise 3: Word counter

Given val text = "to be or not to be that is the question", print how many times each word appears, as a map, and then print the word that appears most often. (Hint: split(" ") turns text into a list of words; then groupBy and mapValues.)

kotlin
fun main() {
 val text = "to be or not to be that is the question"
 val counts = text.split(" ")
 .groupBy { it }
 .mapValues { it.value.size }
 println(counts)
 val top = counts.maxByOrNull { it.value }
 println("Most common: ${top?.key} (${top?.value} times)")
}

Expected output:

{to=2, be=2, or=1, not=1, that=1, is=1, the=1, question=1}
Most common: to (2 times)

groupBy { it } uses the word itself as the key, so identical words land in the same list. to and be both appear twice; maxByOrNull returns the first entry with the largest value, and to comes first.

✏️ Your Task

In the Playground, create a list of at least eight numbers of your own. Using chained functions, print: the numbers greater than your list's average, the same numbers doubled, and whether any number is divisible by 7. Predict each result on paper before running.

Phase 11 of 12
Common Beginner Mistakes (and How to Fix Them)

Mistake #1: Trying to add to a read-only list. The wrong code: val items = listOf("a"); items.add("b") The problem: List has no add function, so you get Unresolved reference: add. The fix: declare it with mutableListOf, or build a new list with val more = items + "b". The + operator returns a new list and leaves the old one alone.

Mistake #2: Using an index that does not exist. The wrong code: val list = listOf(1, 2, 3); println(list[3]) The problem: valid indexes are 0, 1 and 2, so this crashes with IndexOutOfBoundsException. The fix: use getOrNull(3) or check list.lastIndex first. A list of size n has last index n - 1.

Mistake #3: Using first() on a list that might be empty. The wrong code: emptyList<Int>().first() The problem: throws NoSuchElementException. The fix: firstOrNull(), then handle null with ?:.

Mistake #4: Expecting map or sorted to change the original. The wrong code: scores.sorted(); println(scores) The problem: sorted() returns a new list; the original is untouched, so the output is the old order. The fix: store the result: val ordered = scores.sorted().

Mistake #5: Confusing map and forEach. The wrong code: val doubled = scores.forEach { it * 2 } The problem: forEach returns Unit (nothing useful). The fix: use map to produce a new list, and forEach only to do something with each item, such as printing.

Mistake #6: Duplicate keys in associateBy. The wrong code: names.associateBy { it.first() } on ["ade", "ayo"] The problem: both have the key a, so only ayo survives. The fix: use groupBy when keys can repeat.

Mistake #7: Forgetting that reduce fails on an empty list. The fix: use fold(initialValue), or sum() for plain totals.

Mistake #8: Writing it inside a nested lambda and getting the wrong item. The wrong code: groups.map { it.map { it * 2 } } The problem: the inner it hides the outer it, which is confusing to read. The fix: name the parameters: groups.map { group -> group.map { n -> n * 2 } }.

✏️ Your Task
Practise what you just learned about Common Beginner Mistakes (and How to Fix Them). Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 12 of 12
Knowledge Check

Answer each question in your head (or on paper), then reveal the answer.

Question 1. What is the difference between listOf and mutableListOf?

listOf creates a read-only List: you can read items but there are no add or remove functions. mutableListOf creates a MutableList whose contents you can add to, remove from and replace.

Question 2. What does this print? println(listOf(4, 8, 15, 16).filter { it % 2 == 0 }.map { it / 2 })

[2, 4, 8]. The filter keeps the even numbers [4, 8, 16] (15 is odd), then map halves each one.

Question 3. Why is val scores = mutableListOf(1, 2) followed by scores.add(3) allowed even though scores is a val?

val means the label scores cannot be pointed at a different list. It does not make the list itself unchangeable. Whether the contents can change depends on the collection type (MutableList here).

Question 4. What does listOf("a", "bb", "cc", "d").groupBy { it.length } return?

A map from length to the items of that length: {1=[a, d], 2=[bb, cc]}.

Question 5. Give one reason to use a sequence instead of a list, and one situation where a plain list is fine.

A sequence processes items lazily, one at a time through the whole chain, so it avoids building intermediate lists and can stop early (for example with first()). It helps with very large collections or long chains. For short lists, such as the handful of items shown on a phone screen, a plain list is simpler and fast enough.

Go deeper: official resources

✏️ Your Task
Practise what you just learned about Knowledge Check. Open Android Studio, type the examples by hand, change a value, and observe what happens.
🏗️ Build It — Mini Project
Class Results Analyser

Project goal: Build a small console program that analyses the results of a class and prints a tidy report. This is the kind of logic that will later sit behind a real school app.

Requirements:

  1. Create a data class Result(val student: String, val subject: String, val score: Int).
  2. Create a list of at least ten results covering at least three students and three subjects (every student should have a score in every subject).
  3. Print each student's average score across subjects.
  4. Print each subject's highest score and who achieved it.
  5. Use partition to print who passed (average of 50 or more) and who did not.
  6. Print the students ranked from the highest average to the lowest, with their position (1, 2, 3 ...).
  7. Use at least one of groupBy, associate, fold and any/all.

Stretch goal: Add a mutable map that records each student's grade letter (A for 70 or more, B for 60 to 69, C for 50 to 59, F below 50) using when, and print how many students got each grade.

Main.kt
data class Result(val student: String, val subject: String, val score: Int)
fun gradeFor(average: Double): String = when {
 average >= 70 -> "A"
 average >= 60 -> "B"
 average >= 50 -> "C"
 else -> "F"
}
fun main() {
 val results = listOf(
 Result("Ade", "Maths", 80), Result("Ade", "English", 60), Result("Ade", "Science", 70),
 Result("Bola", "Maths", 45), Result("Bola", "English", 55), Result("Bola", "Science", 40),
 Result("Chidi", "Maths", 90), Result("Chidi", "English", 75), Result("Chidi", "Science", 85)
 )
 // Average per student
 val averages: Map<String, Double> = results
 .groupBy { it.student }
 .mapValues { (_, rows) -> rows.map { it.score }.average() }
 println("=== AVERAGES ===")
 averages.forEach { (student, avg) -> println("$student: $avg") }
 // Best score per subject
 println("=== TOP IN EACH SUBJECT ===")
 results.groupBy { it.subject }.forEach { (subject, rows) ->
 val best = rows.maxByOrNull { it.score }
 println("$subject: ${best?.student} (${best?.score})")
 }
 // Pass and fail
 val (passed, failed) = averages.toList().partition { it.second >= 50 }
 println("=== PASSED ===")
 println(passed.map { it.first })
 println("=== NOT PASSED ===")
 println(failed.map { it.first })
 // Ranking
 println("=== RANKING ===")
 averages.toList()
 .sortedByDescending { it.second }
 .forEachIndexed { index, (student, avg) ->
 println("${index + 1}. $student ($avg)")
 }
 // Stretch: grade counts
 val gradeCounts = mutableMapOf<String, Int>()
 for ((_, avg) in averages) {
 val grade = gradeFor(avg)
 gradeCounts[grade] = (gradeCounts[grade] ?: 0) + 1
 }
 println("=== GRADES ===")
 println(gradeCounts)
}

Expected output:

=== AVERAGES ===
Ade: 70.0
Bola: 46.666666666666664
Chidi: 83.33333333333333
=== TOP IN EACH SUBJECT ===
Maths: Chidi (90)
English: Chidi (75)
Science: Chidi (85)
=== PASSED ===
[Ade, Chidi]
=== NOT PASSED ===
[Bola]
=== RANKING ===
1. Chidi (83.33333333333333)
2. Ade (70.0)
3. Bola (46.666666666666664)
=== GRADES ===
{A=2, F=1}

Notes on the solution: Ade's average is (80 + 60 + 70) / 3 = 70.0, Bola's is (45 + 55 + 40) / 3 = 46.67 and Chidi's is (90 + 75 + 85) / 3 = 83.33. The long decimals are normal for floating-point numbers; in a real app you would round them for display, for example with "%.1f".format(avg). The (student, avg) in forEachIndexed is destructuring of the pair. The grade map uses gradeCounts[grade] ?: 0 to start a count at zero the first time a grade appears. Because the order of grades follows the order the students are processed (Ade first, then Bola, then Chidi), the map prints {A=2, F=1}: Ade creates A, Bola creates F, Chidi increments A; map printing follows insertion order, so the keys appear as A then F.

1

Open your finished project in Android Studio. Check that the .gitignore file the wizard created is in the project root. It keeps local.properties, build folders and keystores out of Git. Never commit passwords or signing keys.

2

Choose Git > Create Git Repository… (or VCS > Enable Version Control Integration in older versions), select the project folder, then open Git > Commit, tick every file, and write the message “Add Android lesson 14 project”.

3

On github.com click New repository, name it android-lesson-14-collections-and-higher-order-functions, set it to Public, leave Add a README unticked, and click Create repository.

4

Back in Android Studio choose Git > Manage Remotes…, add the repository URL as origin, then choose Git > Push… and push your main branch. Prefer the terminal? Run git remote add origin <your-url> then git push -u origin main.

5

Add a README.md with a screenshot of your app running and a short description. Employers and clients read READMEs first. Your project is now part of your public portfolio. 🎉

Lesson 14 complete! 🎉

You covered:

  • Lesson Introduction
  • Prerequisite Concepts
  • Conceptual Understanding: Three Kinds of Collection
  • Worked Examples: Lists
  • Worked Examples: Looping Through Collections
  • Higher-Order Functions: Transforming and Filtering
  • Higher-Order Functions: Summarising, Grouping and Sorting
  • Sequences: Doing Less Work
  • Practice: Classes and Collections
  • Guided Practice Exercises
  • Common Beginner Mistakes (and How to Fix Them)
  • Knowledge Check
All 72 lessons in Google Android Studio ·Techbase Academy ·© 2025–2026 Techbase Consultant Services, Ibadan
Esc
↑ ↓ to moveEnter to openEsc to close