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.