Why I Built lazy_wrap for Flutter
I wanted a Flutter layout that creates children lazily but still lets different-sized items wrap naturally. The fixed mode is there for items with known dimensions.
By Hensell
I built lazy_wrap after running into a gap in Flutter’s collection widgets. Wrap gives children their natural size and flows them into runs, which is exactly what I want for a small collection. I needed that same behavior for a much larger collection without creating every child up front. GridView.builder creates children lazily, but assumes regular grid cells. I wanted lazy creation with wrap behavior, including when item widths vary.
I also wanted a simple option for uniform items: provide their width and height once, then let the layout wrap them. That became LazyWrap.fixed. LazyWrap.dynamic handles the cases where each item’s size is different or unknown until it is built.
Choosing between Wrap, lists, and grids
Flutter already has strong collection widgets. The right choice depends on whether your items should flow naturally, follow a regular grid, or stay in one axis—and whether you want to provide the whole child list or create children on demand.
The table describes the Flutter 3.47.9 APIs and lazy_wrap 1.1.3. It compares how they create and size children; it is not a performance ranking.
| Widget | How children are provided | Size and layout | Best fit |
|---|---|---|---|
Wrap | Complete children list. | Each child keeps its own size and flows into runs. | Small collections with varied item sizes. |
ListView.builder | itemBuilder creates children on demand. | One scrolling axis; no wrapping rows. | Large linear lists. |
GridView.count | Complete children list. | Regular, equal-sized tiles with a fixed column count. | Grids where you know how many columns you want. |
GridView.extent | Complete children list. | Regular, equal-sized tiles; set a maximum width and derive the column count. | Grids that should adapt to available width. |
GridView.builder | itemBuilder creates children on demand. | Regular grid cells described by a delegate. | Large grids with consistent cell geometry. |
lazy_wrap | itemBuilder creates children on demand. | Wrap-style flow with variable sizes or explicit fixed dimensions. | Large collections that need natural wrapping. |
For layouts outside these standard patterns, Flutter also supports custom grid delegates, but you need to describe the cell geometry yourself.
Two ways to size the items
Some collections have a predictable layout. Others don’t. lazy_wrap has a mode for each case:
LazyWrap.fixed
Use it when every child has the same width and height. You provide those dimensions, so the widget can place items without the offstage measurement used by dynamic mode. It’s a straightforward fit when the sizes really are uniform.
LazyWrap.dynamic
Use it for chips, labels, or tiles whose dimensions vary or aren’t known ahead of time. Items are measured offstage before they enter the visible wrap, so the layout can use their actual geometry.
A fixed-size example looks like this:
LazyWrap.fixed(
itemCount: products.length,
estimatedItemWidth: 120,
estimatedItemHeight: 96,
spacing: 8,
runSpacing: 8,
itemBuilder: (context, index) => ProductCard(products[index]),
)
The parameter names say “estimated,” but in fixed mode those values are used as the actual layout dimensions. They need to match the intended size of every child. If they don’t, the wrap can’t place the rows accurately.
What the dynamic mode handles
Variable-size layouts need measurement. Rather than guessing how many rows fit, LazyWrap.dynamic measures items offstage and uses their actual sizes to place them. It also supports batched loading and measurement, an external scroll controller, an optional item fade-in, and vertical or horizontal scrolling.
Those options are there for tuning, not because every screen needs them. If a child is expensive to build, keep measurement batches conservative. Increasing the cache distance can make scrolling feel smoother, but it also does more work. The complexity of each child still matters.
A fixed-size performance comparison
I compared LazyWrap.fixed with two ways to lay out the same fixed-size cards: GridView.builder, and a ListView.builder that lazily creates rows of cards. The row-based list is a manual implementation reference, not a drop-in grid widget.
The test ran on November 2, 2026, on a Mac with an Apple M4 chip, using Flutter 3.47.9 and Chrome 156 at a 1280 × 832 CSS-pixel viewport and 60 Hz. Each implementation rendered 10,000 equal-size cards with the same responsive column count, dimensions, spacing, padding, 300 px cache distance, and 90-step scroll profile. I rotated the order and ran each implementation six times.
This comparison covers fixed only. Flutter’s built-in Wrap handles variable-size flow but takes the full child list; lazy grids assume regular cells. There isn’t a built-in widget that combines lazy creation with mixed-size wrapping, so comparing dynamic with either one would change the problem being measured. The repository includes an optional stress test that traverses 200,000 variable-size items. It checks large-list behavior, not frame-rate performance.
| Implementation | p95 build (ms) | p95 raster (ms) | Jank (%) | Time to first interaction (ms) |
|---|---|---|---|---|
LazyWrap.fixed | 5.55 [4.20–8.30] | 2.45 [1.70–3.40] | 0.34 [0.00–2.99] | 275.40 [224.00–401.30] |
GridView.builder | 7.35 [5.50–11.50] | 2.95 [2.20–4.40] | 3.34 [0.67–28.24] | 304.15 [247.10–367.80] |
ListView.builder (grouped rows) | 7.20 [4.30–9.50] | 2.95 [2.00–3.50] | 1.83 [0.00–5.35] | 295.70 [251.00–381.80] |
In the November 2 Mac/Chrome test, LazyWrap.fixed had the lowest median across the four reported metrics. The ranges overlap on several measurements, and this is one browser workload on one machine—not a promise that it will be faster in every app or on every device. Here, p95 means the 95th-percentile build or raster time; jank is the share of sampled frames over the 16.67 ms budget. The benchmark notes contain the full table and explain how to reproduce the runs.
The benchmark run was captured while preparing lazy_wrap 1.1.2. The fixed implementation measured there is unchanged in 1.1.3, so these figures still describe the current release. They remain specific to the Mac/Chrome setup above.
Where it fits
For a handful of tags, Wrap is still the simpler choice. For a long one-axis list, use ListView.builder; for a regular grid, GridView.builder is a natural fit. I reach for lazy_wrap when a large collection needs to keep that natural flow. Then I choose dynamic if item sizes vary, or fixed if every item has the same known dimensions.
Try it
Install it with flutter pub add lazy_wrap, then import package:lazy_wrap/lazy_wrap.dart. The pub.dev page has installation details and API documentation. The source code and benchmark harness are on GitHub, and the interactive demo lets you try both fixed and dynamic layouts.
Comments
Post a Comment