Dart & Flutter Control Flow and Conditional Rendering Techniques
Answer
Overview
Dart and Flutter provide a rich set of control flow constructs — both for logic flow in Dart and for conditional rendering in the widget tree. Mastering these makes your code concise, readable, and idiomatic.
| # | Concept | Type | Used In |
|---|---|---|---|
| 1 | If-Else | Conditional | Dart logic + Flutter widget tree |
| 2 | Switch Case | Multi-branch | Dart logic |
| 3 | For Loop | Iteration | Dart logic + Flutter widget lists |
| 4 | While Loop | Iteration | Dart logic |
| 5 | Do-While Loop | Iteration | Dart logic |
| 6 | Ternary Operator | Inline conditional | Dart logic + Flutter widget tree |
| 7 | Collection If | Conditional widget | Flutter widget tree |
| 8 | Collection For | Iterative widget | Flutter widget tree |
| 9 | Spread Operator | List merging | Dart + Flutter widget lists |
| 10 | Switch Expression | Expression-based | Dart 3.0+ |
| 11 | Null-Coalescing text text | Null fallback | Dart logic + Flutter |
| 12 | Null-Aware Access text | Safe access | Dart logic + Flutter |
| 13 | break / continue | Loop control | Dart logic |
1. If-Else Condition
The most fundamental control flow — executes code based on a boolean condition.
dart// Basic if-else int score = 75; if (score >= 90) { print('Grade: A'); } else if (score >= 75) { print('Grade: B'); } else if (score >= 60) { print('Grade: C'); } else { print('Grade: F'); }
In Flutter — conditional widget rendering:
dartWidget build(BuildContext context) { bool isLoggedIn = true; return Column( children: [ if (isLoggedIn) const Text('Welcome back!') else const Text('Please log in'), // Multi-widget if-else using blocks if (_isLoading) const CircularProgressIndicator() else if (_hasError) const Text('Something went wrong') else ListView.builder( itemCount: _items.length, itemBuilder: (_, i) => ListTile(title: Text(_items[i])), ), ], ); }
Use when: You need to branch logic based on a condition, or conditionally show/hide a single widget.
2. Switch Case
Matches a value against multiple cases — cleaner than many
else ifdartString day = 'Monday'; switch (day) { case 'Monday': case 'Tuesday': case 'Wednesday': case 'Thursday': case 'Friday': print('Weekday'); break; case 'Saturday': case 'Sunday': print('Weekend'); break; default: print('Unknown day'); }
With enums (most common Flutter usage):
dartenum AppStatus { loading, success, error, empty } Widget _buildContent(AppStatus status) { switch (status) { case AppStatus.loading: return const CircularProgressIndicator(); case AppStatus.success: return const Text('Data loaded!'); case AppStatus.error: return const Text('Error occurred'); case AppStatus.empty: return const Text('No data found'); } }
Use when: Comparing one variable against multiple known values — especially with enums or string constants.
3. For Loop
Iterates a fixed number of times, or over a collection.
dart// Standard for loop for (int i = 0; i < 5; i++) { print('Step $i'); } // for-in — clean iteration over any Iterable final fruits = ['Apple', 'Banana', 'Cherry']; for (final fruit in fruits) { print(fruit); } // forEach — functional style fruits.forEach((fruit) => print(fruit));
In Flutter — generating widget lists:
dart// for loop inside children Widget build(BuildContext context) { return Column( children: [ for (int i = 1; i <= 5; i++) ListTile(title: Text('Item $i')), ], ); } // for-in to map model objects to widgets final List<String> categories = ['Dart', 'Flutter', 'State', 'Testing']; Column( children: [ for (final cat in categories) Chip(label: Text(cat)), ], )
Use when: You need index-based iteration (
), clean iteration over collections (textfor), or generating repeated widgets.textfor-in
4. While Loop
Repeats as long as a condition is true. Condition is checked before each iteration.
dartint count = 0; while (count < 5) { print('Count: $count'); count++; } // Practical: polling until a condition is met while (!isConnected) { await Future.delayed(Duration(seconds: 1)); isConnected = await checkConnection(); }
With async in Flutter:
dartFuture<void> retryUntilSuccess() async { int attempts = 0; bool success = false; while (!success && attempts < 3) { try { await apiCall(); success = true; } catch (_) { attempts++; await Future.delayed(const Duration(seconds: 2)); } } }
Use when: You don't know how many iterations are needed upfront — condition-driven repetition.
5. Do-While Loop
Like
whiledartint number; do { number = Random().nextInt(10); print('Got: $number'); } while (number != 7); // Keeps going until we roll a 7
Practical use — input validation:
dartString input; do { input = getUserInput(); } while (input.isEmpty); // Always asks at least once
Use when: The loop body must run at least once regardless of the condition (e.g., showing a prompt before validating).
6. Ternary Operator
A one-line
if-elsedart// Syntax: condition ? valueIfTrue : valueIfFalse int age = 20; String status = age >= 18 ? 'Adult' : 'Minor'; // Nested ternary (use sparingly — hurts readability) String grade = score >= 90 ? 'A' : score >= 75 ? 'B' : 'C';
In Flutter — inline widget switching:
dartWidget build(BuildContext context) { return Column( children: [ // ✅ Clean single-line conditional widget Text(_isOnline ? 'Online' : 'Offline'), // ✅ Switch between two widgets _isLoading ? const CircularProgressIndicator() : ElevatedButton( onPressed: _submit, child: const Text('Submit'), ), // ✅ Conditional styling Container( color: _hasError ? Colors.red : Colors.green, child: const Text('Status'), ), ], ); }
Use when: You need a simple two-option inline expression. Avoid nesting ternaries — use
ortextif-elseinstead.textswitch
7. Collection If
Dart's
ifdart// In a plain Dart list bool showBonus = true; final items = [ 'Base salary', 'Health insurance', if (showBonus) 'Annual bonus', // Included only if showBonus is true if (showBonus) 'Stock options', // Another conditional item ];
In Flutter — most common use case:
dartWidget build(BuildContext context) { bool isAdmin = true; bool isPremium = false; return Drawer( child: Column( children: [ const ListTile(title: Text('Home')), const ListTile(title: Text('Profile')), // Conditionally include menu items if (isAdmin) const ListTile(title: Text('Admin Panel')), if (isPremium) const ListTile(title: Text('Premium Features')) else ListTile( title: const Text('Upgrade to Premium'), onTap: _showUpgradeDialog, ), ], ), ); }
Use when: You want to conditionally include one or more widgets in a list without wrapping in a ternary or extracting a method.
8. Collection For
Dart's
formap().toList()dart// Generate a list with collection for final numbers = [1, 2, 3, 4, 5]; final squares = [for (final n in numbers) n * n]; // [1, 4, 9, 16, 25] // With index — use for loop form final labels = [ for (int i = 0; i < 5; i++) 'Item ${i + 1}', ]; // ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5']
In Flutter — generating repeated widgets:
dartfinal List<String> tabs = ['All', 'Active', 'Completed']; final List<Product> products = [...]; Widget build(BuildContext context) { return Column( children: [ // Generate tab chips Row( children: [ for (final tab in tabs) Padding( padding: const EdgeInsets.only(right: 8), child: ChoiceChip(label: Text(tab), selected: false), ), ], ), // Generate product cards for (final product in products) ProductCard(product: product), // With index — collection for with counter for (int i = 0; i < products.length; i++) ListTile( leading: Text('${i + 1}'), title: Text(products[i].name), ), ], ); }
Use when: You want to inline-generate multiple widgets from a list — cleaner than
.text.map((e) => ...).toList()
9. Spread Operator
The
......?nulldartfinal basics = ['Dart', 'Flutter']; final advanced = ['Riverpod', 'Clean Architecture']; // Merge two lists final allTopics = [...basics, ...advanced]; // ['Dart', 'Flutter', 'Riverpod', 'Clean Architecture'] // Null-aware spread — safe when list might be null List<String>? extras; final safe = [...basics, ...?extras]; // extras is null — no crash
In Flutter — merging widget lists:
dartfinal List<Widget> baseActions = [ IconButton(icon: const Icon(Icons.search), onPressed: _search), IconButton(icon: const Icon(Icons.notifications), onPressed: _notify), ]; final List<Widget> adminActions = [ IconButton(icon: const Icon(Icons.settings), onPressed: _settings), IconButton(icon: const Icon(Icons.delete), onPressed: _delete), ]; AppBar( actions: [ ...baseActions, if (isAdmin) ...adminActions, // Spread + Collection If combined ], ) // Combining dynamic widget groups Column( children: [ ...headerWidgets, ...?optionalBannerWidgets, // null-aware spread ...contentWidgets, ], )
Use when: You need to merge multiple widget lists or conditionally inject a group of widgets into a parent list.
10. Switch Expression (Dart 3.0+)
Dart 3.0 introduced
switchdart// Old way — switch statement (requires variable + break) String result; switch (status) { case 'active': result = 'Active User'; break; default: result = 'Inactive'; } // ✅ New way — switch expression (Dart 3.0+) String result = switch (status) { 'active' => 'Active User', 'inactive' => 'Inactive User', 'banned' => 'Banned User', _ => 'Unknown', // _ is the default };
With enums — exhaustive checking:
dartenum ConnectionState { connecting, connected, disconnected, error } // Dart compiler ensures ALL cases are handled — compile error if you miss one Widget buildStatus(ConnectionState state) => switch (state) { ConnectionState.connecting => const CircularProgressIndicator(), ConnectionState.connected => const Icon(Icons.wifi, color: Colors.green), ConnectionState.disconnected => const Icon(Icons.wifi_off), ConnectionState.error => const Icon(Icons.error, color: Colors.red), };
In Flutter — inline widget selection:
dartWidget build(BuildContext context) { return switch (_status) { AppStatus.loading => const Center(child: CircularProgressIndicator()), AppStatus.empty => const Center(child: Text('Nothing here yet')), AppStatus.error => ErrorView(message: _errorMessage), AppStatus.success => ProductGrid(items: _items), }; }
Use when: Dart 3.0+ project and you need to return a value from a multi-branch condition — especially with enums. More concise and exhaustive than switch statements.
11. Null-Coalescing Operators: text??
and text??=
????=??null??=nulldart// ?? — null fallback String? name; String displayName = name ?? 'Guest'; // 'Guest' because name is null // Chaining String result = a ?? b ?? c ?? 'default'; // First non-null value // ??= — assign only if null String? cached; cached ??= 'computed value'; // Assigned because cached is null cached ??= 'another value'; // NOT assigned — cached already has a value print(cached); // 'computed value'
In Flutter — safe defaults for optional data:
dartWidget build(BuildContext context) { final user = _currentUser; return Column( children: [ Text(user?.name ?? 'Anonymous'), // Safe name fallback Text(user?.email ?? 'No email provided'), // Safe email fallback // Null-coalescing in widget selection user?.avatarUrl != null ? CachedNetworkImage(imageUrl: user!.avatarUrl!) : const CircleAvatar(child: Icon(Icons.person)), // ??= for lazy initialization in state Text(_cachedLabel ??= _computeLabel()), ], ); }
Use when: You need a fallback value for a nullable variable — the most idiomatic Dart way to handle optional data.
12. Null-Aware Access: text?.
?.The
?.nulldartUser? user; // Without null-aware — crashes if user is null // print(user.name); // ❌ Null check operator used on a null value // With null-aware — returns null safely print(user?.name); // null (no crash) print(user?.address?.city); // Chained — null if any link is null // Combined with ?? String city = user?.address?.city ?? 'Unknown city';
In Flutter — conditional method calls and property access:
dartvoid _onButtonTap() { _scrollController?.animateTo( 0, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); // Only called if _scrollController is not null } Widget build(BuildContext context) { return Column( children: [ Text(widget.subtitle ?? ''), // safe access Text(_profile?.displayName ?? 'You'), // null-aware chain ], ); }
Use when: Accessing properties or calling methods on a nullable object — prevents null dereference without verbose null checks.
13. break and continue
Loop control statements —
breakcontinuedart// break — exit loop early for (int i = 0; i < 10; i++) { if (i == 5) break; // Stops at 5 — prints 0,1,2,3,4 print(i); } // continue — skip an iteration for (int i = 0; i < 10; i++) { if (i % 2 == 0) continue; // Skip even numbers print(i); // Prints only: 1, 3, 5, 7, 9 } // Practical: finding first match List<Product> products = [...]; Product? found; for (final product in products) { if (product.id == targetId) { found = product; break; // No need to check remaining items } } // Practical: filtering during iteration List<String> results = []; for (final item in rawData) { if (item.isEmpty) continue; // Skip empty strings if (item.startsWith('#')) continue; // Skip comments results.add(item.trim()); }
Use when:
— stop as soon as a condition is met (early exit).textbreak— skip invalid/irrelevant items without nesting extratextcontinueblocks inside loops.textif
Quick Reference — When to Use What
| Scenario | Best Choice |
|---|---|
| Simple true/false branch | text |
| One variable vs many values | text text |
| Return value from multi-branch | text |
| Inline two-option expression | Ternary text |
| Conditionally include widget | Collection text |
| Generate repeated widgets from list | Collection text |
| Merge widget lists | Spread text |
| Null fallback value | text |
| Safe property access on nullable | text |
| Fixed iterations / indexed loop | text |
| Condition-driven repetition | text |
| Run at least once then check | text |
| Exit loop early | text |
| Skip current iteration | text |