Dart Programming Fundamentals

Collections: Lists

40 min Lesson 9 of 13

Introduction to Lists in Dart

A List is one of the most commonly used collection types in Dart. It represents an ordered group of elements that can be accessed by their index position. Lists in Dart are similar to arrays in other programming languages, but they come with a rich set of built-in methods that make them extremely powerful and flexible.

Note: In Dart, all lists are objects of the List<T> class, where T is the type of elements the list holds. Dart does not have a separate "array" type — lists are the go-to ordered collection.

Creating Lists

There are several ways to create lists in Dart. Let's explore each approach.

List Literals (Growable)

The most common way to create a list is using square bracket syntax. By default, these lists are growable, meaning you can add or remove elements after creation.

Creating Growable Lists

void main() {
  // Type is inferred as List<int>
  var numbers = [1, 2, 3, 4, 5];
  print(numbers); // [1, 2, 3, 4, 5]

  // Explicit type annotation
  List<String> fruits = ['Apple', 'Banana', 'Cherry'];
  print(fruits); // [Apple, Banana, Cherry]

  // Empty list with type annotation
  List<double> prices = [];
  prices.add(9.99);
  prices.add(14.50);
  print(prices); // [9.99, 14.5]

  // Mixed types using dynamic (not recommended)
  List<dynamic> mixed = [1, 'hello', true, 3.14];
  print(mixed); // [1, hello, true, 3.14]
}

Fixed-Length Lists

You can create a list with a fixed size using List.filled(). Fixed-length lists cannot grow or shrink — you can only change the values at existing positions.

Fixed-Length Lists

void main() {
  // Create a fixed list of 5 elements, all initialized to 0
  var fixedList = List<int>.filled(5, 0);
  print(fixedList); // [0, 0, 0, 0, 0]

  // Modify elements by index
  fixedList[0] = 10;
  fixedList[1] = 20;
  fixedList[4] = 50;
  print(fixedList); // [10, 20, 0, 0, 50]

  // fixedList.add(60); // ERROR! Cannot add to a fixed-length list
  // fixedList.removeAt(0); // ERROR! Cannot remove from a fixed-length list

  // Fixed list of Strings
  var names = List<String>.filled(3, '');
  names[0] = 'Ahmed';
  names[1] = 'Sara';
  names[2] = 'Omar';
  print(names); // [Ahmed, Sara, Omar]
}
Warning: The old List(n) constructor for creating fixed-length lists is deprecated in Dart 2. Always use List.filled(length, defaultValue) or List.generate() instead.

Accessing Elements by Index

List elements are accessed using zero-based indexing. The first element is at index 0, the second at index 1, and so on.

Index-Based Access

void main() {
  var colors = ['Red', 'Green', 'Blue', 'Yellow', 'Purple'];

  print(colors[0]);   // Red (first element)
  print(colors[2]);   // Blue (third element)
  print(colors[4]);   // Purple (last element)

  // Access the first and last elements
  print(colors.first); // Red
  print(colors.last);  // Purple

  // Modify an element
  colors[1] = 'Lime';
  print(colors); // [Red, Lime, Blue, Yellow, Purple]

  // Get the length
  print(colors.length); // 5

  // Access last element using length
  print(colors[colors.length - 1]); // Purple
}
Warning: Accessing an index that is out of bounds (e.g., colors[10] on a 5-element list) will throw a RangeError at runtime. Always check the list length before accessing by index if you are unsure.

Adding Elements

Growable lists support several methods for adding new elements.

Adding Elements to a List

void main() {
  var languages = ['Dart', 'Python'];

  // add() - Adds a single element to the end
  languages.add('JavaScript');
  print(languages); // [Dart, Python, JavaScript]

  // addAll() - Adds all elements from another iterable
  languages.addAll(['Go', 'Rust']);
  print(languages); // [Dart, Python, JavaScript, Go, Rust]

  // insert() - Inserts at a specific index
  languages.insert(1, 'Java');
  print(languages); // [Dart, Java, Python, JavaScript, Go, Rust]

  // insertAll() - Inserts multiple elements at a specific index
  languages.insertAll(3, ['C++', 'Swift']);
  print(languages);
  // [Dart, Java, Python, C++, Swift, JavaScript, Go, Rust]
}

Removing Elements

There are multiple ways to remove elements from a growable list.

Removing Elements from a List

void main() {
  var items = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'];

  // remove() - Removes the first occurrence of a value
  items.remove('Cherry');
  print(items); // [Apple, Banana, Date, Elderberry]

  // removeAt() - Removes element at a specific index
  var removed = items.removeAt(0);
  print(removed); // Apple
  print(items);   // [Banana, Date, Elderberry]

  // removeLast() - Removes and returns the last element
  var last = items.removeLast();
  print(last);  // Elderberry
  print(items); // [Banana, Date]

  // removeRange() - Removes a range of elements
  var numbers = [1, 2, 3, 4, 5, 6, 7];
  numbers.removeRange(2, 5); // Remove index 2 to 4
  print(numbers); // [1, 2, 6, 7]

  // removeWhere() - Removes elements matching a condition
  var scores = [85, 42, 91, 37, 78, 55];
  scores.removeWhere((score) => score < 50);
  print(scores); // [85, 91, 78, 55]

  // clear() - Removes all elements
  scores.clear();
  print(scores); // []
}

Essential List Methods

Dart lists come with a wide variety of methods. Here are the most important ones you will use regularly.

Searching and Checking

Search Methods

void main() {
  var fruits = ['Apple', 'Banana', 'Cherry', 'Apple', 'Date'];

  // contains() - Check if an element exists
  print(fruits.contains('Banana')); // true
  print(fruits.contains('Mango'));  // false

  // indexOf() - Find the first index of an element
  print(fruits.indexOf('Apple'));   // 0
  print(fruits.indexOf('Mango'));   // -1 (not found)

  // lastIndexOf() - Find the last index of an element
  print(fruits.lastIndexOf('Apple')); // 3

  // isEmpty and isNotEmpty
  print(fruits.isEmpty);    // false
  print(fruits.isNotEmpty); // true

  // length
  print(fruits.length); // 5

  // any() - Check if any element matches a condition
  print(fruits.any((f) => f.startsWith('C'))); // true

  // every() - Check if ALL elements match a condition
  print(fruits.every((f) => f.length > 2)); // true
}

Sorting

Sorting Lists

void main() {
  // sort() - Sorts in place (modifies the original list)
  var numbers = [5, 2, 8, 1, 9, 3];
  numbers.sort();
  print(numbers); // [1, 2, 3, 5, 8, 9]

  // Sort strings alphabetically
  var names = ['Charlie', 'Alice', 'Bob', 'David'];
  names.sort();
  print(names); // [Alice, Bob, Charlie, David]

  // Custom sort with comparator
  var words = ['banana', 'apple', 'cherry', 'date'];
  words.sort((a, b) => a.length.compareTo(b.length));
  print(words); // [date, apple, banana, cherry]

  // Sort descending
  var scores = [75, 92, 88, 64, 95];
  scores.sort((a, b) => b.compareTo(a));
  print(scores); // [95, 92, 88, 75, 64]
}

Reversed and Sublist

Reversed and Sublist

void main() {
  var letters = ['A', 'B', 'C', 'D', 'E'];

  // reversed - Returns an Iterable (not a List)
  var reversedLetters = letters.reversed;
  print(reversedLetters); // (E, D, C, B, A)

  // Convert reversed Iterable to List
  var reversedList = letters.reversed.toList();
  print(reversedList); // [E, D, C, B, A]

  // sublist() - Extract a portion of the list
  var subset1 = letters.sublist(1, 4); // From index 1 to 3
  print(subset1); // [B, C, D]

  var subset2 = letters.sublist(2); // From index 2 to end
  print(subset2); // [C, D, E]

  // Note: sublist creates a NEW list (not a view)
  subset1[0] = 'Z';
  print(letters); // [A, B, C, D, E] (unchanged)
}

Iterating Over Lists

Dart provides several ways to loop through list elements.

Iteration Methods

void main() {
  var cities = ['Riyadh', 'Dubai', 'Cairo', 'Istanbul'];

  // 1. for-in loop (most common)
  for (var city in cities) {
    print(city);
  }

  // 2. Traditional for loop (when you need the index)
  for (int i = 0; i < cities.length; i++) {
    print('$i: ${cities[i]}');
  }

  // 3. forEach method
  cities.forEach((city) {
    print('City: $city');
  });

  // 4. forEach with tear-off
  cities.forEach(print);

  // 5. Using asMap() to get index and value
  cities.asMap().forEach((index, city) {
    print('$index: $city');
  });

  // 6. Using indexed (Dart 3+)
  for (var (index, city) in cities.indexed) {
    print('$index: $city');
  }
}
Tip: Use for-in when you only need the values. Use a traditional for loop when you need the index. Use forEach for simple one-line operations. Avoid forEach when you need to use break or return — it does not support them.

The Spread Operator (...)

The spread operator ... allows you to unpack all elements of a list into another list. It is incredibly useful for combining lists.

Spread Operator

void main() {
  var first = [1, 2, 3];
  var second = [4, 5, 6];

  // Combine two lists
  var combined = [...first, ...second];
  print(combined); // [1, 2, 3, 4, 5, 6]

  // Add elements before, between, or after
  var full = [0, ...first, 99, ...second, 100];
  print(full); // [0, 1, 2, 3, 99, 4, 5, 6, 100]

  // Null-aware spread operator (...?)
  List<int>? maybeNull;
  var safe = [1, 2, ...?maybeNull, 3];
  print(safe); // [1, 2, 3]

  // Useful for conditionally including lists
  maybeNull = [10, 20];
  var withValues = [1, 2, ...?maybeNull, 3];
  print(withValues); // [1, 2, 10, 20, 3]
}

Collection if and Collection for

Dart supports conditional elements and loops directly inside list literals. These are called collection if and collection for, and they make list building very expressive.

Collection if

Conditional Elements in Lists

void main() {
  bool isLoggedIn = true;
  bool isAdmin = false;

  var menu = [
    'Home',
    'About',
    if (isLoggedIn) 'Profile',
    if (isLoggedIn) 'Settings',
    if (isAdmin) 'Admin Panel',
    'Contact',
  ];

  print(menu); // [Home, About, Profile, Settings, Contact]

  // Collection if-else
  var greeting = [
    if (isLoggedIn) 'Welcome back!' else 'Please log in',
  ];
  print(greeting); // [Welcome back!]
}

Collection for

Loop Elements in Lists

void main() {
  // Generate a list of squared numbers
  var squares = [
    for (int i = 1; i <= 5; i++) i * i,
  ];
  print(squares); // [1, 4, 9, 16, 25]

  // Transform one list into another
  var names = ['alice', 'bob', 'charlie'];
  var uppercased = [
    for (var name in names) name.toUpperCase(),
  ];
  print(uppercased); // [ALICE, BOB, CHARLIE]

  // Combine collection if and for
  var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
  var evenSquares = [
    for (var n in numbers)
      if (n.isEven) n * n,
  ];
  print(evenSquares); // [4, 16, 36, 64, 100]
}
Tip: Collection if and collection for are resolved at build time of the list. They are not just syntactic sugar for loops — they integrate directly into list literal construction and are very efficient.

Immutable Lists

Sometimes you want a list that cannot be modified after creation. Dart provides several ways to create immutable (read-only) lists.

Creating Immutable Lists

void main() {
  // Using const keyword - compile-time constant
  const colors = ['Red', 'Green', 'Blue'];
  // colors.add('Yellow'); // ERROR! Cannot modify a const list
  // colors[0] = 'Pink';   // ERROR! Cannot modify a const list

  // const list assigned to a var variable
  var moreColors = const ['Cyan', 'Magenta', 'Yellow'];
  // moreColors.add('Black'); // ERROR at runtime!

  // But you CAN reassign the variable itself
  moreColors = ['Black', 'White']; // OK - new growable list
  moreColors.add('Gray');            // OK - this list is growable

  // List.unmodifiable() - runtime immutable from any iterable
  var original = [1, 2, 3, 4, 5];
  var unmodifiable = List<int>.unmodifiable(original);
  // unmodifiable.add(6);   // ERROR! Unsupported operation
  // unmodifiable[0] = 99;  // ERROR! Unsupported operation

  // Original list is still modifiable
  original.add(6);
  print(original);     // [1, 2, 3, 4, 5, 6]
  print(unmodifiable); // [1, 2, 3, 4, 5] (snapshot at creation time)
}
Note: const lists are compile-time constants and are deeply immutable. List.unmodifiable() creates a runtime unmodifiable view. Use const when the values are known at compile time, and List.unmodifiable() when you want to freeze a dynamically built list.

List.generate()

The List.generate() constructor creates a list of a given length with values computed by a function. It is very handy for initializing lists with patterns.

Generating Lists

void main() {
  // Generate a list of 5 elements: index * 2
  var doubles = List<int>.generate(5, (index) => index * 2);
  print(doubles); // [0, 2, 4, 6, 8]

  // Generate multiplication table for 7
  var table = List<String>.generate(
    10,
    (i) => '7 x ${i + 1} = ${7 * (i + 1)}',
  );
  for (var row in table) {
    print(row);
  }
  // 7 x 1 = 7
  // 7 x 2 = 14
  // ... and so on

  // Generate a list of empty lists (2D structure)
  var grid = List<List<int>>.generate(3, (_) => []);
  grid[0].addAll([1, 2, 3]);
  grid[1].addAll([4, 5, 6]);
  grid[2].addAll([7, 8, 9]);
  print(grid); // [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

  // Fixed-length generated list
  var fixed = List<int>.generate(5, (i) => i * 10, growable: false);
  print(fixed); // [0, 10, 20, 30, 40]
  // fixed.add(50); // ERROR! Cannot add to a fixed-length list
}

Common Patterns: Filtering, Mapping, Reducing

Dart lists support powerful functional-style operations that let you transform data without writing explicit loops.

Filtering with where()

Filtering Lists

void main() {
  var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

  // where() returns a lazy Iterable - convert to List with toList()
  var evens = numbers.where((n) => n.isEven).toList();
  print(evens); // [2, 4, 6, 8, 10]

  var greaterThan5 = numbers.where((n) => n > 5).toList();
  print(greaterThan5); // [6, 7, 8, 9, 10]

  // firstWhere() - Find first matching element
  var firstEven = numbers.firstWhere((n) => n.isEven);
  print(firstEven); // 2

  // firstWhere with orElse for safety
  var bigNumber = numbers.firstWhere(
    (n) => n > 100,
    orElse: () => -1,
  );
  print(bigNumber); // -1
}

Mapping with map()

Transforming Lists

void main() {
  var numbers = [1, 2, 3, 4, 5];

  // map() transforms each element
  var doubled = numbers.map((n) => n * 2).toList();
  print(doubled); // [2, 4, 6, 8, 10]

  var names = ['alice', 'bob', 'charlie'];
  var capitalized = names.map((name) {
    return name[0].toUpperCase() + name.substring(1);
  }).toList();
  print(capitalized); // [Alice, Bob, Charlie]

  // Chaining where and map
  var students = [
    {'name': 'Ahmed', 'grade': 92},
    {'name': 'Sara', 'grade': 45},
    {'name': 'Omar', 'grade': 88},
    {'name': 'Layla', 'grade': 37},
  ];

  var passingNames = students
      .where((s) => (s['grade'] as int) >= 50)
      .map((s) => s['name'])
      .toList();
  print(passingNames); // [Ahmed, Omar]
}

Reducing with reduce() and fold()

Reducing Lists to a Single Value

void main() {
  var numbers = [10, 20, 30, 40, 50];

  // reduce() - Combines all elements into one value
  var sum = numbers.reduce((a, b) => a + b);
  print(sum); // 150

  var max = numbers.reduce((a, b) => a > b ? a : b);
  print(max); // 50

  // fold() - Like reduce but with an initial value
  // Safer for empty lists since reduce throws on empty lists
  var total = numbers.fold<int>(0, (sum, n) => sum + n);
  print(total); // 150

  // fold with a different return type
  var words = ['Hello', 'Dart', 'World'];
  var sentence = words.fold<String>('', (result, word) {
    return result.isEmpty ? word : '$result $word';
  });
  print(sentence); // Hello Dart World

  // join() - Simpler way to concatenate strings
  var joined = words.join(' ');
  print(joined); // Hello Dart World

  // Calculate average
  var scores = [85, 90, 78, 92, 88];
  var average = scores.fold<double>(0, (sum, s) => sum + s) / scores.length;
  print(average); // 86.6
}
Tip: Prefer fold() over reduce() when the list might be empty. reduce() throws a StateError on an empty list, while fold() simply returns the initial value. Also use fold() when the return type differs from the element type.

Useful List Tricks

Practical List Operations

void main() {
  // Remove duplicates using toSet()
  var withDuplicates = [1, 2, 3, 2, 4, 1, 5, 3];
  var unique = withDuplicates.toSet().toList();
  print(unique); // [1, 2, 3, 4, 5]

  // Flatten a list of lists using expand()
  var nested = [[1, 2], [3, 4], [5, 6]];
  var flat = nested.expand((list) => list).toList();
  print(flat); // [1, 2, 3, 4, 5, 6]

  // Take and skip
  var items = ['a', 'b', 'c', 'd', 'e', 'f'];
  print(items.take(3).toList());  // [a, b, c]
  print(items.skip(3).toList());  // [d, e, f]

  // Swap two elements
  var list = [10, 20, 30, 40];
  var temp = list[0];
  list[0] = list[3];
  list[3] = temp;
  print(list); // [40, 20, 30, 10]

  // Copy a list (shallow copy)
  var original = [1, 2, 3];
  var copy = [...original]; // or List.from(original) or .toList()
  copy.add(4);
  print(original); // [1, 2, 3]
  print(copy);     // [1, 2, 3, 4]

  // Check list equality (element by element)
  // import 'package:collection/collection.dart';
  // ListEquality().equals([1, 2], [1, 2]); // true
}

Practice Exercise

Create a Dart program that manages a shopping list. Your program should:

  1. Create a list of 5 grocery items.
  2. Add 2 more items to the end of the list.
  3. Insert 1 item at the beginning (index 0).
  4. Remove the third item from the list.
  5. Sort the list alphabetically.
  6. Use map() to create a new list where each item is prefixed with a number (e.g., "1. Milk", "2. Eggs").
  7. Print the final numbered list.
  8. Use where() to filter items that start with a specific letter and print the result.

Try using collection if to conditionally add a "Discount Coupon" item only if the list has more than 5 items.