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 columnscolumns = min(maxColumns, max(1, floor((width + gap) / (minColumnWidth + gap)))), from a LayoutBuilder (the Flutter analog of the source's ResizeObserver), 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 droppedestimateSize 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

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 (source endState).
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 (source estimateSize = () => 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 (source renderLoadingItem).
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