BeuiInfiniteMasonry<T> class
A columned, infinite-scroll masonry feed — the Flutter port of beUI's
infinite-masonry block.
Items flow into a responsive number of columns (shortest-column-first), the feed auto-loads more as you approach the end, and it renders empty / loading / end / error tail states. New items (those appended after the initial batch) reveal with a per-lane-staggered spring rise + fade.
Virtualization simplification (read this)
The source uses @tanstack/react-virtual (useVirtualizer with
lanes = columns) for windowed virtualization: it positions every item
absolutely from measured heights and only mounts the DOM nodes inside the
scroll viewport + overscan. Flutter has no equivalent — neither SliverGrid
nor flutter_staggered_grid_view does windowed, variable-height masonry with
dynamic measurement, and doing it faithfully requires a bespoke RenderSliver
with a measurement feedback loop (which risks layout jitter and never-settling
tests).
This port therefore implements a correct, eager masonry rather than true windowing, matching the observable behavior and API:
- Responsive columns —
columns = min(maxColumns, max(1, floor((width + gap) / (minColumnWidth + gap)))), from a LayoutBuilder (the Flutter analog of the source'sResizeObserver), identical formula. - Shortest-column-first distribution — items are assigned to the currently shortest lane using estimateSize, exactly as react-virtual assigns lanes from estimates before measurement. Each column is a real Flutter Column, so items lay out at their true measured heights — no overlap, no estimate-driven position drift (a fidelity improvement over absolute estimate positioning).
- Lazy at the feed level — the "infinite" behavior lives in onLoadMore: more items are fetched on demand as you scroll near the end. Per-item DOM windowing is not replicated; all currently-loaded items are built.
- overscan / estimateSize are threaded through, not dropped — estimateSize drives lane assignment and the prefetch distance; overscan widens the prefetch trigger window (its react-virtual role of enlarging the render window has no analog in an eager build, so it is repurposed to the nearest observable effect and documented here).
Trade-off: memory and build cost grow with the total number of loaded
items. For very long feeds, consumers should cap the retained items
(windowing their own data), just as they would page a ListView.
Motion
Items with index >= items.length at mount reveal (unless animateItems is
false or reduced motion is on): y: 12 → 0 on beuiSpringPanel plus
opacity: 0 → 1 over 200ms beuiEaseOut, both delayed by
min(lane, 3) * 0.04s. Each key reveals once — re-layout (e.g. a column
count change) never re-triggers it. Reduced motion drops the reveal entirely
(the source's useReducedMotion() branch).
Layout
This is a scrollable feed and expects a bounded height from its parent
(wrap it in a SizedBox/Expanded), like any vertical scroll view.
- Inheritance
-
- Object
- DiagnosticableTree
- Widget
- StatefulWidget
- BeuiInfiniteMasonry
Constructors
-
BeuiInfiniteMasonry({required List<
T> items, required BeuiMasonryKeyBuilder<T> getItemKey, required BeuiMasonryItemBuilder<T> renderItem, required FutureOr<void> onLoadMore(), required bool hasMore, bool loading = false, Widget? error, VoidCallback? onRetry, BeuiMasonrySizeEstimator<T> ? estimateSize, Widget renderLoadingItem(int index)?, Widget? emptyState, Widget? endState, double minColumnWidth = 208, int maxColumns = 4, double gap = 12, int overscan = 4, int prefetch = 3, bool animateItems = true, String ariaLabel = 'Infinite masonry feed', ScrollController? controller, Key? key}) -
Creates an infinite masonry feed.
const
Properties
- animateItems → bool
-
Whether newly appended items reveal with the stagger spring. Ignored under
reduced motion (source
animateItems = true).final - ariaLabel → String
-
Accessibility label for the feed region (source
ariaLabel).final - controller → ScrollController?
-
Optional external scroll controller. When null, an internal one is created
and disposed.
final
- emptyState → Widget?
-
Shown when the feed is empty and no more will load. Defaults to an Inbox
glyph + copy (source
emptyState).final - endState → Widget?
-
Footer shown below the grid once everything has loaded (
!hasMore) and the feed is non-empty (sourceendState).final - error → Widget?
-
When non-null, renders an error tail card (with onRetry) instead of the
loading skeletons (source
error).final -
estimateSize
→ BeuiMasonrySizeEstimator<
T> ? -
Estimated item height for lane assignment and the prefetch distance. The
real height is measured at layout. Defaults to
240(sourceestimateSize = () => 240).final - gap → double
-
Gap between columns and between items, in px (source
gap = 12).final -
getItemKey
→ BeuiMasonryKeyBuilder<
T> -
Returns the stable key for an item (source
getItemKey).final - hashCode → int
-
The hash code for this object.
no setterinherited
- hasMore → bool
-
Whether more items remain to load (source
hasMore).final -
items
→ List<
T> -
The currently loaded items, in feed order.
final
- key → Key?
-
Controls how one widget replaces another widget in the tree.
finalinherited
- loading → bool
-
Whether a load is in flight — drives the pulsing skeleton tail (source
loading).final - maxColumns → int
-
Hard cap on the responsive column count (source
maxColumns = 4).final - minColumnWidth → double
-
Minimum column width; smaller viewports collapse to fewer columns (source
minColumnWidth = 208).final -
onLoadMore
→ FutureOr<
void> Function() -
Called to fetch the next page when the feed nears its end. May be async;
the feed guards against overlapping calls (source
onLoadMore).final - onRetry → VoidCallback?
-
Retry callback shown on the error tail card (source
onRetry).final - overscan → int
-
Extra prefetch window depth. Threaded through to widen the load-more
trigger distance (source
overscan = 4; see the class docs on how its react-virtual role maps here).final - prefetch → int
-
Loads more when the feed scrolls within roughly this many items of the end
(source
prefetch = 3).final -
renderItem
→ BeuiMasonryItemBuilder<
T> -
Builds an item's card (source
renderItem).final - renderLoadingItem → Widget Function(int index)?
-
Builds a loading skeleton for the tail at
index(0-based within the tail). Defaults to a pulsing placeholder card (sourcerenderLoadingItem).final - runtimeType → Type
-
A representation of the runtime type of the object.
no setterinherited
Methods
-
createElement(
) → StatefulElement -
Creates a StatefulElement to manage this widget's location in the tree.
inherited
-
createState(
) → State< BeuiInfiniteMasonry< T> > -
Creates the mutable state for this widget at a given location in the tree.
override
-
debugDescribeChildren(
) → List< DiagnosticsNode> -
Returns a list of DiagnosticsNode objects describing this node's
children.
inherited
-
debugFillProperties(
DiagnosticPropertiesBuilder properties) → void -
Add additional properties associated with the node.
inherited
-
noSuchMethod(
Invocation invocation) → dynamic -
Invoked when a nonexistent method or property is accessed.
inherited
-
toDiagnosticsNode(
{String? name, DiagnosticsTreeStyle? style}) → DiagnosticsNode -
Returns a debug representation of the object that is used by debugging
tools and by DiagnosticsNode.toStringDeep.
inherited
-
toString(
{DiagnosticLevel minLevel = DiagnosticLevel.info}) → String -
A string representation of this object.
inherited
-
toStringDeep(
{String prefixLineOne = '', String? prefixOtherLines, DiagnosticLevel minLevel = DiagnosticLevel.debug, int wrapWidth = 65}) → String -
Returns a string representation of this node and its descendants.
inherited
-
toStringShallow(
{String joiner = ', ', DiagnosticLevel minLevel = DiagnosticLevel.debug}) → String -
Returns a one-line detailed description of the object.
inherited
-
toStringShort(
) → String -
A short, textual description of this widget.
inherited
Operators
-
operator ==(
Object other) → bool -
The equality operator.
inherited