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:
reducethrowsUnsupportedOperationExceptionon an empty list. UsereduceOrNullfor 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
foldoverreducefor safety; preferfirstOrNulloverfirst
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!
Comments (0)