material_async_button 4.0.0 copy "material_async_button: ^4.0.0" to clipboard
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.dart to package:material_ui/material_ui.dart (^1.6.0). All Material types in the public API (ButtonStyle, ThemeExtension, IconAlignment, WidgetStatesController, VisualDensity, MaterialTapTargetSize, etc.) now come from package:material_ui/material_ui.dart. Your app must also import package:material_ui/material_ui.dart (dart fix --apply --code=migrate_design_widgets); with package:flutter/material.dart, the buttons read material_ui's default Theme and ignore your app's ThemeData.
  • Changing a button's controller mid-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 minLoadingDuration floor: the run and its floor belong to the controller, which stays loading until the floor elapses or you reset() or dispose() it.
  • AsyncButtonController.trigger() and reset() throw a StateError after dispose() 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 the onPressed of the AsyncButton it is bound to; attach() was a test hook that let a caller replace that callback behind the bound button's back.
  • debugLineBoxCache is no longer part of the public API.
  • The widget classes are closed: AsyncMaterialButton is sealed (like AsyncStandardMaterialButton), and ElevatedAsyncButton, FilledAsyncButton, OutlinedAsyncButton, TextAsyncButton, IconAsyncButton, FloatingActionAsyncButton, AsyncButton, AsyncButtonTheme and AsyncButtonSpinner are final. Compose them instead of extending or implementing them. AsyncButtonController stays open: extend it, or fake it only for code that calls trigger(); a fake that implements it can't be passed to a button.
  • AsyncButtonController.trigger() throws a StateError when no AsyncButton is 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: false or onPressed: null) still makes trigger() 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 its minLoadingDuration) and sees its error, so await controller.trigger(); Navigator.pop(context); no longer pops while the save is still running. It still never starts a second run.
  • One AsyncButtonController bound to two mounted buttons throws a StateError once 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's onPressed.

Fixed #

  • minLoadingDuration now uses a cancellable Timer instead of Future.delayed, so reset(), dispose(), or unmounting a button that owns its controller (no controller passed) mid-floor cancels the timer immediately without leaving pending timers or blocking trigger() callers.
  • IconAsyncButton with maintainSize: true and isSelected: true now overlays selectedIcon (when provided) instead of icon so the button keeps its selected footprint while loading. Like IconButton, only Material 3 shows selectedIcon; under Material 2 a selected button shows icon.
  • FloatingActionAsyncButton.extended with isExtended: false and no icon now renders the loading spinner in the icon slot instead of the collapsed zero-width label slot.
  • The AsyncButtonTheme docs, README and skill pass loadingBuilder and transitionBuilder as top-level functions: an inline closure makes every ThemeData built in build() unequal, so MaterialApp restarts its theme animation on every rebuild.

3.1.0 - 2026-10-04 #

Added #

  • IconAsyncButton (all four constructors) forwards onHover, onLongPress (dropped while loading) and statesController to its IconButton.
  • TextAsyncButton.isSemanticButton, forwarded to TextButton.
  • maintainSize on AsyncButtonTheme, AsyncButton and every wrapper: the loading view overlays the invisible idle child, so the button keeps its idle footprint while loading (.icon constructors keep their icon).
  • minLoadingDuration on AsyncButtonTheme, AsyncButton and 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 AsyncButtonController bound 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: null with an onLongPress keeps the button enabled for long-press, as in Flutter.
  • The agent skill moved to skills/material-async-button-usage/ for dart run skills@ get (was skills/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 returning AsyncButtonSpinner).
  • 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: canTrigger is false and trigger() is a no-op.
  • trigger() after dispose() never runs onPressed.
  • A selected IconAsyncButton (isSelected: true with a selectedIcon) shows the spinner while loading.
  • A collapsed extended FAB (FloatingActionAsyncButton.extended with isExtended: false) shows the spinner while loading.
  • Every .icon / .tonalIcon constructor accepts enabled.
  • The default spinner no longer leaks a TextPainter per 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 #

  • AsyncStandardMaterialButton is now sealed and can no longer be extended outside the package; compose AsyncButton for 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 TextPainter cache 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.lerp by using covariant for the other parameter.
  • Added comprehensive test coverage for AsyncButtonController disposal during an active asynchronous operation and external controller swapping.

2.1.0 #

Added #

  • New FloatingActionAsyncButton wrapper. Adds drop-in async loading states to FloatingActionButton with support for all four core variants: standard, .small, .large, and .extended.
  • Improved accessibility for screen readers. Added semanticsLabel parameter to AsyncButtonSpinner (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. .icon buttons take max(iconSize, lineBox), text-only buttons take the line box, icon-only buttons keep iconSize (unchanged). The line box is measured with the ambient TextScaler, 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: iconSize for IconAsyncButton, max(iconSize, fontSize) for the .icon constructors (the idle row height), and fontSize for text-only buttons (unchanged). Sizes are read from the button's resolved IconTheme / DefaultTextStyle, so a per-widget iconSize and 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 AsyncButtonStatus entirely (the state is now a plain bool isLoading), along with onSuccess, onError, successBuilder, errorBuilder, feedbackDuration, cooldownDuration, markSuccess, markError, haptic / HapticOn, and announce / 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.
  • onPressed throws now re-propagate. When onPressed throws, the button returns to idle and re-throws so the error reaches FlutterError.onError / your runZonedGuarded zone. controller.trigger() rethrows instead of completing normally.
  • AsyncButtonController is now a read-only ValueListenable<bool> (ChangeNotifier implements ValueListenable<bool>). There is no public value setter — drive it with trigger() / reset() and observe with addListener / ValueListenableBuilder<bool>. Builders receive bool isLoading instead of an AsyncButtonStatus.
  • AsyncButtonTheme carries only loadingBuilder + transitionBuilder. AsyncButtonTheme.empty (spinner-only) is the sole baseline; removed AsyncButtonTheme.material(). It complements ButtonStyle — no styling.
  • disabled flag → enabled (defaults to true). Affirmative naming that pairs with a tear-off onPressed. Either enabled: false or onPressed: null disables — both also no-op an external controller.trigger().
  • loadingChild (a Widget) → loadingBuilder (a WidgetBuilder) on both the widgets and AsyncButtonTheme.
  • Removed the loading-colour knobs (loadingForegroundColor, material(loadingColor:)). The spinner inherits each button variant's foreground; recolour a single button via AsyncButtonSpinner(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 onLongPress is gated off while busy. The button shows the disabled look only when explicitly disabled via onPressed: null.
  • .icon buttons drop the icon while loading, rendering the spinner alone.

Improved #

  • The default AsyncButtonSpinner derives 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 flutter SDK 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.

1
likes
160
points
252
downloads

Documentation

API reference

Publisher

verified publishermehmetesen.com

Weekly Downloads

Drop-in loading state for Elevated, Filled, Outlined, Text, Icon and FloatingAction Material buttons — spinner while onPressed runs, AsyncButtonTheme, AsyncButtonController.

Repository (GitHub)
View/report issues
Contributing

Topics

#button #loading #async #material #widget

License

MIT (license)

Dependencies

flutter, material_ui

More

Packages that depend on material_async_button