material_async_button 3.1.0
material_async_button: ^3.1.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 #
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.