Collections #

When developing apps with Flutter, you don’t just deal with single variables — you deal with dynamic, complex sets of data, like article lists from a server, user configuration settings, and visual component hierarchies. The ability to manage, filter, and transform these data structures rests entirely on Dart’s Collections system. We’ll break down in depth the main collection types in Dart like List, Set, Map, Queue, and LinkedList, analyze their performance efficiency differences, master modern operators like the Spread Operator and Collection If/For, and explore flexible, efficient functional operations.

Choosing the Right Collection (Decision Matrix) #

Every collection type in Dart is designed with different performance characteristics and storage algorithms. Choosing the wrong data structure can badly impact your app’s performance, especially when managing thousands of data items on a screen requiring smooth animations.

Before we discuss code and implementation, let’s look at the decision flow diagram below to determine which collection best fits your data needs:

flowchart TD
    Start["What are your data's characteristics?"] --> Q1{"Does the data have Key-Value pairs?"}
    Q1 -- "Yes" --> Map["Choose: Map"]
    Q1 -- "No" --> Q2{"Must the data be unique without duplicates?"}
    Q2 -- "Yes" --> Set["Choose: Set"]
    Q2 -- "No" --> Q3{"Do you need fast operations at both FIFO/LIFO ends?"}
    Q3 -- "Yes" --> Queue["Choose: Queue"]
    Q3 -- "No" --> List["Choose: List"]

In general:

  • Use List if item order is very important and you need to access elements by index number.
  • Use Set if data uniqueness is the top priority (e.g., a list of selected IDs) and you want very fast existence checks.
  • Use Map if you want to associate a lookup key with another data value.

List — Ordered Indexed Data Collection #

List is the most commonly used collection type in Dart. This collection stores elements in order (ordered collection) where each element can be directly accessed through a zero-based numeric index.

In Dart, List is divided into two main categories based on its memory size allocation capability:

1. Growable List (Dynamic Capacity) #

By default, when you create a List literal, its size is dynamic. You can freely add or remove elements at any time. Dart automatically allocates extra memory space in the background when the list grows full.

// Creating a growable list
final names = <String>[];
names.add('Andi');
names.add('Siti'); // The list size automatically grows to 2

2. Fixed-Length List #

A fixed-length list is defined with a static length from the moment it’s created. You’re not allowed to add or remove elements from this list, but you can change the value of existing elements within those index bounds.

// Creating a fixed-length list with 3 elements initially null
final fixedList = List<String?>.filled(3, null, growable: false);
fixedList[0] = 'Budi'; // VALID

// DON'T: Trying to add a new element to a fixed-length list
// fixedList.add('Sari'); // ERROR runtime: Unsupported operation: Cannot add to a fixed-length list

Advanced List Constructors #

Dart provides several helper constructors to initialize Lists instantly:

List.generate: Creates elements dynamically based on a math formula or the generation index.

// Creating a list of square numbers: [0, 1, 4, 9, 16]
final squares = List.generate(5, (index) => index * index);

List.unmodifiable: Creates a list that cannot be changed at all, neither its values nor its size (immutable list). This constructor is great for protecting data from accidental changes.

final original = [1, 2, 3];
final readOnlyList = List.unmodifiable(original);

// readOnlyList[0] = 99; // ERROR runtime: Unsupported operation: Cannot modify an unmodifiable list

Set — A High-Performance Unique Data Collection #

Set is a collection of unique elements that doesn’t allow data duplication. If you try to insert a value that already exists in the Set, it’s automatically ignored.

Set’s main strength over List is data processing efficiency. In a List, to check whether an element exists (using the .contains() method), the compiler must scan all elements from start to finish, which takes linear time $O(n)$. Meanwhile, in a Set, Dart uses a hashing algorithm so lookups complete in constant time $O(1)$ regardless of how much data it holds.

// Initializing an empty Set (must include an explicit data type)
final uniqueIds = <int>{};

// DON'T confuse this with an empty Map initialization, which also uses curly braces
final emptyMap = {}; // This is interpreted as Map<dynamic, dynamic> by Dart

// Duplicates are automatically discarded
final genders = {'Male', 'Female', 'Male', 'Female'};
print(genders); // Output: {Male, Female}

Set Algebra Operations #

Sets in Dart come with built-in methods for performing mathematical set operations directly:

final programmer = {'Alice', 'Bob', 'Charlie'};
final designer = {'Charlie', 'Diana', 'Eve'};

// 1. Union: Combining all unique members
final allEmployees = programmer.union(designer);
// Output: {Alice, Bob, Charlie, Diana, Eve}

// 2. Intersection: Taking members present in BOTH groups
final hybridStaff = programmer.intersection(designer);
// Output: {Charlie}

// 3. Difference: Taking members that ONLY exist in programmer
final pureProgrammer = programmer.difference(designer);
// Output: {Alice, Bob}

Map — Key-Value Pair Mapping Structure #

Map (often called a dictionary in other programming languages) is a collection storing data in key-value pair format. Every key in a Map is unique and acts as an index pointer to access the associated value.

// Defining a Map with String keys and int values
final userScores = <String, int>{
  'Alice': 95,
  'Bob': 80,
};

// Accessing values using square brackets []
print(userScores['Alice']); // Output: 95
print(userScores['Charlie']); // Output: null (If the key is not found)

Advanced Manipulation Methods #

To manage data inside a Map professionally, avoid manual null validation writing and start leveraging the following built-in APIs:

final cart = {'apple': 5, 'banana': 2};

// 1. putIfAbsent: Adds a new entry ONLY if the key isn't registered yet
cart.putIfAbsent('apple', () => 10); // 'apple' stays at 5 (not overwritten)
cart.putIfAbsent('orange', () => 3);  // 'orange' is added with value 3

// 2. update: Updates a value based on its previous value
cart.update('banana', (existingValue) => existingValue + 3); // 'banana' becomes 5

// 3. Handling keys that might not exist during update using the ifAbsent parameter
cart.update(
  'mango',
  (val) => val + 1,
  ifAbsent: () => 1, // If 'mango' doesn't exist yet, initialize it with value 1
);

Transforming Collections into Maps #

You can idiomatically convert a List of objects into a Map using the Map.fromEntries method:

class Product {
  final String sku;
  final String name;
  Product(this.sku, this.name);
}

final productsList = [
  Product('SKU-A', 'Laptop'),
  Product('SKU-B', 'Keyboard'),
];

// Converting the List into a Map with SKU as the lookup Key
final productMap = Map.fromEntries(
  productsList.map((product) => MapEntry(product.sku, product)),
);

print(productMap['SKU-A']?.name); // Output: Laptop (Lookup runs in O(1))

Modern Operators: Spread, Collection If, and Collection For #

Dart provides very powerful declarative functionality directly inside collection literal blocks. These features are very important when building UI layouts in Flutter.

Spread Operator (... and ...?) #

The spread operator instantly inserts all elements from one collection into another collection.

final baseFeatures = ['Login', 'Register'];
final premiumFeatures = ['Live Chat', 'Analytics'];

// Combining lists
final allFeatures = [...baseFeatures, ...premiumFeatures];

// Null-aware spread operator (...?): Prevents crashes if the inserted list is null
List<String>? optionalFeatures;
final safeFeatures = ['Home', ...?optionalFeatures, 'Settings']; 
// Runs safely without triggering a Null Pointer Exception

Collection If and Collection For #

These two operators let you insert logic conditions and loops directly inside collection declarations:

bool isLoggedIn = true;
final rawNotifications = ['System Update', 'New Promo'];

final dashboardMenu = [
  'Home',
  'Search',
  if (isLoggedIn) 'Profile', // Collection If: Only inserted if isLoggedIn is true
  for (final notification in rawNotifications) 'Notif: $notification', // Collection For
];

Best Practice in Flutter: Use Collection If instead of ternary operators or manual list manipulation when conditionally adding child Widgets. This keeps your Widget Tree clean and readable:

Column(
  children: [
    const HeaderWidget(),
    if (isNewUser) const WelcomeBannerWidget(), // Very clean!
    const FooterWidget(),
  ],
)

Special Collections: Queue and LinkedList #

Besides the three main collections above, Dart’s standard library package (dart:collection) provides two special data structures for advanced optimization scenarios:

1. Queue (Double-Ended Queue) #

Queue is a collection specifically designed to add and remove elements at the front or back of the queue in constant $O(1)$ speed. Unlike List, inserting at the front index of a List requires shifting all other elements in memory, which takes $O(n)$ time.

import 'dart:collection';

void runQueue() {
  final printerQueue = Queue<String>();
  
  printerQueue.addLast('Document_A.pdf'); // Adding at the end
  printerQueue.addLast('Document_B.pdf');
  printerQueue.addFirst('Urgent_Doc.pdf'); // Adding at the very front (Priority)

  // Consuming the queue from the front (FIFO)
  while (printerQueue.isNotEmpty) {
    final doc = printerQueue.removeFirst();
    print('Printing: $doc');
  }
}

2. LinkedList (Doubly Linked List) #

LinkedList is a doubly linked list implementation. Note that this class does not implement Dart’s built-in List class. Every element inserted must be a subclass of the LinkedListEntry class.

LinkedList is very efficient if you often insert or delete elements in the middle of the collection, because those operations complete in $O(1)$ time using neighbor pointer references without shifting other memory elements.

import 'dart:collection';

class TaskEntry extends LinkedListEntry<TaskEntry> {
  final String title;
  TaskEntry(this.title);
  
  @override
  String toString() => title;
}

void runLinkedList() {
  final list = LinkedList<TaskEntry>();
  
  final task1 = TaskEntry('Buy Milk');
  final task2 = TaskEntry('Do Laundry');
  
  list.addAll([task1, task2]);
  
  // Inserting a new task right in the middle directly (O(1))
  task1.insertAfter(TaskEntry('Sweep the Floor'));
  
  // Removing an element directly from the entry object (O(1))
  task2.unlink();
  
  print(list); // Output: (Buy Milk, Sweep the Floor)
}

Functional Programming on Collections #

All Dart collections implementing the Iterable interface have built-in methods for processing data with the functional programming paradigm.

The Lazy Evaluation Nature #

One crucial thing to understand about functional operations in Dart (like map and where) is their lazy nature. Those operations don’t actually execute or iterate the data when you write the code.

Dart only creates a new transformation pointer. The real data iteration only runs when you convert that iterable into a concrete collection (e.g., by calling .toList(), .toSet(), or doing a for-in loop).

final numbers = [1, 2, 3, 4];

// The map operation below hasn't executed any print or multiplication yet!
final doubleNumbers = numbers.map((n) {
  print('Processing number: $n');
  return n * 2;
});

print('Mapping step finished being written.');

// The processing execution only runs here when toList() is called
final finalResult = doubleNumbers.toList();

Here’s a visualization of how data flows through a functional operation chain combining filtering (where) and mapping (map):

flowchart LR
    Input["List: [1, 2, 3, 4]"] --> Filter{"where(isEven)"}
    Filter -->|"Only Evens"| Mid["Filtered: [2, 4]"]
    Mid --> MapOp{"map(x * 10)"}
    MapOp --> Output["Output: [20, 40]"]

Aggregation Operations: reduce vs fold #

Both methods are used to reduce all elements in a collection into a single value (e.g., summing up a shopping cart’s total price).

reduce: Uses the first element as the initial accumulation value. This method throws a StateError if the collection is empty.

final prices = [100, 200, 300];
final totalPrice = prices.reduce((accumulator, element) => accumulator + element);

fold: Requires an explicit initial value. This method is very safe to use because it can handle empty collections without crashing.

final emptyPrices = <int>[];
// The initial value 0 is passed as the first fold parameter
final safeTotal = emptyPrices.fold(0, (accumulator, element) => accumulator + element);
print(safeTotal); // Output: 0 (Safe from crashes!)

Quick Navigation and Checks #

Dart provides very intuitive built-in methods for filtering elements without writing manual loops:

  • every: Returns true if all elements meet a specific condition.
  • any: Returns true if at least one element meets a specific condition.
  • firstWhere: Gets the first element meeting a condition, with an orElse parameter for handling missing data.
  • take(n): Takes the first n elements from the front.
  • skip(n): Skips the first n elements and takes the rest.

Operation Complexity Table #

To make optimizing your app code easier, here’s a summary matrix of time complexity (Big O Notation) for each collection type in Dart:

OperationListSetMapQueueLinkedList
Index / Key Access$O(1)$$O(1)$$O(n)$
Value Lookup (contains)$O(n)$$O(1)$$O(1)$$O(n)$$O(n)$
Insert at End (add)$O(1)$\*$O(1)$$O(1)$$O(1)$\*$O(1)$
Insert at Front$O(n)$$O(1)$$O(1)$\\
Delete at Front$O(n)$$O(1)$$O(1)$$O(1)$$O(1)$\\
Delete in the Middle$O(n)$$O(1)$$O(1)$$O(n)$$O(1)$\\

* Average (amortized) time value. Execution time can occasionally rise to $O(n)$ when the buffer memory capacity is full and the system performs a resize. ** Operations run in $O(1)$ only if you already hold the pointer reference of the target LinkedListEntry object directly.

Summary #

  • List: An ordered, index-based collection. Use a Growable List for dynamic capacity and Fixed-length List / Unmodifiable List for efficiency and data safety.
  • Set: A unique element collection with instant $O(1)$ lookup performance. Very efficient for processing non-duplicate data and mathematical set operations.
  • Map: Stores Key-Value paired data. Has safe methods like putIfAbsent and update for manipulating data entries.
  • Modern Operators: The Spread Operator (...) simplifies collection merging, while Collection If is the best way to conditionally build widget layouts in Flutter.
  • Queue & LinkedList: Advanced collections optimized for queue-end manipulation (Queue) and instant middle insertion/deletion (LinkedList).
  • Functional Programming: Operations like map and where use Lazy Evaluation, saving CPU cycles by deferring data iteration until truly needed.
  • fold: Always choose fold over reduce when aggregating potentially empty data to avoid runtime exceptions.

← Previous: Dart 3 Features   Next: OOP in Dart →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact