material_async_button 4.0.0
material_async_button: ^4.0.0 copied to clipboard
Drop-in loading state for Elevated, Filled, Outlined, Text, Icon and FloatingAction Material buttons — spinner while onPressed runs, AsyncButtonTheme, AsyncButtonController.
Changelog #
4.0.0 - 2026-10-15 #
Breaking #
- Migrated from
package:flutter/material.darttopackage:material_ui/material_ui.dart(^1.6.0). All Material types in the public API (ButtonStyle,ThemeExtension,IconAlignment,WidgetStatesController,VisualDensity,MaterialTapTargetSize, etc.) now come frompackage:material_ui/material_ui.dart. Your app must also importpackage:material_ui/material_ui.dart(dart fix --apply --code=migrate_design_widgets); withpackage:flutter/material.dart, the buttons readmaterial_ui's defaultThemeand ignore your app'sThemeData. - Changing a button's
controllermid-run no longer moves the run: it stays with the controller that started it, which stays loading until the run settles, and the button shows its new controller's state. In 3.1.0 the incoming controller adopted the run, so a button swapped from no controller to one mid-run now shows idle and a tap starts a new run. - Unmounting a button no longer cancels its external controller's
minLoadingDurationfloor: the run and its floor belong to the controller, which stays loading until the floor elapses or youreset()ordispose()it. AsyncButtonController.trigger()andreset()throw aStateErrorafterdispose()in every build mode (3.1.0: a debug assertion, and a silent no-op in release).AsyncButtonController.attach()is removed. A controller runs only theonPressedof theAsyncButtonit is bound to;attach()was a test hook that let a caller replace that callback behind the bound button's back.debugLineBoxCacheis no longer part of the public API.- The widget classes are closed:
AsyncMaterialButtonissealed(likeAsyncStandardMaterialButton), andElevatedAsyncButton,FilledAsyncButton,OutlinedAsyncButton,TextAsyncButton,IconAsyncButton,FloatingActionAsyncButton,AsyncButton,AsyncButtonThemeandAsyncButtonSpinnerarefinal. Compose them instead of extending or implementing them.AsyncButtonControllerstays open: extend it, or fake it only for code that callstrigger(); a fake that implements it can't be passed to a button. AsyncButtonController.trigger()throws aStateErrorwhen noAsyncButtonis bound — the controller was never passed to one, or its button unmounted or switched to another controller. In 3.1.0 it silently did nothing. A bound but disabled button (enabled: falseoronPressed: null) still makestrigger()a no-op.AsyncButtonController.trigger()while a run is in flight returns that run's Future instead of an already-completed one:await trigger()waits for the run (and itsminLoadingDuration) and sees its error, soawait controller.trigger(); Navigator.pop(context);no longer pops while the save is still running. It still never starts a second run.- One
AsyncButtonControllerbound to two mounted buttons throws aStateErroronce the frame settles, in every build mode. In 3.1.0 this was a debug assertion, and release builds kept last-binder-wins, so tapping one button ran the other'sonPressed.
Fixed #
minLoadingDurationnow uses a cancellableTimerinstead ofFuture.delayed, soreset(),dispose(), or unmounting a button that owns its controller (nocontrollerpassed) mid-floor cancels the timer immediately without leaving pending timers or blockingtrigger()callers.IconAsyncButtonwithmaintainSize: trueandisSelected: truenow overlaysselectedIcon(when provided) instead oficonso the button keeps its selected footprint while loading. LikeIconButton, only Material 3 showsselectedIcon; under Material 2 a selected button showsicon.FloatingActionAsyncButton.extendedwithisExtended: falseand noiconnow renders the loading spinner in the icon slot instead of the collapsed zero-width label slot.- The
AsyncButtonThemedocs, README and skill passloadingBuilderandtransitionBuilderas top-level functions: an inline closure makes everyThemeDatabuilt inbuild()unequal, soMaterialApprestarts its theme animation on every rebuild.
3.1.0 - 2026-10-04 #
Added #
IconAsyncButton(all four constructors) forwardsonHover,onLongPress(dropped while loading) andstatesControllerto itsIconButton.TextAsyncButton.isSemanticButton, forwarded toTextButton.maintainSizeonAsyncButtonTheme,AsyncButtonand every wrapper: the loading view overlays the invisible idle child, so the button keeps its idle footprint while loading (.iconconstructors keep their icon).minLoadingDurationonAsyncButtonTheme,AsyncButtonand every wrapper: an anti-flicker floor that holds the loading view at least that long; an error rethrows once the floor has elapsed.
Changed #
- Requires Dart 3.13 / Flutter 3.47 (was Dart 3.10 / Flutter 3.38).
- One
AsyncButtonControllerbound to two mounted buttons now fails a debug assertion (release builds keep last-binder-wins). AsyncButtonController.reset()abandons the in-flight run: the button is idle and re-armed at once, and the abandoned run's completion is ignored.onPressed: nullwith anonLongPresskeeps the button enabled for long-press, as in Flutter.- The agent skill moved to
skills/material-async-button-usage/fordart run skills@ get(wasskills/flutter-material-async-button/).
Fixed #
- Loading buttons no longer grow to fill their parent (every non-FAB wrapper
since 2.0.0, and custom
loadingBuilders returningAsyncButtonSpinner). reset()mid-flight no longer lets the stale run clear a new run's loading state.- Swapping a button's controller mid-flight keeps its loading state: the incoming controller adopts the run that button is showing.
- An external controller detaches when its button unmounts or is swapped out:
canTriggerisfalseandtrigger()is a no-op. trigger()afterdispose()never runsonPressed.- A selected
IconAsyncButton(isSelected: truewith aselectedIcon) shows the spinner while loading. - A collapsed extended FAB (
FloatingActionAsyncButton.extendedwithisExtended: false) shows the spinner while loading. - Every
.icon/.tonalIconconstructor acceptsenabled. - The default spinner no longer leaks a
TextPainterper line-box cache miss. - The default spinner re-measures after fonts load (e.g. google_fonts): the line-box cache is cleared on every system font change.
3.0.0 #
Breaking #
AsyncStandardMaterialButtonis now sealed and can no longer be extended outside the package; composeAsyncButtonfor custom buttons.
2.1.2 #
Improved #
- Smarter line-box cache eviction. The bounded text line-box cache now evicts only its least-recently-used entry when full, instead of clearing entirely — cycling through many text styles no longer re-measures every cached style.
- Added test coverage for
AsyncButtonSpinner.strokeWidth.
2.1.1 #
Fixed #
- Avoid layout cache thrashing. Replaced the singleton
TextPaintercache with a bounded Map. This fixes a performance issue where the cache would thrash (and incorrectly re-measure) if multiple buttons with different text styles were rendered in the same frame. - Dead code warning. Fixed a static analysis warning in
AsyncButtonTheme.lerpby usingcovariantfor theotherparameter. - Added comprehensive test coverage for
AsyncButtonControllerdisposal during an active asynchronous operation and external controller swapping.
2.1.0 #
Added #
- New
FloatingActionAsyncButtonwrapper. Adds drop-in async loading states toFloatingActionButtonwith support for all four core variants: standard,.small,.large, and.extended. - Improved accessibility for screen readers. Added
semanticsLabelparameter toAsyncButtonSpinner(defaulting to'Loading') to ensure screen readers actively announce the progress of asynchronous operations when buttons are clicked.
2.0.2 #
Fixed #
- Buttons no longer shrink by a sub-pixel while loading. The default spinner
now sizes to the label's line-box height (the idle content's vertical
extent) rather than the raw
fontSize, which is shorter than the rendered line..iconbuttons takemax(iconSize, lineBox), text-only buttons take the line box, icon-only buttons keepiconSize(unchanged). The line box is measured with the ambientTextScaler, so the loading view also tracks text scaling.
2.0.1 #
Fixed #
- Icon-bearing buttons no longer shrink vertically while loading. The
default spinner sizes to the button's shape instead of always to the font
size:
iconSizeforIconAsyncButton,max(iconSize, fontSize)for the.iconconstructors (the idle row height), andfontSizefor text-only buttons (unchanged). Sizes are read from the button's resolvedIconTheme/DefaultTextStyle, so a per-widgeticonSizeand theme overrides flow through.
2.0.0 #
Breaking redesign. The button now does exactly one job — show a spinner while
onPressed is in flight — and stays out of error handling, success feedback,
and styling. State collapses to a single bool (loading), and the theme becomes
a pure complement to ButtonStyle.
Breaking #
- Loading-only: no success or error state. Removed
AsyncButtonStatusentirely (the state is now a plainbool isLoading), along withonSuccess,onError,successBuilder,errorBuilder,feedbackDuration,cooldownDuration,markSuccess,markError,haptic/HapticOn, andannounce/AsyncButtonAnnouncer/defaultAsyncButtonAnnouncer. An in-button error view is a Material anti-pattern; success is handled by what your action already does (navigate away, flip the label). Both belong to your state management, not the button. onPressedthrows now re-propagate. WhenonPressedthrows, the button returns to idle and re-throws so the error reachesFlutterError.onError/ yourrunZonedGuardedzone.controller.trigger()rethrows instead of completing normally.AsyncButtonControlleris now a read-onlyValueListenable<bool>(ChangeNotifier implements ValueListenable<bool>). There is no publicvaluesetter — drive it withtrigger()/reset()and observe withaddListener/ValueListenableBuilder<bool>. Builders receivebool isLoadinginstead of anAsyncButtonStatus.AsyncButtonThemecarries onlyloadingBuilder+transitionBuilder.AsyncButtonTheme.empty(spinner-only) is the sole baseline; removedAsyncButtonTheme.material(). It complementsButtonStyle— no styling.disabledflag →enabled(defaults totrue). Affirmative naming that pairs with a tear-offonPressed. Eitherenabled: falseoronPressed: nulldisables — both also no-op an externalcontroller.trigger().loadingChild(aWidget) →loadingBuilder(aWidgetBuilder) on both the widgets andAsyncButtonTheme.- Removed the loading-colour knobs (
loadingForegroundColor,material(loadingColor:)). The spinner inherits each button variant's foreground; recolour a single button viaAsyncButtonSpinner(color: ...).
Fixed #
- Loading never disables the button. It keeps the button's themed enabled
colours — being loading and being disabled are different things. The spinner
inherits the enabled foreground for free (no more greyed indicator); taps that
can't run are silently swallowed, and
onLongPressis gated off while busy. The button shows the disabled look only when explicitly disabled viaonPressed: null. .iconbuttons drop the icon while loading, rendering the spinner alone.
Improved #
- The default
AsyncButtonSpinnerderives its size from the ambient font size, so it matches the button's label instead of a fixed 16px. - Guarded against state changes on an unmounted button.
- Corrected the
flutterSDK constraint.
1.0.1 #
- Docs: use tear-off form (
onPressed: api.save) in README examples. - CI: split workflow into format/analyze/test/pana/example, add tag-triggered pub.dev publish via OIDC trusted publishing.
1.0.0 #
Initial release. Renamed from async_button_builder to material_async_button
with a redesigned, theme-aware API.