Dart & Flutter Control Flow and Conditional Rendering Techniques

#dart#flutter#control-flow#conditional-rendering#switch#loops#collection-if#collection-for#spread-operator#ternary

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.

#ConceptTypeUsed In
1If-ElseConditionalDart logic + Flutter widget tree
2Switch CaseMulti-branchDart logic
3For LoopIterationDart logic + Flutter widget lists
4While LoopIterationDart logic
5Do-While LoopIterationDart logic
6Ternary OperatorInline conditionalDart logic + Flutter widget tree
7Collection IfConditional widgetFlutter widget tree
8Collection ForIterative widgetFlutter widget tree
9Spread OperatorList mergingDart + Flutter widget lists
10Switch ExpressionExpression-basedDart 3.0+
11Null-Coalescing
text
??
/
text
??=
Null fallbackDart logic + Flutter
12Null-Aware Access
text
?.
Safe accessDart logic + Flutter
13break / continueLoop controlDart 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:

dart
Widget 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

text
else if
chains when comparing a single variable.

dart
String 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):

dart
enum 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 (

text
for
), clean iteration over collections (
text
for-in
), or generating repeated widgets.


4. While Loop

Repeats as long as a condition is true. Condition is checked before each iteration.

dart
int 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:

dart
Future<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

text
while
, but the body executes at least once — condition is checked after each iteration.

dart
int number;

do {
  number = Random().nextInt(10);
  print('Got: $number');
} while (number != 7); // Keeps going until we roll a 7

Practical use — input validation:

dart
String 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

text
if-else
expression. Returns one of two values based on a condition.

dart
// 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:

dart
Widget 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

text
if-else
or
text
switch
instead.


7. Collection If

Dart's

text
if
inside a collection literal — adds an element to a list only when a condition is true. Extremely useful in Flutter widget trees.

dart
// 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:

dart
Widget 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

text
for
inside a collection literal — generates multiple elements inline. Removes the need for
text
map().toList()
patterns.

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:

dart
final 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

text
...
operator inserts all elements of a list into another list. The
text
...?
(null-aware spread) does the same but safely handles
text
null
.

dart
final 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:

dart
final 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

text
switch
as an expression — it returns a value directly, making it usable anywhere an expression is allowed.

dart
// 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:

dart
enum 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:

dart
Widget 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
??=

text
??
returns the right-hand value when the left is
text
null
.
text
??=
assigns only if the variable is currently
text
null
.

dart
// ?? — 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:

dart
Widget 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

text
?.
operator accesses a property or calls a method only if the object is not null — short-circuits to
text
null
if it is.

dart
User? 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:

dart
void _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 —

text
break
exits the loop entirely,
text
continue
skips the current iteration and moves to the next.

dart
// 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:

text
break
— stop as soon as a condition is met (early exit).
text
continue
— skip invalid/irrelevant items without nesting extra
text
if
blocks inside loops.


Quick Reference — When to Use What

ScenarioBest Choice
Simple true/false branch
text
if-else
One variable vs many values
text
switch case
or
text
switch expression
Return value from multi-branch
text
switch expression
(Dart 3.0+)
Inline two-option expressionTernary
text
? :
Conditionally include widgetCollection
text
if
Generate repeated widgets from listCollection
text
for
Merge widget listsSpread
text
...
Null fallback value
text
??
Safe property access on nullable
text
?.
Fixed iterations / indexed loop
text
for
Condition-driven repetition
text
while
Run at least once then check
text
do-while
Exit loop early
text
break
Skip current iteration
text
continue