In Dart, when a Flutter build method creates button callbacks in a for loop, which index does each callback see, and when do they all share one variable?
answer
- a fresh variable per iteration
- applies to variables declared in the loop
- for-in and C-style alike
- outer variable: one shared binding
- while loop plus counter: last value
basics
~20 sA variable declared in a Dart for loop's header is fresh each iteration, so each callback keeps its own index. Callbacks share one variable only when it is declared outside the loop, as with a while-loop counter, and then all see its final value.
solid answer
~50 sDart closures capture **variables**, not snapshots of values. What saves you in a loop is that Dart creates a **new variable for each iteration** when the variable is declared in the loop header: `for (var i = 0; i < n; i++)` and `for (final label in labels)` both give each closure its own binding, so the button built for index 1 calls `select(1)`. The dart.dev tour contrasts this with JavaScript's `var`, where the same code sees the final value. The shared-variable bug still appears in Dart when the variable is declared **outside** the loop: a `while` loop with a counter, or `var i; for (i = 0; ...)`. Every closure then reads the single `i` when tapped, after the loop has finished, and sees its final value. The fix is to declare the variable in the loop header, or copy it into a local declared inside the body.
code
dart · 16 linesvoid main() {
final perIteration = <void Function()>[];
for (var i = 0; i < 3; i++) {
perIteration.add(() => print('for: $i'));
}
final shared = <void Function()>[];
var j = 0;
while (j < 3) {
shared.add(() => print('while: $j'));
j++;
}
for (final f in perIteration) f(); // for: 0, for: 1, for: 2
for (final f in shared) f(); // while: 3, while: 3, while: 3
}go deeper
Recall that callbacks created in a Dart for loop keep their own index, and that closures capture variables rather than values.
Explain the per-iteration variable rule, which loops it covers, and why an outer counter is shared.
Spot refactors that move a loop variable outside the loop, copy values into locals where needed, and back it with a test that taps several buttons.
Encourage data-driven iteration over manual counters in shared UI code, so capture bugs cannot be introduced by routine refactors.
## Closures capture variables A **closure** is a function that refers to variables from the scope where it was written. In Dart it captures the **variable itself**, not a copy of its value at creation time: ```dart var count = 0; final increment = () => count++; increment(); print(count); // 1: the closure changed the captured variable ``` So in a loop, the question "which index does the callback see?" becomes "how many variables are there?" ## Dart's for loops create one variable per iteration When the loop variable is **declared in the loop header**, Dart gives every iteration its own fresh variable. For the C-style form `for (var i = 0; i < n; i++)`, each iteration starts with a new `i` initialised from the previous one, and the increment runs on the new copy. The effect is that a closure created in an iteration keeps the value its own `i` had in that iteration. The dart.dev language tour uses exactly this example: ```dart var callbacks = []; for (var i = 0; i < 2; i++) { callbacks.add(() => print(i)); } for (final c in callbacks) { c(); } // prints 0, then 1 ``` The tour notes that the same code in JavaScript, written with `var`, prints `2` twice. A `for`-in loop (`for (final label in labels)`) behaves the same way: each iteration's `label` is a separate variable. ## When callbacks do share one variable The per-iteration rule applies only to a variable **declared by the loop**. Any variable declared outside it is a single binding shared by every closure: | Loop | Variables | What three callbacks print | |---|---|---| | `for (var i = 0; i < 3; i++)` | one per iteration | `0`, `1`, `2` | | `for (final i in [0, 1, 2])` | one per iteration | `0`, `1`, `2` | | `var i = 0; while (i < 3) { ...; i++; }` | one, outside | `3`, `3`, `3` | | `int i; for (i = 0; i < 3; i++)` | one, outside | `3`, `3`, `3` | Buttons are tapped long after `build` returns, so a shared counter has already reached its final value by the time any callback runs. ## The Flutter scenario A toolbar builds one button per label: ```dart final buttons = <Widget>[]; for (var i = 0; i < labels.length; i++) { buttons.add(TextButton( onPressed: () => _select(i), child: Text(labels[i]), )); } ``` This is correct: each `onPressed` closure has its own `i`. The bug appears after an innocent-looking refactor to a `while` loop or to a counter declared above the loop; then every button selects `labels.length`, often throwing a `RangeError` when `_select` indexes the list. ## A closure also reads fields at call time Captured **fields** are a separate case. `() => _select(_current)` reads `_current` through `this` when the button is tapped, not when it was built. If you mean "the value at build time", copy it into a local first: `final current = _current;` then `() => _select(current)`. ## Why typical Flutter code rarely hits it Most Flutter code avoids the shared-variable case without trying: - `for (var i = 0; ...)` and `for (final item in items)` inside `build` both declare their variable in the header; - `ListView.builder` passes the index to `itemBuilder` as a **parameter**, and every call has its own parameter, so `(context, index) => TextButton(onPressed: () => _select(index), ...)` is safe; - callbacks that close over the **item** (`() => _open(item)`) rather than an index do not depend on a counter at all. The risky shapes are hand-written `while` loops, counters declared before the loop and reused, and closures stored for later in a field or a map that outlives the loop. ## Fixes and habits 1. Declare loop variables in the loop header (`for (var i ...)` or `for (final x in ...)`). 2. When a `while` loop is unavoidable, copy the counter into a `final` local inside the body and capture the local. 3. Prefer iterating the data (`for (final (i, label) in labels.indexed)`) so there is no outer counter to share. 4. Add a widget test that taps two different buttons and checks each selects its own item.
- How do you fix Dart callbacks built in a while loop that all see the final counter?Copy the counter into a variable declared inside the loop body, such as `final index = j;`, and capture `index` instead of `j`. Each iteration's body creates a new `index`, so each closure keeps its own value. Converting to a `for` loop that declares the counter in its header works too.
- If a Dart closure created inside a for loop body changes i after being created, what does it see later?It sees that iteration's variable as it was left at the end of the body. The loop's increment runs on the next iteration's fresh copy, so it does not affect earlier closures, but any change the body itself makes to `i` after creating the closure is visible to it.
saying these in an interview costs you the question
- Dart closures in a for loop all see the final index, as in JavaScript with var
- Dart closures copy captured values when they are created
- A while loop with an outer counter also gets a fresh variable per iteration
- Declaring the loop variable final is what makes each closure get its own value
- for-in loops share one variable while C-style for loops do not