Flutter Layouts & Responsive Design

Wrap & Flow

45 min Lesson 8 of 16

The Wrap Widget

The Wrap widget lays out its children in a horizontal or vertical run. When there is not enough space on the current run, it wraps to the next line. Unlike Row, which overflows when children exceed available space, Wrap gracefully handles overflow by creating new runs.

Key Difference: A Row places all children on one line and overflows if they don’t fit. A Wrap automatically moves overflowing children to a new line. Use Wrap whenever you have a dynamic number of children that may not fit in a single row.

Wrap vs Row: The Overflow Problem

Row Overflow vs Wrap

// ROW - causes overflow error when chips don't fit!
Row(
  children: [
    Chip(label: Text('Flutter')),
    Chip(label: Text('Dart')),
    Chip(label: Text('Firebase')),
    Chip(label: Text('Material Design')),
    Chip(label: Text('Responsive')),
    Chip(label: Text('Animation')),  // OVERFLOW!
  ],
)

// WRAP - gracefully wraps to next line
Wrap(
  spacing: 8.0,      // Horizontal gap between chips
  runSpacing: 4.0,   // Vertical gap between lines
  children: [
    Chip(label: Text('Flutter')),
    Chip(label: Text('Dart')),
    Chip(label: Text('Firebase')),
    Chip(label: Text('Material Design')),
    Chip(label: Text('Responsive')),
    Chip(label: Text('Animation')),  // Wraps to next line
  ],
)

Wrap Properties

Wrap provides several properties to control layout behavior:

Wrap Configuration

Wrap(
  // Direction: horizontal (default) or vertical
  direction: Axis.horizontal,

  // Alignment of children within a run
  alignment: WrapAlignment.start,  // start, end, center, spaceBetween, spaceAround, spaceEvenly

  // Spacing between children on the same run
  spacing: 8.0,

  // Spacing between runs (lines)
  runSpacing: 12.0,

  // Alignment of runs within the Wrap itself
  runAlignment: WrapAlignment.start,

  // Cross-axis alignment of children within a run
  crossAxisAlignment: WrapCrossAlignment.center,

  // Text direction (affects layout order for RTL)
  textDirection: TextDirection.ltr,

  children: [
    // Children widgets...
  ],
)

Wrap Alignment Options

Alignment Examples

// Center-aligned tag cloud
Wrap(
  alignment: WrapAlignment.center,
  spacing: 8.0,
  runSpacing: 8.0,
  children: tags.map((tag) {
    return Chip(
      label: Text(tag),
      backgroundColor: Colors.blue.shade50,
    );
  }).toList(),
)

// Space-evenly distributed buttons
Wrap(
  alignment: WrapAlignment.spaceEvenly,
  spacing: 8.0,
  runSpacing: 12.0,
  children: [
    ElevatedButton.icon(
      onPressed: () {},
      icon: const Icon(Icons.share),
      label: const Text('Share'),
    ),
    ElevatedButton.icon(
      onPressed: () {},
      icon: const Icon(Icons.bookmark),
      label: const Text('Save'),
    ),
    ElevatedButton.icon(
      onPressed: () {},
      icon: const Icon(Icons.download),
      label: const Text('Download'),
    ),
    ElevatedButton.icon(
      onPressed: () {},
      icon: const Icon(Icons.print),
      label: const Text('Print'),
    ),
  ],
)

// Vertical wrap (top to bottom, then next column)
Wrap(
  direction: Axis.vertical,
  spacing: 8.0,
  runSpacing: 16.0,
  children: items.map((item) => Text(item)).toList(),
)
Tip: Use WrapAlignment.spaceBetween for tag clouds where you want items to spread across the full width. Use WrapAlignment.center when you want a balanced, visually centered layout.

Practical Example: Tag Cloud

Interactive Tag Cloud

class TagCloudWidget extends StatefulWidget {
  final List<String> allTags;

  const TagCloudWidget({super.key, required this.allTags});

  @override
  State<TagCloudWidget> createState() => _TagCloudWidgetState();
}

class _TagCloudWidgetState extends State<TagCloudWidget> {
  final Set<String> _selectedTags = {};

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 8.0,
      runSpacing: 8.0,
      children: widget.allTags.map((tag) {
        final isSelected = _selectedTags.contains(tag);
        return FilterChip(
          label: Text(tag),
          selected: isSelected,
          onSelected: (selected) {
            setState(() {
              if (selected) {
                _selectedTags.add(tag);
              } else {
                _selectedTags.remove(tag);
              }
            });
          },
          selectedColor: Colors.blue.shade100,
          checkmarkColor: Colors.blue,
        );
      }).toList(),
    );
  }
}

Practical Example: Responsive Button Bar

Responsive Action Bar

class ResponsiveActionBar extends StatelessWidget {
  const ResponsiveActionBar({super.key});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Wrap(
        alignment: WrapAlignment.end,
        spacing: 12.0,
        runSpacing: 8.0,
        children: [
          OutlinedButton(
            onPressed: () {},
            child: const Text('Cancel'),
          ),
          OutlinedButton(
            onPressed: () {},
            child: const Text('Save Draft'),
          ),
          FilledButton(
            onPressed: () {},
            child: const Text('Publish'),
          ),
        ],
      ),
    );
  }
}

The Flow Widget

The Flow widget provides a highly efficient way to create custom layouts. It uses a FlowDelegate to position children with absolute precision. Flow is extremely performant because it uses a transformation matrix to position children without triggering a relayout, making it ideal for animations.

Warning: Flow is a low-level widget intended for custom animated layouts. For most wrapping scenarios, Wrap is simpler and sufficient. Only use Flow when you need custom positioning logic or animated layout transitions.

Flow with FlowDelegate

class SimpleFlowDelegate extends FlowDelegate {
  final Animation<double> animation;

  SimpleFlowDelegate({required this.animation}) : super(repaint: animation);

  @override
  void paintChildren(FlowPaintingContext context) {
    final childCount = context.childCount;
    for (int i = 0; i < childCount; i++) {
      final dx = i * 60.0;
      final dy = 0.0;
      context.paintChild(
        i,
        transform: Matrix4.translationValues(dx, dy, 0),
      );
    }
  }

  @override
  bool shouldRepaint(SimpleFlowDelegate oldDelegate) {
    return animation != oldDelegate.animation;
  }

  @override
  Size getSize(BoxConstraints constraints) {
    return Size(constraints.maxWidth, 60.0);
  }
}

// Usage
Flow(
  delegate: SimpleFlowDelegate(animation: _animationController),
  children: List.generate(5, (index) {
    return Container(
      width: 50.0,
      height: 50.0,
      decoration: BoxDecoration(
        color: Colors.primaries[index % Colors.primaries.length],
        shape: BoxShape.circle,
      ),
      child: Center(
        child: Text(
          '\${index + 1}',
          style: const TextStyle(color: Colors.white),
        ),
      ),
    );
  }),
)

Practical Example: Animated Circular Menu

Circular Flow Menu

class CircularMenuDelegate extends FlowDelegate {
  final Animation<double> animation;
  final double radius;

  CircularMenuDelegate({
    required this.animation,
    this.radius = 100.0,
  }) : super(repaint: animation);

  @override
  void paintChildren(FlowPaintingContext context) {
    final childCount = context.childCount;
    final angleStep = (pi * 0.5) / (childCount - 1);

    for (int i = 0; i < childCount; i++) {
      if (i == childCount - 1) {
        // Last child is the main button (center)
        context.paintChild(i);
      } else {
        // Fan out menu items in a quarter circle
        final angle = i * angleStep;
        final progress = animation.value;
        final dx = -cos(angle) * radius * progress;
        final dy = -sin(angle) * radius * progress;

        context.paintChild(
          i,
          transform: Matrix4.translationValues(dx, dy, 0)
            ..scale(progress, progress),
          opacity: progress,
        );
      }
    }
  }

  @override
  bool shouldRepaint(CircularMenuDelegate oldDelegate) {
    return animation != oldDelegate.animation;
  }

  @override
  Size getSize(BoxConstraints constraints) {
    return const Size(56.0, 56.0);
  }
}

// Usage in a StatefulWidget with AnimationController
class CircularMenu extends StatefulWidget {
  const CircularMenu({super.key});

  @override
  State<CircularMenu> createState() => _CircularMenuState();
}

class _CircularMenuState extends State<CircularMenu>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  bool _isOpen = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
  }

  void _toggle() {
    setState(() {
      _isOpen = !_isOpen;
      if (_isOpen) {
        _controller.forward();
      } else {
        _controller.reverse();
      }
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Flow(
      delegate: CircularMenuDelegate(
        animation: _controller,
        radius: 120.0,
      ),
      children: [
        _buildMenuItem(Icons.camera_alt, Colors.red),
        _buildMenuItem(Icons.photo, Colors.green),
        _buildMenuItem(Icons.videocam, Colors.blue),
        // Main toggle button (last child)
        FloatingActionButton(
          onPressed: _toggle,
          child: AnimatedIcon(
            icon: AnimatedIcons.menu_close,
            progress: _controller,
          ),
        ),
      ],
    );
  }

  Widget _buildMenuItem(IconData icon, Color color) {
    return FloatingActionButton(
      mini: true,
      backgroundColor: color,
      onPressed: () {
        _toggle();
        // Handle menu item tap
      },
      child: Icon(icon),
    );
  }
}
Tip: The key advantage of Flow over other layout widgets is that paintChildren uses a transformation matrix to position children. This means repositioning children does not trigger a layout pass — only a repaint — making animations extremely smooth and efficient.
Summary: Use Wrap when you need children to automatically flow to the next line when space runs out — ideal for tag clouds, chip groups, and responsive button bars. Use Flow when you need complete control over child positioning with high-performance animations — ideal for custom menus and animated layout effects.