Collections & Lambdas

TrailLog's whole reason for existing is a list of trails — so this lesson is about the tools Kotlin gives you for working with lists of things. We'll cover Kotlin's collection types and the read-only/mutable distinction that runs through all of them, lambda expressions (small, inline blocks of code you pass around like values), and the collection functions — filter, map, sortedBy, groupBy — that let you transform a List<Trail> into exactly the view you need, in a single readable chain.

Read-Only vs. Mutable Collections

Kotlin splits its collection interfaces into read-only and mutable versions. A plain List<Trail> exposes no way to add, remove, or replace elements — it's read-only as a reference. If you actually need to change the collection's contents, you reach for the explicitly named MutableList<Trail> instead:

val trails: List<Trail> = repository.getTrails()
// trails.add(...)   // compiler error — List has no add() function

val editableTrails: MutableList<Trail> = mutableListOf()
editableTrails.add(trails.first())  // fine — MutableList exposes add()

This is a deliberate design choice, not an accident: by making the mutable version something you have to explicitly opt into, Kotlin nudges you toward passing around read-only Lists everywhere by default, and only reaching for MutableList at the specific spot that actually needs to build or edit a collection. The same pattern applies to Map/ MutableMap and Set/MutableSet.

Lambda Expressions

A lambda is a small block of code you can pass around as a value — most often, as an argument to another function. Kotlin's syntax wraps the lambda in curly braces, with an optional list of parameters followed by an arrow:

val isLongTrail = { trail: Trail -> trail.distanceKm > 5.0 }

When a function's last parameter is a lambda — which is extremely common for collection functions — Kotlin lets you move it outside the parentheses entirely, and drop the parentheses altogether if the lambda is the only argument. This is called trailing-lambda syntax, and it's the form you'll see almost everywhere:

// Without trailing-lambda syntax:
val longTrails = trails.filter({ trail -> trail.distanceKm > 5.0 })

// With trailing-lambda syntax (idiomatic Kotlin):
val longTrails = trails.filter { trail -> trail.distanceKm > 5.0 }

Filtering: .filter { }

filter takes a lambda that returns true/false and produces a new list containing only the elements the lambda accepted:

val longTrails: List<Trail> = trails.filter { it.distanceKm > 5.0 }

Transforming: .map { }

map takes a lambda that transforms each element into something else, and produces a new list of the transformed values — the result list is the same size, but potentially a different type entirely:

val trailNames: List<String> = trails.map { it.name }

Both examples above use it — Kotlin's implicit name for a lambda's single parameter when you don't name one yourself. It's optional (you could write { trail -> trail.name } instead), but for short, single-argument lambdas like these, it is the idiomatic, widely-used default.

Sorting: .sortedBy { } and .sortedByDescending { }

These return a new list ordered by whatever key the lambda extracts:

val byDistance = trails.sortedBy { it.distanceKm }              // shortest first
val newestFirst = trails.sortedByDescending { it.date }         // most recent first

Grouping: .groupBy { }

groupBy turns a list into a Map, keyed by whatever the lambda returns — useful for something like grouping TrailLog's hikes by the month they were logged in:

val trailsByMonth: Map<Int, List<Trail>> = trails.groupBy { trail ->
    Calendar.getInstance().apply { timeInMillis = trail.date }.get(Calendar.MONTH)
}

Worked Example: Chaining a Realistic TrailLog Query

These functions are designed to be chained. Here's a single, realistic query against TrailLog's trail list: get only completed trails, sort the newest first, and keep just the five most recent — exactly the kind of query the trail-list screen's "Recent Hikes" section would run:

val recentCompletedTrails: List<Trail> = trails
    .filter { it.distanceKm > 0.0 }       // only trails that were actually completed
    .sortedByDescending { it.date }  // most recent first
    .take(5)                        // keep only the top 5

Read top to bottom, that chain reads almost like a sentence: "take the trails, keep the completed ones, sort them newest-first, take the top five." Each step returns a new list, which the next step operates on — no mutable accumulator variable, no manual loop, no intermediate bookkeeping.

Next up: Collections handle data you already have in memory. Coroutines Basics covers how TrailLog actually gets that list of trails in the first place, from a database or the network, without freezing the UI while it waits.