Field Note · android / mobile-architecture

Sequences vs Collections in Kotlin

A reproducible way to decide when Kotlin sequences help, when collections are clearer, and when the difference does not matter.

Kotlin sequences are useful when a pipeline has enough work for intermediate allocations and eager passes to matter. They are not a performance badge. For a short list or one transformation, a collection is often clearer and just as fast.

What changes

A collection pipeline materializes the result after each filter or map. A sequence builds a lazy pipeline and visits each element through the operations when a terminal operation, such as toList(), runs. That can reduce intermediate allocations and stop earlier when the terminal operation short-circuits.

The trade-off is an extra abstraction and iterator/lambda machinery. Measure the path that matters instead of replacing every collection by habit.

A reproducible benchmark

The original comparison transforms URLs six times: filter by host, remove two protocols and www., trim a trailing slash, and append a path. To make the result useful, record the Kotlin version, JDK, device/CPU, compiler mode, warm-up policy, dataset size, and allocation profile. Run multiple forks with JMH or Android Macrobenchmark rather than trusting one measureTimedValue call in a cold process.

Example pipeline:

val eager = urls
    .filter { it.contains("example.com") }
    .map { it.removePrefix("https://") }
    .map { it.removePrefix("http://") }
    .map { it.removePrefix("www.") }
    .map { it.removeSuffix("/") }
    .map { "$it/random_path" }

val lazy = urls.asSequence()
    .filter { it.contains("example.com") }
    .map { it.removePrefix("https://") }
    .map { it.removePrefix("http://") }
    .map { it.removePrefix("www.") }
    .map { it.removeSuffix("/") }
    .map { "$it/random_path" }
    .toList()

Compare equal outputs first. Then run 20, 2,000, 20,000, 200,000, and 2,000,000 inputs, with both six transformations and a single filter. The size sweep matters: a result that wins at two million elements may be irrelevant to a screen that normally renders 20.

How to read the result

Sequences are a stronger candidate when the dataset is large, the pipeline has several transformations, or a terminal operation can stop early. Collections are usually preferable when the dataset is small, the result is reused, or the pipeline is simple enough that intermediate allocation is not on the profile.

Do not use a microbenchmark to justify a broader architecture change. In a mobile product, database queries, serialization, network latency, and UI work often dominate this difference. Profile the end-to-end flow and keep the simpler form until the evidence says otherwise.

INGENIOUS.BUILD