in Dart, closures are functions that can capture and remember variables from their surrounding lexical scope, even after that scope has finished executing. And lexical scoping means that the scope of variables is determined by where they are defined in the source code, not by where they are called.
Let me break that down with a simple example:
Function makeCounter() {
int count = 0; // variable defined in outer function
return () {
count++;
return count;
};
}
void main() {
var counter1 = makeCounter();
print(counter1()); // 1
print(counter1()); // 2
print(counter1()); // 3
var counter2 = makeCounter();
print(counter2()); // 1 (independent state)
}
Here’s what happens:
- The function
makeCounterdefines a local variablecountand returns an anonymous function. - That inner function “closes over” the
countvariable — meaning it remembers its value, even aftermakeCounter()finishes executing. - So each time
counter1()is called, it keeps updating its owncount.
This demonstrates closure behavior — the returned function still has access to the count variable from its original lexical scope.
I’ve used this pattern in real-world Flutter projects, for instance, in stateful data streams or animation controllers where I needed to keep stateful callbacks. For example, when implementing a custom pagination loader, I used closures to track the last fetched page number without maintaining a separate class-level variable.
Another example:
List callbacks = [];
for (int i = 0; i print('Index: $i'));
}
callbacks.forEach((callback) => callback());
Now, because of lexical scoping, the variable i is shared by all closures. So this will print:
Index: 3
Index: 3
Index: 3
not 0, 1, 2 — since by the time callbacks execute, i has become 3.
To fix that, I can capture i in a new local scope:
for (int i = 0; i print('Index: $current'));
}
Now it prints:
Index: 0
Index: 1
Index: 2
This shows how lexical scoping controls variable access — closures capture variables, not values, unless we create a new local reference.
A challenge I faced early on was debugging closures inside loops or asynchronous code — for example, using a loop with Future.delayed() and expecting different values. All closures used the same final loop variable, leading to unexpected results. I fixed it by introducing a local copy within the loop so that each closure had its own lexical scope.
For example:
for (int i = 0; i < 3; i++) {
Future.delayed(Duration(seconds: 1), () {
print('After 1s: $i');
});
}
All three print “After 1s: 3”. The fix:
for (int i = 0; i < 3; i++) {
var current = i;
Future.delayed(Duration(seconds: 1), () {
print('After 1s: $current');
});
}
Now they print 0, 1, 2 correctly.
In terms of limitations, closures in Dart capture variables by reference, not by value — which is sometimes unintuitive if you come from a language that copies the variable’s value at capture time. Also, closures can lead to memory retention if you accidentally keep references alive longer than needed (for example, holding onto large objects in a closure that never gets disposed).
As an alternative in more structured scenarios — like managing persistent state — I often use classes or StatefulWidgets instead of closures, since they make lifecycle and memory management clearer.
In summary:
- Dart uses lexical scoping, meaning variable visibility is determined by code location.
- Closures can capture and preserve variables from their defining scope.
- They’re extremely useful for callbacks, event handlers, and maintaining state across invocations.
- But they need careful handling in loops and async code to avoid common pitfalls.
