Skip to main content

Module refresh_indicator

Module refresh_indicator 

Source
Expand description

M3E pull-to-refresh: a wrapper around a scrollable child that shows the crate::loading_indicator spinner while the app’s async refresh runs, triggered by pulling past the baseline’s own overscroll trigger.

§Architecture: this widget owns the frust::scroll_view

Upstream wraps an app-supplied Scrollable descendant via NotificationListener, observing whatever scroll surface happens to sit underneath. The framework’s public surface has no notification-bubbling seam like that — the two seams a scroll surface exposes are frust::ScrollView::on_scroll and frust::ScrollView::on_refresh_release, both installed on the view at construction, not observed after the fact. So refresh_indicator takes the child content (not an already-built ScrollView) and builds the scroll_view itself, installing both callbacks internally — the same shape crate’s frust_shadcn::scroll_area precedent uses for the one other in-tree wrapper over this baseline.

§Pull-progress mapping

Upstream’s drag mapping normalizes the raw drag delta against 25% of the scrollable’s containerExtent (_checkDragOffset, m3e_refresh_indicator_scroll.dart:248-279) — a value ScrollInfo does not carry (no viewport dimension crosses this framework’s seam). The port maps ScrollInfo::overscroll (already resistance-applied by ScrollView’s own rubber-band, negative past the top) directly in logical px instead: [visual_pull] clamps -overscroll to displacement + indicator_height, the same “resting refresh position” target upstream’s own curve converges on. This is a simpler curve, not a functionality loss — document, not a LIMITATIONS.md entry.

§Refresh lifecycle: controlled

refresh_indicator’s on_refresh callback fires exactly once per pull released past the baseline’s trigger — frust::ScrollView’s own REFRESH_TRIGGER_PX (64px, post-resistance) is the arming threshold, not a value this crate restates. Unlike upstream’s Future-returning onRefresh, which the indicator itself awaits via a Completer, this port cannot hold an arbitrary async task inside a widget (there is no widget-local executor seam — see docs/CORE_ARCHITECTURE.md’s spawn/spawn_blocking routing). The lifecycle is controlled instead, matching this crate’s Checkbox/Slider convention: on_refresh returns control to the app immediately; the app flips its own refreshing state to true (typically inside on_refresh, spawning the real async work alongside it) and feeds it back through RefreshIndicatorView::refreshing on the next rebuild. The indicator shows its spinner for as long as refreshing reads true, and only ends it (scaling away) on a rebuild where the app confirms refreshing = false — an app that never flips it back leaves the spinner showing forever, the same contract violation an unconfirmed Checkbox toggle is.

A second release-past-trigger while a refresh is already in flight does not re-invoke on_refresh (in_flight, guarded at the callback itself) — the underlying scroll content stays interactive during a refresh (see the next section), so a fast second pull is a real, reachable case, not a hypothetical one.

§Documented seam gap: the scroll offset is not held during a refresh

Upstream visually “holds” the pulled content at the indicator’s resting offset for the whole refresh, tied to the same _positionController that drives the indicator. ScrollView’s public seam has no “pin the offset until released” affordance — its own release-settle always eases back to the clamped edge once the pointer lifts, independent of whatever this wrapper is doing. The port accepts this: the indicator overlays its own resting position via Widget::paint-time compositing (translate + optional scale, frust::authoring::PaintScene::push_transform) while the scroll content underneath is free to settle and scroll normally during a refresh. FINDING candidate: a ScrollView seam to suppress the release-settle until a caller releases it would close this gap properly; per the plugin charter this crate does not patch frust-widgets to add it.

A second, narrower gap: frust::ScrollView::on_scroll delivers an animation-driven change (the release-settle) one event late (documented on that method) rather than every paint frame, so this widget’s own reveal never rides the baseline’s settle motion — it runs its own snap/retract AnimationControllers from the pull distance observed at release/cancel instead, independent of whatever the underlying surface does afterward.

§Variants

refresh_indicator is the expressive default (crate::LoadingIndicatorVariant::Default); RefreshIndicatorView::contained switches to crate::LoadingIndicatorVariant::Contained (both delegate their MaterialTokens color resolution to crate::loading_indicator itself — this module never overrides a color). Upstream’s material and adaptive variants (RefreshProgressIndicator, CupertinoActivityIndicator) are out of scope: only the two M3E-shaped ones plus RefreshIndicatorView::no_spinner (upstream’s M3ERefreshIndicator.noSpinner — the trigger/lifecycle contract with no visual at all, for an app that draws its own affordance elsewhere) are ported.

Structs§

RefreshIndicatorView
A declarative M3E pull-to-refresh wrapper. See the module docs.
RefreshIndicatorWidget
The retained widget for a RefreshIndicatorView. See the module docs.

Functions§

refresh_indicator
Wrap child in a pull-to-refresh surface, firing on_refresh once per pull released past the baseline’s trigger. See the module docs for the controlled refreshing lifecycle contract.