Kotlin Collections vs Sequences: Optimizing Memory in Android
Table of Contents
🧐 The Performance Dilemma in Lists
In modern Android development, we manipulate lists constantly: API responses, contact lists, shopping cart items. Kotlin offers a rich and expressive collections API (map, filter, groupBy), but naive usage can skyrocket memory and CPU consumption.
The key question is: When should I use a standard List (Iterable) and when a Sequence?
Eager vs Lazy Evaluation
To understand the difference, we must look at how operations are executed.
Collections (Iterable): Eager Evaluation
Each operation creates a complete intermediate collection.
val list = listOf(1, 2, 3, 4, 5)
val result = list
.map { it * 2 } // Creates a new temporary list: [2, 4, 6, 8, 10]
.filter { it > 5 } // Creates ANOTHER temporary list: [6, 8, 10]
.first() // Returns 6
In this simple example, it’s not serious. But if the list had 1 million elements, you would be creating two lists of 1 million integers in memory just to get the first result. 💥
Sequences: Lazy Evaluation
Operations are not executed until the final result (terminal operation) is requested. Additionally, they process element by element.
val result = list.asSequence()
.map { it * 2 }
.filter { it > 5 }
.first()
Execution Flow:
- Takes
1->map(1*2=2)->filter(2 > 5? No)-> Discarded. - Takes
2->map(2*2=4)->filter(4 > 5? No)-> Discarded. - Takes
3->map(3*2=6)->filter(6 > 5? Yes)->first()takes it and terminates.
4 and 5 are not processed! And most importantly: No intermediate lists were created.
🛠️ When to use Sequences in Android
Don’t use Sequence for everything. They have an overhead of creating wrapper objects.
Golden Rule:
Use Sequence if:
- The list is large (tens of thousands of elements).
- You have multiple chained steps (
map+filter+sorted…). - You use “short-circuit” operations like
first,take,any, where you don’t need to process the entire list.
If the list is small (e.g., 100 items in a RecyclerView), the standard List is faster due to lower overhead.
⚡ Key Functional Operations
Beyond map and filter, mastering these operations cleans up your code:
1. fold and reduce
To accumulate a result from a list.
data class Product(val price: Double, val quantity: Int)
val cart = listOf(Product(10.0, 2), Product(5.0, 3))
// Reduce: The accumulator starts with the first element (can throw exception if empty)
// Fold: You define the initial value of the accumulator (safer)
val total = cart.fold(0.0) { acc, product ->
acc + (product.price * product.quantity)
}
// Result: 35.0
2. associateBy and groupBy
Vital for optimizing searches. Converting a List to a Map reduces search complexity from O(N) to O(1).
// Inefficient: O(N^2) Search
val users = getUsers() // List<User>
val orders = getOrders() // List<Order> (has userId)
orders.forEach { order ->
val user = users.find { it.id == order.userId } // Traverses the entire user list for each order
// ...
}
// Efficient: O(N) Search
val userMap = users.associateBy { it.id } // Map<String, User>
orders.forEach { order ->
val user = userMap[order.userId] // Instant lookup
// ...
}
3. zip
Combines two lists pair by pair.
val names = listOf("Alice", "Bob")
val scores = listOf(95, 80)
val results = names.zip(scores) { name, score ->
"$name scored $score"
}
// ["Alice scored 95", "Bob scored 80"]
🚫 Common Anti-Patterns
1. Unnecessary sorting (sorted) in Sequences
The sorted operation is a “stateful intermediate operation”. To sort, the sequence MUST process all elements and load them into memory, nullifying much of the lazy evaluation benefit.
If you are going to sort, do it at the end or evaluate if you really need a sequence.
2. Breaking the Sequence Chain
list.asSequence()
.map { ... }
.toList() // ❌ Materializes the list
.filter { ... } // ❌ Creates another list again
.first()
Keep the sequence (asSequence) until the final terminal operation.
🎯 Conclusion
Collections in Kotlin are powerful, but understanding their internal implementation distinguishes a Junior developer from a Senior.
- Small lists and simple operations: Use standard
List. - Large lists, long chains, or short-circuits: Use
asSequence(). - Frequent searches: Convert to
MapwithassociateBy.
Optimizing memory usage in Android not only avoids OutOfMemoryError, but reduces Garbage Collector frequency, resulting in a smoother UI (less “jank”).
You might also be interested in
Advanced Kotlin Flow: Operators and Patterns
Level up with Kotlin Flow. Master operators like combine, zip, flatMapLatest, and learn to handle complex reactive streams in Android.
Clean Architecture: The Ultimate Guide for Modern Android
Demystifying Clean Architecture: A deep dive into layers, dependencies, and data flow to build indestructible Android apps.
Kotlin Coroutines: The Android Guide
Mastering Kotlin Coroutines on Android. Dispatchers, structured concurrency, and best practices for asynchronous programming.