If collections are the containers, collection operations are what you do with them. Kotlin's standard library has over 150 collection functions — but you'll use about 20 of them in 90% of your Android code. This guide covers all the essential ones with clear explanations, practical examples, and real Android use cases.


Why Collection Operations Matter

Without collection operations, working with data looks like this:

// Java-style — verbose and error-prone
val publishedArticles = mutableListOf<Article>()
for (article in articles) {
    if (article.isPublished) {
        publishedArticles.add(article)
    }
}
publishedArticles.sortByDescending { it.publishedAt }

With Kotlin collection operations:

// Kotlin — clean, readable, expressive
val publishedArticles = articles
    .filter { it.isPublished }
    .sortedByDescending { it.publishedAt }

Same result. Half the code. Zero mutable state. Much harder to get wrong.


Transformation Operations

map — Transform Each Element

map applies a function to every element and returns a new list with the transformed values.

val numbers = listOf(1, 2, 3, 4, 5)
val doubled = numbers.map { it * 2 }
// [2, 4, 6, 8, 10]

val names = listOf("alice", "bob", "charlie")
val capitalized = names.map { it.replaceFirstChar { c -> c.uppercase() } }
// [Alice, Bob, Charlie]

Real Android use — transform API response to display model:

val displayItems = articles.map { article ->
    ArticleDisplayItem(
        id = article.id,
        title = article.title,
        excerpt = article.content.take(150) + "...",
        authorName = article.author.name,
        formattedDate = article.publishedAt.toRelativeTime(),
        readTimeText = "${article.readTimeMinutes} min read"
    )
}

mapNotNull — Transform and Drop Nulls

Like map, but any element that transforms to null is excluded from the result.

val strings = listOf("1", "two", "3", "four", "5")
val numbers = strings.mapNotNull { it.toIntOrNull() }
// [1, 3, 5] — "two" and "four" returned null, excluded

// Real use — extract valid IDs from a messy list
val validUserIds = rawIds.mapNotNull { id ->
    if (id.isNotBlank()) id.trim() else null
}

mapIndexed — Transform with Index

val items = listOf("Apple", "Banana", "Cherry")
val numbered = items.mapIndexed { index, item ->
    "${index + 1}. $item"
}
// ["1. Apple", "2. Banana", "3. Cherry"]

flatMap — Transform and Flatten

flatMap maps each element to a list, then flattens all those lists into one.

val sentences = listOf("Hello World", "Kotlin is great", "Android dev")
val words = sentences.flatMap { it.split(" ") }
// [Hello, World, Kotlin, is, great, Android, dev]

// Each author has multiple articles — get all articles from all authors
val allArticles = authors.flatMap { author -> author.articles }

// Each category has multiple tags — get all unique tags
val allTags = categories.flatMap { it.tags }.distinct()

flatten — Flatten Without Transforming

val nested = listOf(listOf(1, 2), listOf(3, 4), listOf(5, 6))
val flat = nested.flatten()
// [1, 2, 3, 4, 5, 6]

Filtering Operations

filter — Keep Elements That Match

val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val evens = numbers.filter { it % 2 == 0 }
// [2, 4, 6, 8, 10]

val articles = listOf(/* articles */)
val published = articles.filter { it.isPublished }
val kotlinArticles = articles.filter { it.category == "Kotlin" }
val recent = articles.filter { it.publishedAt > sevenDaysAgo }

filterNot — Keep Elements That Don't Match

val articles = articles.filterNot { it.isDeleted }
val nonEmpty = strings.filterNot { it.isBlank() }

filterNotNull — Remove Null Elements

val nullable = listOf("Alice", null, "Bob", null, "Charlie")
val nonNull: List<String> = nullable.filterNotNull()
// [Alice, Bob, Charlie]

filterIsInstance — Keep Elements of a Specific Type

val mixed: List<Any> = listOf(1, "hello", 2.5, "world", true, 42)
val strings = mixed.filterIsInstance<String>()
// [hello, world]

val numbers = mixed.filterIsInstance<Int>()
// [1, 42]

partition — Split Into Two Lists

partition splits a list into two — a Pair where the first list matches the condition and the second doesn't.

val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val (evens, odds) = numbers.partition { it % 2 == 0 }
// evens: [2, 4, 6, 8, 10]
// odds:  [1, 3, 5, 7, 9]

val (published, drafts) = articles.partition { it.isPublished }
println("Published: ${published.size}, Drafts: ${drafts.size}")

Aggregation Operations

reduce — Combine All Elements Into One

reduce combines all elements from left to right using an accumulator. The first element is the initial accumulator.

val numbers = listOf(1, 2, 3, 4, 5)
val sum = numbers.reduce { acc, n -> acc + n }
// acc starts as 1 (first element)
// 1+2=3, 3+3=6, 6+4=10, 10+5=15
println(sum)   // 15

val longest = listOf("cat", "elephant", "dog", "rhinoceros")
    .reduce { longest, word -> if (word.length > longest.length) word else longest }
println(longest)   // rhinoceros

Warning: reduce throws UnsupportedOperationException on an empty list. Use reduceOrNull for safety.

val safe = emptyList<Int>().reduceOrNull { acc, n -> acc + n }
println(safe)   // null — no crash

fold — Like reduce but with an Initial Value

fold is like reduce but you provide the starting value — safer and more flexible.

val numbers = listOf(1, 2, 3, 4, 5)

// fold with initial value 0
val sum = numbers.fold(0) { acc, n -> acc + n }
// 0+1=1, 1+2=3, 3+3=6, 6+4=10, 10+5=15
println(sum)   // 15

// fold to build a string
val sentence = listOf("Kotlin", "is", "awesome")
    .fold("") { acc, word -> if (acc.isEmpty()) word else "$acc $word" }
println(sentence)   // Kotlin is awesome

// fold to count by category
val countByCategory = articles.fold(mutableMapOf<String, Int>()) { map, article ->
    map[article.category] = (map[article.category] ?: 0) + 1
    map
}
// {Kotlin=5, Android=8, Compose=3}

sum, average, min, max, count

val scores = listOf(85, 92, 78, 95, 88)

println(scores.sum())        // 438
println(scores.average())    // 87.6
println(scores.min())        // 78
println(scores.max())        // 95
println(scores.count())      // 5
println(scores.count { it >= 90 })  // 2 — count with condition

With custom objects — use sumOf, minByOrNull, maxByOrNull:

val articles = listOf(/* articles with viewCount */)

val totalViews = articles.sumOf { it.viewCount }
val mostViewed = articles.maxByOrNull { it.viewCount }
val leastViewed = articles.minByOrNull { it.viewCount }
val avgReadTime = articles.map { it.readTimeMinutes }.average()

Grouping Operations

groupBy — Group Elements by a Key

groupBy returns a Map<K, List<V>> where each key maps to the list of elements that produced that key.

val articles = listOf(/* articles with category */)

val byCategory: Map<String, List<Article>> = articles.groupBy { it.category }
// {
//   "Kotlin"  -> [article1, article3, article7],
//   "Android" -> [article2, article5],
//   "Compose" -> [article4, article6]
// }

// Count per category
byCategory.forEach { (category, articles) ->
    println("$category: ${articles.size} articles")
}

// Get articles for a specific category
val kotlinArticles = byCategory["Kotlin"] ?: emptyList()

// Group users by first letter of name
val users = listOf(/* users */)
val alphabetical = users.groupBy { it.name.first().uppercaseChar() }
// {A -> [Alice, Anna], B -> [Bob], C -> [Charlie, Chris]}

groupingBy + eachCount — Count per Group

val articles = listOf(/* articles */)

val countPerCategory = articles
    .groupingBy { it.category }
    .eachCount()
// {Kotlin=5, Android=8, Compose=3}

associateBy — List to Map with Key Selector

Convert a list to a map for fast lookup by a unique key:

val articles = listOf(/* articles with id */)

val articleMap: Map<String, Article> = articles.associateBy { it.id }

// Now O(1) lookup instead of O(n) search
val article = articleMap["article_001"]

// associateBy with value transform
val titleById = articles.associateBy(
    keySelector = { it.id },
    valueTransform = { it.title }
)
// {"1" -> "Kotlin Guide", "2" -> "Android Tips"}

associate — Full Control Over Key and Value

val users = listOf(User("Alice", 25), User("Bob", 30))
val nameToAge = users.associate { it.name to it.age }
// {Alice=25, Bob=30}

Ordering Operations

sorted, sortedBy, sortedByDescending

val numbers = listOf(3, 1, 4, 1, 5, 9, 2, 6)
println(numbers.sorted())              // [1, 1, 2, 3, 4, 5, 6, 9]
println(numbers.sortedDescending())    // [9, 6, 5, 4, 3, 2, 1, 1]

val articles = listOf(/* articles */)
val byDate = articles.sortedByDescending { it.publishedAt }
val byTitle = articles.sortedBy { it.title.lowercase() }
val byViews = articles.sortedByDescending { it.viewCount }

sortedWith — Multiple Sort Criteria

val articles = articles.sortedWith(
    compareByDescending<Article> { it.isFeatured }
        .thenByDescending { it.publishedAt }
        .thenBy { it.title }
)
// Featured first, then newest, then alphabetical

reversed

val numbers = listOf(1, 2, 3, 4, 5)
println(numbers.reversed())   // [5, 4, 3, 2, 1]

Searching Operations

find / firstOrNull

val articles = listOf(/* articles */)

val featured = articles.find { it.isFeatured }
val kotlinArticle = articles.firstOrNull { it.category == "Kotlin" }
// Returns null if not found — safe

val first = articles.first()           // throws if empty
val firstSafe = articles.firstOrNull() // null if empty

last / lastOrNull

val latest = articles.lastOrNull { it.isPublished }

any, all, none

val articles = listOf(/* articles */)

val hasKotlin = articles.any { it.category == "Kotlin" }
val allPublished = articles.all { it.isPublished }
val noneDeleted = articles.none { it.isDeleted }

// Short-circuit — stops as soon as result is known

contains / in

val tags = listOf("Kotlin", "Android", "Compose")
println(tags.contains("Kotlin"))   // true
println("Kotlin" in tags)          // true — same thing

Combining Operations

zip — Combine Two Lists Into Pairs

val names = listOf("Alice", "Bob", "Charlie")
val scores = listOf(95, 87, 92)

val combined = names.zip(scores)
// [(Alice, 95), (Bob, 87), (Charlie, 92)]

// With transform
val result = names.zip(scores) { name, score ->
    "$name: $score"
}
// ["Alice: 95", "Bob: 87", "Charlie: 92"]

Real use — combine parallel data:

val articleIds = listOf("1", "2", "3")
val readCounts = listOf(150, 320, 87)

val articleStats = articleIds.zip(readCounts) { id, count ->
    ArticleStat(id = id, readCount = count)
}

unzip — Split Pairs Into Two Lists

val pairs = listOf("Alice" to 95, "Bob" to 87, "Charlie" to 92)
val (names, scores) = pairs.unzip()
// names:  [Alice, Bob, Charlie]
// scores: [95, 87, 92]

plus and minus — Add/Remove Elements

val list = listOf(1, 2, 3)
val added = list + 4           // [1, 2, 3, 4]
val removed = list - 2         // [1, 3]
val combined = list + listOf(4, 5)  // [1, 2, 3, 4, 5]

Utility Operations

distinct / distinctBy

val numbers = listOf(1, 2, 2, 3, 3, 3, 4)
println(numbers.distinct())   // [1, 2, 3, 4]

val articles = listOf(/* articles possibly with duplicate categories */)
val uniqueCategories = articles.distinctBy { it.category }
// One article per category — first occurrence kept

take / drop / takeLast / dropLast

val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

println(numbers.take(3))         // [1, 2, 3]
println(numbers.drop(3))         // [4, 5, 6, 7, 8, 9, 10]
println(numbers.takeLast(3))     // [8, 9, 10]
println(numbers.dropLast(3))     // [1, 2, 3, 4, 5, 6, 7]
println(numbers.takeWhile { it < 5 })  // [1, 2, 3, 4]
println(numbers.dropWhile { it < 5 })  // [5, 6, 7, 8, 9, 10]

chunked — Split Into Batches

val items = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val batches = items.chunked(3)
// [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]

// Send articles in batches of 20
articles.chunked(20).forEach { batch ->
    sendBatch(batch)
}

windowed — Sliding Window

val numbers = listOf(1, 2, 3, 4, 5)
val windows = numbers.windowed(3)
// [[1, 2, 3], [2, 3, 4], [3, 4, 5]]

// Calculate 3-day moving average
val dailyViews = listOf(100, 150, 130, 180, 200, 160)
val movingAvg = dailyViews.windowed(3) { window -> window.average() }
// [126.67, 153.33, 170.0, 180.0]

Chaining Operations — Real Android Example

The real power comes from chaining multiple operations:

fun getHomeScreenArticles(
    articles: List<Article>,
    selectedCategory: String,
    searchQuery: String,
    maxCount: Int = 20
): List<ArticleDisplayItem> {
    return articles
        .filter { !it.isDeleted }                                    // remove deleted
        .filter { selectedCategory == "All" || it.category == selectedCategory }  // category filter
        .filter { searchQuery.isEmpty() || it.title.contains(searchQuery, ignoreCase = true) }  // search
        .sortedWith(
            compareByDescending<Article> { it.isFeatured }
                .thenByDescending { it.publishedAt }
        )                                                             // featured first, then newest
        .take(maxCount)                                              // limit results
        .map { article ->                                            // transform to display model
            ArticleDisplayItem(
                id = article.id,
                title = article.title,
                excerpt = article.content.take(150),
                authorName = article.author.name,
                category = article.category,
                readTime = "${article.readTimeMinutes} min",
                formattedDate = article.publishedAt.toRelativeTime(),
                viewCount = article.viewCount.formatAsCount()
            )
        }
}

Common Mistakes to Avoid

Mistake 1: Using map when mapNotNull is needed

// ❌ Returns List<String?> — nulls included
val names = users.map { it.displayName }  // displayName is nullable

// ✅ Returns List<String> — nulls excluded
val names = users.mapNotNull { it.displayName }

Mistake 2: Using reduce on possibly empty list

// ❌ Throws if list is empty
val total = amounts.reduce { acc, n -> acc + n }

// ✅ Safe with fold or reduceOrNull
val total = amounts.fold(0.0) { acc, n -> acc + n }
val total = amounts.reduceOrNull { acc, n -> acc + n } ?: 0.0

Mistake 3: Calling first() instead of firstOrNull()

// ❌ Throws NoSuchElementException if list is empty
val article = articles.first { it.isFeatured }

// ✅ Returns null if not found
val article = articles.firstOrNull { it.isFeatured }

Mistake 4: Multiple passes when one would do

// ❌ Three passes through the list
val published = articles.filter { it.isPublished }
val sorted = published.sortedByDescending { it.publishedAt }
val top10 = sorted.take(10)

// ✅ Same result, same three operations — but now clearly expressed as a chain
// (Kotlin optimizes this reasonably well — readability matters more here)
val top10 = articles
    .filter { it.isPublished }
    .sortedByDescending { it.publishedAt }
    .take(10)

Mistake 5: Using forEach when map/filter is more appropriate

// ❌ Imperative — building list manually with forEach
val titles = mutableListOf<String>()
articles.forEach { titles.add(it.title) }

// ✅ Declarative — use map
val titles = articles.map { it.title }

Quick Reference

Operation What It Does Returns
map Transform each element List<R>
mapNotNull Transform, drop nulls List<R>
flatMap Transform to list, flatten List<R>
filter Keep matching elements List<T>
filterNot Keep non-matching elements List<T>
filterNotNull Remove nulls List<T>
partition Split into two lists Pair<List, List>
groupBy Group by key Map<K, List<T>>
associateBy List to Map Map<K, T>
reduce Combine into one value T
fold Combine with initial value R
any At least one matches Boolean
all All match Boolean
none None match Boolean
find / firstOrNull First match or null T?
sorted / sortedBy Sort elements List<T>
zip Combine two lists List<Pair>
distinct Remove duplicates List<T>
take / drop Slice the list List<T>
chunked Split into batches List<List<T>>
sumOf Sum by selector Number
maxByOrNull Max by selector T?

Summary

  • Transformation: map, mapNotNull, mapIndexed, flatMap, flatten
  • Filtering: filter, filterNot, filterNotNull, filterIsInstance, partition
  • Aggregation: reduce, fold, sum, average, min, max, count, sumOf
  • Grouping: groupBy, associateBy, associate, groupingBy
  • Ordering: sorted, sortedBy, sortedByDescending, sortedWith, reversed
  • Searching: find, firstOrNull, lastOrNull, any, all, none
  • Combining: zip, unzip, plus, minus
  • Utility: distinct, distinctBy, take, drop, chunked, windowed
  • Chain operations for complex transformations — clean, readable, expressive
  • Prefer fold over reduce for safety; prefer firstOrNull over first

Mastering collection operations is one of the biggest productivity upgrades in Kotlin. Once these become second nature, you'll spend less time writing boilerplate loops and more time expressing exactly what your code means.

Happy coding!