🟣 Kotlin Programming

Kotlin Collections and Functional Programming

Most Android screens are lists transformed into other lists: API results into UI models, items grouped by date, totals summed, duplicates removed. Kotlin's collection API turns these transformations into short, readable pipelines, if you know the right operators.

✍️ Yunok Tech 📅 Updated ⏱ 3 min read 🎯 Intermediate
On this page
  1. Read-only vs mutable collections
  2. A real pipeline: API data to UI sections
  3. The operators you'll use most
  4. fold and reduce
  5. Efficient lookups: avoid O(n²)
  6. Sequences: lazy evaluation
  7. Android-specific tips
  8. Common mistakes
  9. Frequently asked questions
  10. Conclusion

An imperative loop that filters, sorts, groups and maps a list can easily take 25 lines and three mutable variables. The same logic as a Kotlin collection pipeline is often five lines, with no mutation and clear intent. This guide covers the operators you'll use constantly in Android code, and when to reach for sequences or immutable collections.

Read-only vs mutable collections

Read-only by defaultKotlin
val tags: List<String> = listOf("kotlin", "compose")         // read-only interface
val cart: MutableList<String> = mutableListOf("p1")           // mutable
val updated = tags + "flow"                                   // creates a NEW list

List is a read-only view, not a deep guarantee of immutability. The underlying object might be mutable elsewhere. For UI state, create new lists rather than mutating, and consider kotlinx.collections.immutable (persistentListOf, ImmutableList) when you want truly immutable types that Compose treats as stable.

A real pipeline: API data to UI sections

Collection pipeline from transactions to grouped UI sections A list of transactions passes through filter to remove failed ones, sortedByDescending by time, groupBy date, and map to UI section models. List<Txn>from API/Room filterdrop failed sortedByDescnewest first groupByby date map→ sections
Each operator does one job and returns a new collection. Read the pipeline top to bottom like a sentence.
TransactionsMapper.ktKotlin
data class Txn(val id: String, val amountPaise: Long, val status: String, val at: LocalDateTime)
data class TxnSection(val date: LocalDate, val totalPaise: Long, val items: List<Txn>)

fun toSections(txns: List<Txn>): List<TxnSection> =
    txns
        .filter { it.status != "FAILED" }
        .sortedByDescending { it.at }
        .groupBy { it.at.toLocalDate() }          // Map<LocalDate, List<Txn>>, insertion-ordered
        .map { (date, items) -> TxnSection(date, items.sumOf { it.amountPaise }, items) }
Rendering the sections in ComposeKotlin
LazyColumn {
    sections.forEach { section ->
        stickyHeader(key = section.date) { DateHeader(section.date, section.totalPaise) }
        items(section.items, key = { it.id }) { txn -> TxnRow(txn) }
    }
}

The operators you'll use most

GoalOperatorExample
Transform each itemmap, mapNotNulldtos.mapNotNull { it.toDomainOrNull() }
Keep matching itemsfilter, filterIsInstanceitems.filterIsInstance<Video>()
Find onefirstOrNull, findusers.firstOrNull { it.id == id }
Checkany, all, nonecart.any { it.qty == 0 }
AggregatesumOf, count, maxByOrNull, foldcart.sumOf { it.qty * it.price }
Index by keyassociateBy, associateproducts.associateBy { it.id }
GroupgroupBy, partitionval (done, todo) = tasks.partition { it.done }
Deduplicatedistinct, distinctBymessages.distinctBy { it.id }
SortsortedBy, sortedWith(compareBy…)sortedWith(compareBy({ it.priority }, { it.title }))
Split into pageschunked, windowedids.chunked(50) for batched API calls
Combine two listsziplabels.zip(values)

fold and reduce

fold builds a result from a starting valueKotlin
data class Stats(val count: Int = 0, val totalPaise: Long = 0, val refunds: Int = 0)

val stats = txns.fold(Stats()) { acc, t ->
    acc.copy(
        count = acc.count + 1,
        totalPaise = acc.totalPaise + t.amountPaise,
        refunds = acc.refunds + if (t.status == "REFUNDED") 1 else 0,
    )
}

Prefer specific operators (sumOf, count) when they fit. Use fold when you compute several values in one pass.

Efficient lookups: avoid O(n²)

Joining two listsKotlin
// ❌ For each order, scan all products: slow for large lists
val rows = orders.map { o -> o to products.first { it.id == o.productId } }

// ✅ Build a map once, then O(1) lookups
val productsById = products.associateBy { it.id }
val rows2 = orders.mapNotNull { o -> productsById[o.productId]?.let { p -> o to p } }

Sequences: lazy evaluation

List operations are eager: each step creates a full intermediate list. asSequence() makes them lazy, processing one element at a time through the whole chain, and stopping early when possible.

When sequences helpKotlin
val firstTenMatches = hugeList.asSequence()
    .filter { it.matches(query) }
    .map { it.toUi() }
    .take(10)              // stops as soon as 10 are found
    .toList()

Use sequences for large collections with several steps, or when you short-circuit with take or first. For small lists (typical UI lists of tens or hundreds of items), plain lists are usually just as fast and simpler.

Android-specific tips

  • Transform data outside composables, in the ViewModel or with remember(key), so pipelines don't rerun on every recomposition.
  • Move heavy processing off the main thread (withContext(Dispatchers.Default)) for thousands of items.
  • Prefer SQL for filtering and aggregating large datasets in Room, rather than loading everything into memory and filtering in Kotlin.

Common mistakes

  • Using first { } when nothing may match, which throws NoSuchElementException. Use firstOrNull.
  • Mutating a list while iterating over it.
  • Chaining filter then count when count { } does it in one pass.
  • Exposing MutableList in UI state.

Frequently asked questions

Are functional operators slower than for loops?

Collection operators are inline functions, so lambdas don't allocate objects. Each step does create a new list, which rarely matters at UI-list sizes. Measure before optimising, and use sequences or a single loop for hot paths.

What's the difference between map and forEach?

map returns a new transformed collection. forEach returns nothing and is for side effects.

Does groupBy preserve order?

Yes. It returns a LinkedHashMap in which keys appear in first-encounter order, which is why sorting before grouping works.

Conclusion

Kotlin's collection API lets you express data transformations as small, readable, mutation-free pipelines. Learn the dozen core operators, build maps for lookups, use sequences for large or short-circuiting chains, and keep heavy transformations out of composables.

Next step: write your own reusable operators with Extension and Higher-Order Functions.

Official references

📱 Apps built by Yunok Tech

Real Android apps on Google Play, free to download.

See all our apps →

Ready to build real Android applications?

Kotlin fluency makes everything else in Android easier, from coroutines and Flow to Compose and clean architecture. The fastest way to build that fluency is to write real Kotlin every day and get feedback on it.

Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.

Explore the Android course and take your next step as an Android developer.

Share: LinkedIn X / Twitter WhatsApp