Back to All Sheets
Kotlin Standard LibraryKotlin 2.xINTERMEDIATE

Android Kotlin Collection Processing

Beginner-to-advanced Kotlin collection reference for Android, covering mutability, transformations, filtering, grouping, aggregation, sequences, allocation, complexity, concurrency boundaries, and production patterns.

42 min read
Category: Android

Collection Foundations

Core collection types and naming rules.

List, Set & Map

The main Kotlin collection shapes and their mutable variants.

Kotlin Collection Selection
TypeOrderingDuplicatesLookup ModelChoose When
List<T>Index-orderedAllowedBy index; membership is generally linearOrder and repeated values matter
Set<T>Implementation-dependentNot allowed by equalityMembership is typically constant-time for hash setsUniqueness and membership checks dominate
Map<K, V>Implementation-dependentKeys unique; values may repeatBy key, typically constant-time for hash mapsValues have stable lookup keys
Sequence<T>Source orderAllowedLazy pipeline, no indexed contractA multi-step pipeline can avoid intermediate collections
MutableList / Set / MapMatches implementationMatches collection kindIn-place mutationOne clear owner needs controlled mutation
KOTLIN
val list = listOf(1, 2, 2)
val mutableList = mutableListOf(1, 2)
val set = setOf(1, 2, 2)
val mutableSet = mutableSetOf(1, 2)
val map = mapOf("a" to 1, "b" to 2)
val mutableMap = mutableMapOf("a" to 1)
list»[1, 2, 2]
set»[1, 2]
map["b"]»2
💡

List preserves order and duplicates; Set stores unique values; Map stores key-value pairs.

📌

Read-only collections prevent mutation through that reference, but are not always deeply immutable.

listsetmapmutable

Operator Naming Rules

TEXT
By use a selector or key
Indexed lambda also receives the index
OrNull return null instead of throwing
Not invert the condition
Last work from the end
While continue until the condition becomes false
💡

Think in stages: transform → filter → select → group → aggregate.

namingmental-model

Transformations

Convert elements into new values or collection shapes.

map, mapIndexed & mapNotNull

KOTLIN
val nums = listOf(1, 2, 3)
nums.map { it * 2 }
nums.mapIndexed { index, value -> "$index:$value" }
val raw = listOf("10", "x", "20")
raw.mapNotNull { it.toIntOrNull() }
nums.map { it * 2 }»[2, 4, 6]
raw.mapNotNull { it.toIntOrNull() }»[10, 20]
📌

map returns one output element for every input element.

Use mapNotNull when failed conversions or missing values should be skipped.

mapmapIndexedmapNotNull

flatMap & flatten

KOTLIN
val nested = listOf(listOf(1, 2), listOf(3, 4))
nested.flatten()
val sentences = listOf("Kotlin is concise", "Collections are useful")
sentences.flatMap { it.split(" ") }
nested.flatten()»[1, 2, 3, 4]
sentences.flatMap { it.split(" ") }»[Kotlin, is, concise, Collections, are, useful]
💡

map transforms; flatMap transforms and flattens; flatten only removes one nesting level.

flatMapflatten

Filtering & Selection

Keep matching elements or safely retrieve values.

filter, filterNot & Type Filtering

KOTLIN
val nums = listOf(1, 2, 3, 4, 5, 6)
nums.filter { it % 2 == 0 }
nums.filterNot { it % 2 == 0 }
nums.filterIndexed { index, _ -> index % 2 == 0 }
val nullable = listOf("A", null, "B")
nullable.filterNotNull()
val mixed: List<Any> = listOf(1, "Kotlin", 2)
mixed.filterIsInstance<String>()
nums.filter { it % 2 == 0 }»[2, 4, 6]
nullable.filterNotNull()»[A, B]
mixed.filterIsInstance<String>()»[Kotlin]

filter checks the whole collection; takeWhile and dropWhile only inspect from the beginning.

filterfilterNotfilterIndexedfilterNotNull

take, drop, slice, chunked & windowed

KOTLIN
val nums = listOf(1, 2, 3, 4, 5)
nums.take(2)
nums.takeLast(2)
nums.drop(2)
nums.dropLast(2)
nums.slice(1..3)
nums.chunked(2)
nums.windowed(3)
nums.take(2)»[1, 2]
nums.drop(2)»[3, 4, 5]
nums.chunked(2)»[[1, 2], [3, 4], [5]]
nums.windowed(3)»[[1, 2, 3], [2, 3, 4], [3, 4, 5]]
💡

Use chunked for batches; use windowed for moving comparisons or neighbouring values.

takedropslicechunkedwindowed

first, last, single, find & elementAt

KOTLIN
val nums = listOf(1, 2, 3, 4)
nums.first()
nums.first { it > 2 }
nums.firstOrNull { it > 10 }
nums.find { it == 3 }
nums.lastOrNull { it < 3 }
nums.singleOrNull { it == 3 }
nums.elementAtOrNull(10)
nums.first { it > 2 }»3
nums.firstOrNull { it > 10 }»null
nums.find { it == 3 }»3

first(), last(), and single() may throw. Use OrNull variants when absence is expected.

📌

find(predicate) is equivalent to firstOrNull(predicate).

firstlastsinglefindelementAt

Checks, Distinctness & Ordering

any, all, none, contains & count

KOTLIN
val nums = listOf(2, 4, 6)
nums.any { it > 5 }
nums.all { it % 2 == 0 }
nums.none { it < 0 }
nums.contains(4)
4 in nums
nums.count { it > 2 }
nums.any { it > 5 }»true
nums.all { it % 2 == 0 }»true
nums.none { it < 0 }»true
nums.count { it > 2 }»2
anyallnonecount

distinct, sorted & Comparator Chains

KOTLIN
val nums = listOf(4, 1, 3, 1, 2)
nums.distinct()
nums.sorted()
nums.sortedDescending()
nums.reversed()
val words = listOf("cat", "car", "dog")
words.distinctBy { it.first() }
words.sortedBy { it.length }
nums.distinct()»[4, 1, 3, 2]
nums.sorted()»[1, 1, 2, 3, 4]
words.distinctBy { it.first() }»[cat, dog]
📌

reversed() flips current order; sortedDescending() performs a descending sort.

distinctsortedsortedBysortedWith

Grouping & Association

associate, associateBy & associateWith

KOTLIN
data class User(val id: Int, val name: String)
val usersById = users.associateBy { it.id }
val nameLengths = users.associateWith { it.name.length }
val idToName = users.associate { it.id to it.name }
Code Annotations:
associateBy

Selector creates the key; the original item becomes the value.

associateWith

Original item becomes the key; selector creates the value.

Duplicate keys overwrite earlier values in associateBy and associate.

associateassociateByassociateWith

groupBy, groupingBy & partition

KOTLIN
val words = listOf("cat", "car", "dog", "door")
val grouped = words.groupBy { it.first() }
val counts = words
.groupingBy { it.first() }
.eachCount()
val nums = listOf(1, 2, 3, 4, 5)
val (even, odd) = nums.partition { it % 2 == 0 }
grouped»{c=[cat, car], d=[dog, door]}
counts»{c=2, d=2}
even»[2, 4]
💡

groupBy builds lists immediately; groupingBy supports operations such as eachCount, fold, and reduce.

groupBygroupingBypartition

Combining & Aggregation

zip, unzip & Set Operations

KOTLIN
val names = listOf("Alex", "Ben")
val scores = listOf(90, 85)
val pairs = names.zip(scores)
val (unzippedNames, unzippedScores) = pairs.unzip()
val a = setOf(1, 2, 3)
val b = setOf(3, 4, 5)
a union b
a intersect b
a subtract b
pairs»[(Alex, 90), (Ben, 85)]
a intersect b»[3]
📌

zip stops when the shorter collection runs out of elements.

zipunzipunionintersectsubtract

sum, sumOf, average, min & max

KOTLIN
val nums = listOf(1, 2, 3, 4)
nums.sum()
nums.average()
nums.minOrNull()
nums.maxOrNull()
data class Product(val name: String, val price: Double)
val total = products.sumOf { it.price }
val cheapest = products.minByOrNull { it.price }
nums.sum()»10
nums.average()»2.5
sumsumOfaverageminByOrNullmaxByOrNull

fold, reduce & Running Aggregates

KOTLIN
val nums = listOf(1, 2, 3, 4)
nums.reduce { acc, value -> acc + value }
nums.fold(10) { acc, value -> acc + value }
nums.runningReduce { acc, value -> acc + value }
nums.runningFold(0) { acc, value -> acc + value }
nums.reduce { acc, value -> acc + value }»10
nums.fold(10) { acc, value -> acc + value }»20
nums.runningFold(0) { acc, value -> acc + value }»[0, 1, 3, 6, 10]

reduce uses the first element as its accumulator and throws on an empty collection.

💡

fold works with empty collections and can produce a different result type.

foldreducerunningFoldrunningReduce

Lazy Processing & Patterns

Sequence Processing

KOTLIN
val result = (1..1_000_000)
.asSequence()
.filter { it % 2 == 0 }
.map { it * 10 }
.take(5)
.toList()
result»[20, 40, 60, 80, 100]
💡

Collections process each stage eagerly; Sequence moves one element through the full pipeline at a time.

Sequences help most with large inputs, long chains, or early termination—not every small collection.

sequenceasSequencelazy

Complete Processing Pipeline

KOTLIN
data class Product(
val name: String,
val category: String,
val price: Double,
val available: Boolean
)
val result = products
.asSequence()
.filter { it.available }
.filter { it.price >= 10.0 }
.sortedBy { it.price }
.groupBy { it.category }
.mapValues { (_, group) -> group.map { it.name } }
Code Annotations:
products

Begin with the source collection.

.filter { it.available }

Keep only relevant items.

.mapValues

Shape the final result for its consumer.

💡

Read collection chains top to bottom: source → filter → transform → order → group → result.

pipelinepatterns

Common Mistakes & Quick Reference

TEXT
COMMON MISTAKES
Using map only for side effects
Putting forEach in the middle of a transformation chain
Calling first() when no result may exist
Confusing takeWhile with filter
Ignoring duplicate-key replacement in associateBy
Using Sequence for tiny, simple collections
Mutating unrelated external state inside map or filter
QUICK REFERENCE
Transform map, mapIndexed, mapNotNull, flatMap, flatten
Filter filter, filterNot, filterIndexed, filterNotNull
Select first, last, single, find, elementAt, take, drop
Check any, all, none, contains, count
Order sorted, sortedBy, sortedDescending, reversed
Group groupBy, groupingBy, partition
Associate associate, associateBy, associateWith
Combine zip, unzip, plus, union, intersect, subtract
Aggregate sum, sumOf, average, minOrNull, maxOrNull, fold, reduce
Shape distinct, chunked, windowed
Lazy asSequence
Side effect onEach, forEach
📌

Prefer clear chains of small operations over one large lambda with several responsibilities.

mistakesquick-reference

Mutability, Copying & Destinations

Control ownership and allocation when collections cross Android architecture boundaries.

Read-Only Is Not Deeply Immutable

Kotlin's List, Set, and Map interfaces prevent mutation through that reference, but the backing collection or contained objects may still mutate.

kotlin
val backing = mutableListOf("A")
val exposed: List<String> = backing
backing += "B" // exposed now also observes B
// Snapshot ownership at the boundary:
val snapshot: List<String> = backing.toList()
data class UiState(val users: List<User>)
💡
Snapshot at Ownership Boundaries

When a repository or ViewModel publishes state, copy or use a persistent immutable collection if another owner could still mutate the backing data.

⚠️
Shallow Copy

toList copies the container, not mutable elements. Prefer immutable element models or explicitly deep-copy where the domain requires isolation.

immutabilityownershipsnapshot

+, -, += and Mutation

plus/minus return new collections. += mutates a mutable receiver when plusAssign exists, but reassigns a read-only var using plus otherwise.

kotlin
val original = listOf(1, 2)
val added = original + 3 // new List
val mutable = mutableListOf(1, 2)
mutable += 3 // mutates MutableList
var readOnly: List<Int> = listOf(1, 2)
readOnly += 3 // readOnly = readOnly + 3; allocates
📈
Repeated + Can Be Quadratic

Building a large List with result = result + item copies the growing list on every iteration. Use buildList, a mutable builder, or a destination operator.

plusmutationallocation

mapTo, filterTo & Collection Builders

Destination variants reuse or select a result collection type; buildList/buildMap expose controlled mutation only inside the builder.

kotlin
val ids = ArrayList<UserId>(users.size)
users.mapTo(ids) { it.id }
val active = mutableSetOf<User>()
users.filterTo(active) { it.active }
📌
Clarity Before Micro-Allocation

Use ordinary map/filter by default. Destination variants matter in measured hot paths, when appending to an existing result, or when the result type is semantically important.

mapTofilterTobuildListperformance

Advanced Grouping, Indexing & Duplicates

Build indices and summaries without losing records accidentally.

Duplicate Key Policies

associateBy keeps the last value for a duplicate key. Choose reject, first, last, or group explicitly when duplicates carry meaning.

kotlin
val byId = buildMap {
for (user in users) {
require(put(user.id, user) == null) { "Duplicate ${user.id}" }
}
}
⚠️
Silent Overwrite Is Data Loss

If uniqueness is a domain invariant, validate it. Do not let associateBy silently hide malformed server or database data.

associateByduplicatesindex

groupingBy fold, reduce & aggregate

Grouping operations calculate per-key results without allocating a List for every group as groupBy does.

kotlin
data class Order(val customerId: CustomerId, val total: Long)
val spendByCustomer: Map<CustomerId, Long> = orders
.groupingBy(Order::customerId)
.fold(0L) { total, order -> total + order.total }
val counts = orders.groupingBy(Order::customerId).eachCount()
💡
Use aggregate for Full Control

aggregate exposes key, current accumulator, element, and first-element status. Prefer fold/eachCount when they express the calculation more clearly.

groupingByfoldaggregate

Complexity, Concurrency & Android UI

Make performance and thread-safety choices based on actual collection size, access pattern, and ownership.

Complexity & Allocation Guide

Most transformations scan O(n) and allocate a result; sorting is O(n log n); repeated linear membership can turn a pipeline quadratic.

text
List index access: usually O(1)
List contains/remove by value: O(n)
Set/Map lookup: average O(1), subject to hashing
map/filter/distinct: O(n), allocate result structures
sorted/sortedBy: O(n log n), allocate/reorder references
groupBy: O(n) plus lists for every key
list.filter { it.id in selectedList }: O(n × m)
Convert selected IDs to Set first: O(n + m) average
💡
Choose Structure Before Optimizing Lambdas

Replacing repeated List membership with a Set or prebuilt Map usually matters more than reducing a small lambda allocation. Profile realistic devices and data.

complexityallocationperformance

Thread Safety & Publication

Standard mutable collections are not thread-safe. Prefer single ownership and immutable snapshots across coroutine/thread boundaries.

kotlin
private val mutex = Mutex()
private val cache = mutableMapOf<Key, Value>()
suspend fun snapshot(): Map<Key, Value> = mutex.withLock { cache.toMap() }
suspend fun put(key: Key, value: Value) = mutex.withLock {
cache[key] = value
}
🛑
Collections Do Not Become Safe Inside a Coroutine

Coroutines can run concurrently and resume on different threads. Use confinement, Mutex, actors, concurrent structures, or atomic immutable-state updates.

thread-safetymutexconcurrency

Collections as Android UI State

Publish immutable lists with stable item identity. Compute expensive projections outside rendering or memoize them against correct inputs.

kotlin
data class FeedUiState(val items: List<FeedItem> = emptyList())
_state.update { state ->
state.copy(items = state.items.map { item ->
if (item.id == id) item.copy(selected = !item.selected) else item
})
}
// RecyclerView/Compose list identity should use item.id, not index.
💡
New Container, New Changed Elements

Copy only changed immutable elements while reusing unchanged objects. Diffing and Compose skipping can then use stable identity/equality effectively.

🔗
Flow Is a Different Abstraction

Collection operators process in-memory elements; Flow operators process asynchronous emissions. See Android Kotlin Flows for cancellation, context, buffering, and sharing.

ui-stateidentityandroid

Production Checklist

Review semantics, ownership, absence, duplicates, allocation, complexity, and thread safety.

text
List/Set/Map matches ordering and uniqueness needs
Read-only references are not mistaken for deep immutability
Mutable backing data does not escape its owner
OrNull/empty behavior matches expected absence
Duplicate keys have an explicit policy
Side effects are not hidden inside map/filter
Repeated + is not used to build large collections
List membership in loops is replaced by Set/Map where useful
Sequence is used only for a measured/structural benefit
Sorting/grouping allocations are acceptable for input size
Shared mutable collections are confined or synchronized
UI lists use immutable values and stable IDs
checklistproduction
Related References

Recommended Next Sheets