staggered_flow_grid 1.0.1
staggered_flow_grid: ^1.0.1 copied to clipboard
A high-performance, zero-dependency Flutter widget that renders children in a staggered waterfall grid layout using a custom shortest-column RenderObject.
staggered_flow_grid #
A high-performance, zero-dependency Flutter widget that renders children in a staggered (Pinterest / waterfall) grid layout.
Built with a pure custom RenderBox — strictly no external packages required.
Features #
- Shortest-column algorithm — each child is placed in the column with the smallest current height, producing a balanced waterfall effect.
- Zero dependencies — only
flutter/rendering.dart. - Varying aspect ratios — children self-size vertically; no height hints needed.
- RTL support — column ordering automatically mirrors in right-to-left
locales via the ambient
Directionalitywidget. SingleChildScrollViewcompatible — computes its own intrinsic height.computeDryLayoutsupport — correct intrinsic sizing insideIntrinsicHeight/Columnparents.- All platforms — Android, iOS, Web, macOS, Windows, Linux.
Installation #
# pubspec.yaml
dependencies:
staggered_flow_grid: ^1.0.0
flutter pub get
Quick start #
import 'package:staggered_flow_grid/staggered_flow_grid.dart';
StaggeredFlowGrid(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: [
Container(color: Colors.red, height: 120),
Container(color: Colors.green, height: 200),
Container(color: Colors.blue, height: 160),
Container(color: Colors.yellow, height: 100),
],
)
API #
StaggeredFlowGrid #
| Parameter | Type | Default | Description |
|---|---|---|---|
crossAxisCount |
int |
— | Required. Number of columns (≥ 1). |
mainAxisSpacing |
double |
0.0 |
Vertical gap between items in the same column. |
crossAxisSpacing |
double |
0.0 |
Horizontal gap between adjacent columns. |
textDirection |
TextDirection? |
null |
Column order; defaults to ambient direction. |
children |
List<Widget> |
[] |
Child widgets of any height. |
Column width is calculated as:
columnWidth = (totalWidth − (crossAxisCount − 1) × crossAxisSpacing) / crossAxisCount
Usage examples #
Inside SingleChildScrollView #
SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(12),
child: StaggeredFlowGrid(
crossAxisCount: 3,
mainAxisSpacing: 6,
crossAxisSpacing: 6,
children: myWidgets,
),
),
)
Inside a Sliver scroll view #
CustomScrollView(
slivers: [
SliverAppBar(title: const Text('Gallery')),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(12),
child: StaggeredFlowGrid(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: myWidgets,
),
),
),
],
)
Network images with varying aspect ratios #
StaggeredFlowGrid(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: imageUrls.map((url) =>
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(url, fit: BoxFit.cover),
),
).toList(),
)
RTL layout #
Column ordering automatically mirrors when your app runs in a right-to-left
locale (e.g., Arabic, Hebrew) via the ambient Directionality. You can also
override it explicitly:
StaggeredFlowGrid(
crossAxisCount: 3,
textDirection: TextDirection.rtl, // explicit override
children: myWidgets,
)
Performance tip — RepaintBoundary #
Wrap expensive children in RepaintBoundary to isolate their repaint regions:
StaggeredFlowGrid(
crossAxisCount: 3,
children: myHeavyWidgets
.map((w) => RepaintBoundary(child: w))
.toList(),
)
Architecture #
lib/
├── staggered_flow_grid.dart ← public barrel export
└── src/
├── render_staggered_flow_grid.dart ← RenderBox + algorithm
└── staggered_flow_grid.dart ← MultiChildRenderObjectWidget
Layout algorithm (Shortest-Column / Waterfall) #
1. columnWidth = (availableWidth - (C-1) × crossAxisSpacing) / C
2. columnHeights = [0.0, 0.0, ... × C]
3. For each child (in insertion order):
a. shortestCol = argmin(columnHeights)
b. x = _xOffset(shortestCol, columnWidth, totalWidth, textDirection)
c. y = columnHeights[shortestCol] == 0
? 0
: columnHeights[shortestCol] + mainAxisSpacing
d. Lay out child with BoxConstraints.tightFor(width: columnWidth)
e. childParentData.offset = Offset(x, y)
f. columnHeights[shortestCol] = y + child.size.height
4. totalHeight = reduce(max, columnHeights)
Time complexity: O(n × C) per layout pass, where n = child count and C = column count. For typical column counts (2–5), the linear column scan is cache-friendly and faster in practice than a heap-based approach.
Limitations #
| Limitation | Workaround |
|---|---|
| Not lazy — all children are laid out even if off-screen | For very large lists (1000+ items), use a SliverList with a custom delegate, or virtualise with ListView.builder + StaggeredFlowGrid in chunks. |
| Requires a finite cross-axis constraint | Wrap in SizedBox, Expanded, or any width-constrained parent. |
Platform support #
| Android | iOS | Web | macOS | Windows | Linux |
|---|---|---|---|---|---|
| ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Contributing #
- Fork the repo and create a feature branch.
- Run
flutter analyzeandflutter test— both must pass with no issues. - Open a pull request with a clear description and test coverage.
License #
MIT — see LICENSE.