Loops are one of those things I use so often that I barely think about them consciously anymore — until something goes wrong, like an infinite loop freezing my app, or an off-by-one error skipping the last item in a list. In this article, I want to go through Dart’s loop structures in real depth: the classic for loop, the for-in loop, while, do-while, and the control statements break and continue that shape how loops behave. I’ll also touch on performance, common mistakes, and where each loop type actually shines in real Flutter development.
The Classic for Loop
This is the loop I learned first, and it’s still the one I reach for when I need precise control over the iteration — a specific starting point, a specific condition, and a specific increment.
void main() {
for (int i = 0; i < 5; i++) {
print('Iteration: $i');
}
}
Output:
Iteration: 0
Iteration: 1
Iteration: 2
Iteration: 3
Iteration: 4
The structure has three parts inside the parentheses, separated by semicolons:
- Initialization (
int i = 0) — runs once, before the loop starts. - Condition (
i < 5) — checked before every iteration; the loop continues as long as this istrue. - Increment/update (
i++) — runs after every iteration.
I can manipulate all three parts to control the loop precisely — counting backwards, skipping by twos, or using multiple variables.
void main() {
for (int i = 10; i > 0; i -= 2) {
print(i);
}
}
Output:
10
8
6
4
2
The for-in Loop
When I’m just iterating over a collection and don’t need the index, I almost always reach for for-in instead, since it’s cleaner and less error-prone.
void main() {
var fruits = ['Apple', 'Banana', 'Mango'];
for (var fruit in fruits) {
print('Fruit: $fruit');
}
}
Output:
Fruit: Apple
Fruit: Banana
Fruit: Mango
This works on any object that implements Dart’s Iterable interface — lists, sets, maps (via .entries, .keys, or .values), and even custom iterable classes I define myself.
void main() {
var scores = {'Ali': 85, 'Sara': 92, 'Zain': 78};
for (var entry in scores.entries) {
print('${entry.key}: ${entry.value}');
}
}
Output:
Ali: 85
Sara: 92
Zain: 78
If I do need the index alongside the value, I’ll typically use asMap().entries on a list, or just fall back to the classic for loop:
void main() {
var colors = ['Red', 'Green', 'Blue'];
colors.asMap().forEach((index, color) {
print('$index: $color');
});
}
Output:
0: Red
1: Green
2: Blue
The while Loop
I use while when I don’t know in advance how many iterations I’ll need — the loop continues as long as a condition remains true, and the condition is checked before each iteration.
void main() {
int count = 0;
while (count < 5) {
print('Count is $count');
count++;
}
}
Output:
Count is 0
Count is 1
Count is 2
Count is 3
Count is 4
A real-world case I run into often is reading data until some condition is met — for example, processing queued items until the queue is empty:
void main() {
var queue = [5, 3, 8, 1];
while (queue.isNotEmpty) {
var item = queue.removeAt(0);
print('Processing item: $item');
}
print('Queue is now empty.');
}
Output:
Processing item: 5
Processing item: 3
Processing item: 8
Processing item: 1
Queue is now empty.
The do-while Loop
do-while is a variant I reach for far less often, but it has one crucial difference: the loop body runs at least once, before the condition is ever checked.
void main() {
int number = 10;
do {
print('Number is $number');
number++;
} while (number < 5);
}
Output:
Number is 10
Even though number < 5 is false from the very start, the body still executes once, because the condition is checked after the first iteration, not before. I use this pattern when I genuinely need “run this at least once regardless of the condition” logic — a common example is prompting a user for input and validating it, where you need to show the prompt at least once before checking whether the input was valid.
void main() {
int attempts = 0;
bool success = false;
do {
attempts++;
print('Attempt $attempts: trying to connect...');
success = attempts == 3; // simulate success on the third try
} while (!success);
print('Connected after $attempts attempts.');
}
Output:
Attempt 1: trying to connect...
Attempt 2: trying to connect...
Attempt 3: trying to connect...
Connected after 3 attempts.
Controlling Loops: break and continue
I use break when I need to exit a loop entirely, before its natural condition would otherwise stop it.
void main() {
for (int i = 0; i < 10; i++) {
if (i == 5) {
break;
}
print(i);
}
}
Output:
0
1
2
3
4
continue, on the other hand, skips just the current iteration and moves on to the next one, without exiting the loop entirely.
void main() {
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
continue;
}
print(i);
}
}
Output:
1
3
5
7
9
I find continue particularly useful for filtering logic inline, without needing an extra level of if-else nesting.
Labeled Loops
Dart also supports labeled break and continue for nested loops, which I don’t use often, but it’s genuinely useful when I need to break out of an outer loop from inside a nested one.
void main() {
outerLoop:
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (j == 1) {
continue outerLoop;
}
print('i=$i, j=$j');
}
}
}
Output:
i=0, j=0
i=1, j=0
i=2, j=0
Without the label, continue would only skip the inner loop’s current iteration; with the label outerLoop, it skips straight to the next iteration of the outer loop instead.
Real-World Use Case: Building a List in Flutter
Loops show up constantly in Flutter, even when I’m not writing an explicit for or while block myself — internally, widgets like ListView.builder are essentially loops driven by an index. But I also use explicit loops when preparing data before building the UI:
List<Widget> buildProductTiles(List<String> products) {
List<Widget> tiles = [];
for (var product in products) {
tiles.add(ListTile(title: Text(product)));
}
return tiles;
}
This kind of loop-driven widget generation is extremely common when I need to transform raw data — API responses, database results — into a list of UI components.
Performance Considerations
For most everyday app logic, the performance difference between for, for-in, and .forEach() is negligible — Dart’s runtime optimizes all of them reasonably well. However, there are a few things I keep in mind:
- Avoid recalculating
.lengthunnecessarily inside a loop condition if you’re iterating over a large, unchanging list — though for mostListimplementations in Dart,.lengthis an O(1) operation, so this is less of a concern than it is in some other languages. - Be cautious about modifying a collection while iterating over it with
for-in. This throws aConcurrentModificationErrorat runtime.
void main() {
var numbers = [1, 2, 3, 4];
try {
for (var number in numbers) {
if (number == 2) {
numbers.remove(number);
}
}
} catch (e) {
print('Error: $e');
}
}
Output:
Error: Concurrent modification during iteration: Instance(length:4) of '_GrowableList'.
To safely remove items while iterating, I either iterate over a copy of the list, or use methods like removeWhere(), which are designed specifically for this purpose:
void main() {
var numbers = [1, 2, 3, 4];
numbers.removeWhere((number) => number == 2);
print(numbers);
}
Output:
[1, 3, 4]
Infinite Loops and How I Avoid Them
An infinite loop happens when the loop’s condition never becomes false (for while/do-while) or the increment never brings the condition to an end (for for). In a Flutter app, this can freeze the UI thread entirely, since Dart is single-threaded by default for synchronous code.
void main() {
int i = 0;
while (i < 5) {
print(i);
// forgot to increment i -- this would run forever
// i++;
}
}
I’ve genuinely made this mistake before — forgetting the increment statement — and had to force-stop a running program. Now, I make it a habit to double-check that every loop has a guaranteed path to termination before I run new code, especially with while loops where the increment isn’t baked into the loop header the way it is with a for loop.
Common Mistakes
- Off-by-one errors — using
<=instead of<, or vice versa, causing the loop to run one time too many or too few. - Forgetting to update the loop variable in a
whileloop, causing an infinite loop. - Modifying a list while iterating over it directly with
for-in, causing aConcurrentModificationError. - Using
break/continuewithout realizing they only apply to the nearest enclosing loop, unless a label is used.
Best Practices
- I use
for-infor simply iterating over a collection’s values, since it’s the most readable option. - I reach for the classic
forloop when I need explicit control over an index or a custom increment pattern. - I use
whilewhen the number of iterations depends on a runtime condition, not a known count. - I use
do-whileonly when I genuinely need the body to execute at least once, regardless of the condition. - I avoid mutating a collection directly while iterating over it, opting instead for methods like
removeWhere,map, or working on a copy.
Frequently Asked Questions
What’s the difference between for-in and .forEach()? for-in is a language-level loop construct and supports break and continue. .forEach() is a method on Iterable that takes a callback function — since it’s just a regular function call under the hood, break and continue don’t work inside it; you’d need to use return to skip an iteration, though that only exits the current callback invocation, not the whole loop.
Can I loop over a Map directly? Not directly with for-in on the map itself, but I can loop over .entries, .keys, or .values, all of which are iterables.
Why did I get a ConcurrentModificationError? This happens when a collection is structurally modified (items added or removed) while it’s being iterated over with for-in. Use removeWhere, iterate over a copy, or collect changes and apply them after the loop finishes.
Is do-while commonly used in Dart/Flutter apps? Not as commonly as for and while, but it’s useful for “run once, then check” scenarios like retry logic or input validation loops.
Summary
Loops are fundamental to nearly everything I build in Dart — transforming data, validating input, generating widgets, and processing collections. The classic for loop gives me precise control, for-in is my default for simple iteration, while handles condition-driven repetition, and do-while covers the specific case where I need guaranteed first execution. Understanding break, continue, and the risks around modifying collections mid-iteration rounds out what I need to write loops that are both correct and efficient.
References
- Dart Language Tour — Loops: https://dart.dev/language/loops
- Dart API — Iterable class: https://api.dart.dev/stable/dart-core/Iterable-class.html
- Effective Dart: https://dart.dev/effective-dart
- Flutter Documentation: https://docs.flutter.dev