Skip to content
ArceApps Logo ArceApps
ES

Kotlin Collections vs Sequences: Optimizing Memory in Android

⏱️ 4 min read
Kotlin Collections vs Sequences: Optimizing Memory in Android

🧐 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:

  1. Takes 1 -> map(1*2=2) -> filter(2 > 5? No) -> Discarded.
  2. Takes 2 -> map(2*2=4) -> filter(4 > 5? No) -> Discarded.
  3. 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:

  1. The list is large (tens of thousands of elements).
  2. You have multiple chained steps (map + filter + sorted…).
  3. 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 Map with associateBy.

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
Kotlin October 15, 2025

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.

Read more
Clean Architecture: The Ultimate Guide for Modern Android
Architecture October 15, 2025

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.

Read more
Kotlin Coroutines: The Android Guide
Kotlin October 15, 2025

Kotlin Coroutines: The Android Guide

Mastering Kotlin Coroutines on Android. Dispatchers, structured concurrency, and best practices for asynchronous programming.

Read more