lazy_wrap 1.1.1
lazy_wrap: ^1.1.1 copied to clipboard
A Flutter widget that mimics Wrap layout behavior with lazy rendering and zero layout jumps.
lazy_wrap #
lazy_wrap brings lazy rendering to wrap-like Flutter layouts. It is designed
for large collections of chips, tags, cards, and tiles without eagerly building
every child.
Choose between two modes:
LazyWrap.fixedfor uniform item sizes and maximum performance.LazyWrap.dynamicfor variable or unknown item sizes.
Features #
| Feature | Fixed | Dynamic |
|---|---|---|
| Lazy rendering | ✅ | ✅ |
| Vertical and horizontal scrolling | ✅ | ✅ |
| Variable item sizes | — | ✅ |
| Offstage measurement | — | ✅ |
| Optional fade-in | — | ✅ |
| Batched loading | — | ✅ |
External ScrollController |
— | ✅ |
LazyWrap.dynamic uses exact measured row geometry, applies backpressure while
measurement is in progress, and avoids transparent fade frames during large
scroll jumps. This keeps rapid reversals responsive even with very large item
counts.
Installation #
dependencies:
lazy_wrap: ^1.1.0
import 'package:lazy_wrap/lazy_wrap.dart';
Fixed-size items #
Use LazyWrap.fixed when every item has the same width and height. This is the
fastest mode because no measurement pass is required.
LazyWrap.fixed(
itemCount: 10000,
estimatedItemWidth: 120,
estimatedItemHeight: 96,
spacing: 8,
runSpacing: 8,
padding: const EdgeInsets.all(16),
itemBuilder: (context, index) {
return ProductCard(index: index);
},
)
The dimensions supplied to fixed mode are used as the actual layout dimensions, so they should match the intended size of every child.
Variable-size items #
Use LazyWrap.dynamic when item dimensions vary or cannot be known in advance.
Items are measured offstage before they enter the visible wrap.
LazyWrap.dynamic(
itemCount: 10000,
spacing: 8,
runSpacing: 8,
padding: const EdgeInsets.all(16),
itemBuilder: (context, index) {
return Chip(label: Text('Tag $index'));
},
)
Loading and animation #
final controller = ScrollController();
LazyWrap.dynamic(
itemCount: items.length,
controller: controller,
batchSize: 100,
measureBatchSize: 20,
loadThreshold: 400,
cacheExtent: 400,
fadeInItems: true,
fadeInDuration: const Duration(milliseconds: 200),
loadingBuilder: (context) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
);
},
itemBuilder: (context, index) => VariableCard(items[index]),
)
For heavy widgets, keep measureBatchSize conservative. Increasing
cacheExtent can make scrolling smoother at the cost of additional work and
memory.
Choosing a mode #
| Scenario | Recommended mode |
|---|---|
| Uniform cards or tiles | LazyWrap.fixed |
| Chips and tags with different widths | LazyWrap.dynamic |
| Items with variable heights | LazyWrap.dynamic |
| Maximum throughput with known dimensions | LazyWrap.fixed |
Start with fixed mode whenever the dimensions are truly uniform. Otherwise, use dynamic mode with its default engine.
Dynamic API #
| Parameter | Default | Description |
|---|---|---|
spacing |
8 |
Space between items in a run |
runSpacing |
8 |
Space between wrap runs |
padding |
EdgeInsets.zero |
Padding around the content |
scrollDirection |
Axis.vertical |
Scroll axis |
cacheExtent |
300 |
Pre-render distance in pixels |
batchSize |
50 |
Items admitted per load batch |
measureBatchSize |
15 |
Maximum items measured per pass |
loadThreshold |
300 |
Remaining distance that triggers loading |
fadeInItems |
true |
Enables one-time item fade-in |
controller |
null |
Optional external scroll controller |
itemCount, spacing, runSpacing, cacheExtent, and the batch-related
values must be non-negative; batch sizes must be greater than zero.
Experimental engine #
The default LazyWrapEngine.offstageV1 is the production path. An experimental
vertical-only engine is also available for controlled evaluation:
LazyWrap.dynamic(
itemCount: 50000,
engine: LazyWrapEngine.sliverV2,
itemWidthBuilder: (index) => 120 + (index % 4) * 12,
itemHeightBuilder: (index) => 48 + (index % 3) * 8,
itemBuilder: (context, index) => Card(
child: Text('Item $index'),
),
)
sliverV2 requires itemWidthBuilder and itemHeightBuilder, supports vertical
scrolling only, and does not currently use batchSize or loadingBuilder.
Validation #
The default dynamic engine is covered by automated rapid-scroll regressions and an extreme widget test that loads 200,000 variable-size elements, reaches the last item, returns to the first, and repeatedly jumps between both edges. Device performance still depends on the complexity of each child.
The included example is also an interactive test lab with 1K–200K item presets, fixed and dynamic modes, fade and batch controls, current-edge jumps, automatic loading to the last item, and a 30-step rapid-bounce test.
Demo #

Support #
If this package helps you, consider supporting its development:
Created by Hensell.