indexd_stack_dev 0.3.9
indexd_stack_dev: ^0.3.9 copied to clipboard
A high-performance lazy-loading IndexedStack for Flutter that initializes pages only when needed.
0.3.9 #
โก Performance #
- Preloads no longer build in the same frame as a tab switch.
switchTo()followed bypreloadAdjacentPages()(orpreloadPage()) used to build the neighbours in the same frame as the incoming page, right as the transition started. Pages newly preloaded during a switch are now built once the transition finishes, or on the next frame withanimation: none. Preloads requested while idle still build immediately;isLoaded()reports them as loaded straight away. - Hidden cached pages are not re-laid out while the stack resizes. A keyboard or rotation animation changes the stack's size every frame, and every hidden cached page was laid out again each time. Hidden pages now keep their last constraints and pick up the current size when they are shown. README now recommends
MediaQuery.sizeOf/viewInsetsOfoverMediaQuery.ofso hidden pages don't rebuild either. - Controller notifications that don't change the cache no longer rebuild the stack (e.g.
reset()when only the current and preloaded pages are cached). - No clip while idle. The stack only clips while a transition runs; children are never laid out larger than the stack, so the idle clip did nothing but add a clip layer on every paint.
RenderLazyStackreuses the active/outgoing children found during layout for paint, hit-testing and semantics instead of walking the child list each time.- Removed redundant rebuild requests in
didUpdateWidget(the framework already rebuilds after it).
๐ Fixes #
- Changing
animationno longer resets every cached page's State. Each transition type wraps pages differently (nonedoesn't wrap at all), so switching types โ e.g.noneโscaleIn, orfadeโsharedAxisVerticalโ recreated every cached page from scratch. Pages are now keyed so their State (and render subtree) moves to the new wrapper.
๐งน Internal #
LazyStackControllerstores its cache as aLinkedHashSet<int>instead of aLinkedHashMap<int, bool>.
0.3.8 #
๐งน Internal #
- Stop shipping a stray
CLAUDE.mdin the published package. No API or behavior changes.
0.3.7 #
๐ฅ Breaking #
- Removed the
totalPagesparameter fromswitchTo,preloadPage, andpreloadAdjacentPages. It was the root cause of the 0.3.3 out-of-range bug: a caller-supplied number with no cross-check against the widget's realchildren.length, easy to pass stale or miscalculated.LazyStackControllernow has apageCountproperty thatLazyLoadIndexedStacksyncs automatically fromchildren.lengthโ on mount, wheneverchildren.lengthchanges, and whenever the controller is swapped โ so there's a single source of truth instead of one the caller has to keep in sync by hand at every call site.- Migration:
controller.switchTo(index, totalPages)โcontroller.switchTo(index). Same forpreloadPage(index, totalPages)โpreloadPage(index)andpreloadAdjacentPages(totalPages, [range])โpreloadAdjacentPages([range]). - Bounds checking still happens โ
switchTo/preloadPage/preloadAdjacentPagesnow reject an index>= pageCountthemselves, using the synced (correct) count, instead of relying on a debug-mode assert as the only backstop. The 0.3.3 assert stays in place as a last line of defense for the (now narrow) case of driving a controller withoutLazyLoadIndexedStackattached, or settingpageCountmanually to something wrong.
- Migration:
0.3.3 #
๐ Fixes #
LazyStackController.currentIndexgoing out of range now fails loudly in debug mode instead of silently blanking a tab. The controller has no reference toLazyLoadIndexedStack.children.lengthโswitchTo/preloadPage/preloadAdjacentPagesall take a caller-suppliedtotalPageswith no cross-check against the widget's actual child count. A stale/miscalculatedtotalPages, or a tab count that shrinks at runtime without updating it, letcurrentIndexend up pointing past the end ofchildren;RenderLazyStackwould then silently paint nothing and stop responding to hit-tests for that page, with no error anywhere. Added a debug-modeassert(zero cost in release) at both initial mount and every controller-driven index change.- Fixed a
mountedguard asymmetry in_onControllerChanged: the index-changed branch checkedmountedbefore touching state; the branch for other controller notifications (e.g.preloadPage) did not, before writing to_buildVersion(aValueNotifierdisposed alongside theState). No reproducible crash was found under normal widget lifecycle (dispose()removes the listener synchronously before anything else), but the guard now covers both branches for defensive symmetry.
๐ Docs #
- Fixed three places in
README.mdthat incorrectly documentedscaleIn's defaultpaintOrderasIndexdPaintOrder.outgoingOnTopโ the actual default (and the fourth README mention) isIndexdPaintOrder.stack.
๐งน Internal #
scaleInnow sharesRenderLazyStackwith every other animation type instead of a dedicated FlutterStack+Offstagetree._ScaleInLayer/ScaleInPageTransitionare removed;scaleIn's fade + tiny bilateral scale is unchanged, now driven by the sameStackTransitionAnimationstweens/curves the other types already used (IndexdPaintOrder.stackwas already thescaleIndefault, now actually wired throughRenderLazyStack).- Not a measured performance win. The refactor was originally motivated by a suspected relayout-boundary gap (hidden cached pages under
Stack/Offstagesupposedly unable to contain their ownmarkNeedsLayout(), forcing a full-stack relayout). Benchmarking against the pre-refactor implementation disproved this:RenderOffstagesetssizedByParent = offstage, which independently satisfies Flutter's relayout-boundary condition regardless of the parent'sparentUsesSizechoice โ so the oldStack+Offstagepath was already correctly isolating hidden pages. Verified withhidden_relayout_boundary_test.dart's newscaleIncase, which passes on both implementations. - The real payoff is code consolidation: one transition implementation instead of two near-duplicates that had already drifted once (the outgoing-ticker-freeze fix in 0.3.1 only touched the
scaleInpath and had to be reasoned about separately), plus removing a one-time (not per-switch)StackTransitionAnimationstween/curve allocation that the oldscaleInpath built viaensureBuiltbut never consumed.
- Not a measured performance win. The refactor was originally motivated by a suspected relayout-boundary gap (hidden cached pages under
0.3.2 #
โจ Updates #
scaleIncompositing: participating pages useStackFit.expand(full-bleed) by default so the quiet0.992scale reads as a viewport settle, not a floating card shrink.scaleInpaint order: defaults toIndexdPaintOrder.stack(FlutterStack: higher tab index on top). Override withpaintOrder:.scaleInuses FlutterStack+Offstage(_buildScaleInStack); other types keepRenderLazyStack. The stack is not rebuilt every frame โFadeTransition/ScaleTransitiontick on their own.- Type-default duration: omit
animationDurationandscaleInuseskScaleInDuration(320ms); fade / shared-axis usekDefaultAnimationDuration(200ms).
๐ฅ Breaking #
animationDurationis nowDuration?(null= type default). Call sites that relied on the old implicit200msforscaleInnow get320msunless they pass a duration explicitly.
๐ Docs / example #
- Exported
kScaleInCurve,kScaleInBegin,kScaleInDuration,kDefaultAnimationDuration. - Example uses
ComplexLazyStackDemowithscaleInas the default tab transition.
0.3.1 #
๐ Fixes #
- Outgoing page ticks froze too late: the outgoing page's
TickerModestayed enabled for the whole exit transition instead of freezing immediately once it started fading out. Now only the incoming/active page keeps ticking, matching the intended quiet-settle behavior exactly.
0.3.0 #
โจ Updates #
scaleInquiet iOS-style settle: pure FlutterFadeTransition+ScaleTransition, shared soft iOS curve (Cubic(0.22, 1.0, 0.36, 1.0)), fade0โ1+ tiny bilateral scale (0.992โ1.0), no slide. Prefer ~320ms.- All other
IndexdAnimationTypes stay on pure Flutter transitions (FadeTransition/ScaleTransition/SlideTransition) with cached curved animations โ no animation framework dependency.
๐ Docs #
- Product guidance: tab shells โ
scaleIn@ ~320ms (orfade@ ~200โ250ms); keepnoneas the zero-overhead package default; reserve shared-axis for directional flows. - Dartdoc on
IndexdAnimationType/LazyLoadIndexedStackdocuments recommended durations.
โป๏ธ Refactor #
- Split library into
src/by concern:widgets/,controller/,transitions/,rendering/(public API unchanged via barrel export).
0.2.0 #
โจ Updates #
scaleInquiet settle: shared soft iOS curve (Cubic(0.22, 1.0, 0.36, 1.0)), fade + tiny bilateral scale (0.992โ1.0) on both pages, still a complementary crossfade (no luminance dip).
๐ฅ Breaking #
- Removed
scaleBegin: the offset is fixed at0.992; tune feel withanimationDuration(example/docs recommend ~320ms forscaleIn).
0.1.9 #
โก Performance #
- Hidden pages are now relayout boundaries:
_RenderLazyStacklays out hidden cached pages withparentUsesSize: false, so a size change inside a hidden page (async data arriving, a growing list) relays out that page alone instead of dirtying the whole stack. Active and outgoing children are also resolved in a single child traversal instead of repeated linked-list walks. - Repaint boundaries around pages: every loaded page is wrapped in a
RepaintBoundary(at constant tree depth, so page state is preserved), letting transitions composite each page's cached raster instead of re-rasterizing both pages every animation frame. - Fewer allocations on the switch path: cached transition animations are rebuilt only when the animation type (or direction, for shared-axis) changes;
LazyStackController.loadedIndexescaches its unmodifiable view instead of copying aSetper rebuild; protected-index checks use a precomputed set.
โ Tests #
- Regression test proving hidden-page relayout stops at its own boundary.
- Regression test proving the outgoing page's
Statesurvives an eviction mid-transition (oneinitState, onedispose).
0.1.8 #
โจ Features #
- Added
IndexdAnimationType.scaleIn: a subtle iOS-style transition where the incoming page scales fromscaleBegin(default0.98, configurable0.95โ0.99) to1.0while cross-dissolving with the outgoing page. The two opacities are exact complements, so there is no Material fade-through "dip".
๐ Fixes #
switchTovalidation:LazyStackController.switchTo(index, totalPages)now validatesindexagainsttotalPages(previously ignored), preventing an out-of-range crash on negative indexes and a blank page on too-large ones.- Controller swap: swapping the
controlleronLazyLoadIndexedStacknow re-syncs the current index immediately, so the correct page renders instead of a stale/blank one until the new controller emits. - Double rebuild:
switchTowithdisposeUnused: trueno longer notifies listeners twice โ a single rebuild per switch.
๐ Docs & Example #
- README now accurately describes layout vs. paint: every attached child is laid out (Flutter's layout contract), while only the active and outgoing child are painted.
- Example: replaced deprecated
Switch.activeColorwithactiveThumbColor.
0.1.7 #
๐ Fixes #
- Layout-contract crash with cached pages:
_RenderLazyStack.performLayoutnow lays out every attached child each pass, not just the active and outgoing ones. Cached (loaded but inactive) pages are real, mounted subtrees; skipping their layout left them inNEEDS-LAYOUT, so a cached page containing aTextField(or anything whose size is later read by the hostScaffold) would throwRenderBox was not laid out: RenderEditable... NEEDS-LAYOUT. Only the active child โ and the outgoing child during a transition โ still drive the stack's size; painting and hit-testing remain restricted to those, so inactive pages stay invisible. - Semantics: Added
visitChildrenForSemanticsso only the active page is exposed to the accessibility tree, matching stockIndexedStackbehavior now that inactive pages are laid out.
0.1.5 #
โจ Updates & Polish #
- Faster Animations: Decreased default animation duration from 250ms to 200ms for a snappier feel.
- Improved Animation Curves: Changed the shared axis slide animation curve to
fastOutSlowInand adjusted the slide offset from 15% to 7%.
0.1.4 #
โจ Updates & Cleanups #
- Premium Animation Curves: Upgraded the
Fade ThroughandShared Axistransition curves tofastLinearToSlowEaseInfor a much smoother, iOS/Material 3 style spring-damped feel. - Optimized Transition Offsets: Reduced the slide offset scale in Shared Axis transitions from 15% to 8% to create a more premium, subtle depth effect.
- Smoother Cross-fades: Fine-tuned opacity intervals for cross-fading, completely eliminating brief frame flashes during dark mode transitions.
- Codebase Polish: Cleaned up all artificial comments and explanatory text for a pristine, production-ready source code.
0.1.3 #
โก Performance & Fixes #
- Strict Widget Tree Stabilization: Fixed the critical bug where tabs would rebuild on every switch. The
AnimatedBuilderwrapper depth is now perfectly stable across allIndexdAnimationTypetransitions, ensuring Flutter never unmounts the page element. - Zero Per-Frame Allocations: Eliminated all dynamic
Animationobject creation within thebuild()method. AllReverseAnimationand derived animation objects are now tightly pre-cached once per transition, guaranteeingScaleTransitionandFadeTransitionnever invokedidUpdateWidgetrebuilds during layout. - Const Inert Animations: Optimized non-participating loaded tabs to use
static const kInertScale = AlwaysStoppedAnimation(1.0)and similar static wrappers. This allocates 0 bytes of memory per frame for background tabs while perfectly maintaining widget tree depth.
0.1.1 #
๐ Fixes #
- Fixed page rebuild bug: Pages no longer rebuild continuously when animations are enabled.
AnimatedBuilderis now only attached during the active 300ms transition and stripped immediately after. - Eliminated unnecessary setState: Cache/loaded changes now use a targeted
Setequality check instead of blindly callingsetState(() {}).
0.1.0 #
โก Performance (Breaking) #
- Custom RenderObject: Replaced Flutter's native
IndexedStackwith a custom_RenderLazyStackthat skips layout computation entirely for inactive children. Only the active child participates in layout/paint. - Cached Animations: All
CurvedAnimationandTweenobjects are now created once per transition and cached as state fields โ zero per-frame allocations. - Optimized
loadedIndexes: ReturnsSet<int>.unmodifiable()instead of allocating a newSeton every access. - Eliminated double eviction:
switchTono longer calls_enforceMaxSizetwice. - Removed
dart:mathdependency: Inlined max comparisons.
โจ Features #
- Native Tab Animations: Added
IndexdAnimationTypeenum with 5 transition styles:none(zero-overhead, no AnimationController allocated)fadefadeThrough(Material Design spec)sharedAxisHorizontalsharedAxisVertical
- Dynamic Animation Toggling: Switching between
noneand animated types at runtime is fully supported with proper resource lifecycle management. - Nullable AnimationController: When
animation: IndexdAnimationType.none, noAnimationControlleris created โ true zero allocation.
๐ง Breaking Changes #
- Removed
EQprefix:EQLazyStackControllerโLazyStackController,EQLazyLoadIndexedStackโLazyLoadIndexedStack. - Removed
removableIndexes: Dead field that was never used internally. - Changed base class:
LazyLoadIndexedStackis now aStatefulWidget(wasListenableBuilder). - Removed
StackFit sizingparameter: The custom RenderObject handles sizing internally.
๐ Fixes #
- Controller now uses
ChangeNotifierinstead of manual listener management. switchToperforms synchronous eviction beforenotifyListeners()โ no double-frame builds.- Memory pressure handler (
didHaveMemoryPressure) aggressively flushes all inactive pages.