diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000..d425b02
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,157 @@
+name: CI
+
+on:
+ push:
+ pull_request:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+defaults:
+ run:
+ shell: bash
+
+jobs:
+ format:
+ name: format (${{ matrix.channel }})
+ runs-on: ubuntu-latest
+ # An unreleased framework must never be able to block a PR. `dart format`
+ # output changes between formatter versions, so beta is advisory only.
+ continue-on-error: ${{ matrix.channel == 'beta' }}
+ strategy:
+ fail-fast: false
+ matrix:
+ channel: [stable, beta]
+ steps:
+ - uses: actions/checkout@v4
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: ${{ matrix.channel }}
+ cache: true
+ # `dart format` picks its style from the package's resolved language
+ # version, so it MUST run after `pub get`. Without resolution it falls
+ # back to the SDK's own version and reformats the entire tree in the
+ # newer style, failing the check on code that is correctly formatted.
+ - name: Resolve package dependencies
+ run: flutter pub get
+ - name: Resolve example dependencies
+ run: flutter pub get
+ working-directory: example
+ - name: Check formatting
+ run: dart format --output none --set-exit-if-changed .
+
+ analyze:
+ name: analyze (${{ matrix.channel }})
+ runs-on: ubuntu-latest
+ continue-on-error: ${{ matrix.channel == 'beta' }}
+ strategy:
+ fail-fast: false
+ matrix:
+ channel: [stable, beta]
+ steps:
+ - uses: actions/checkout@v4
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: ${{ matrix.channel }}
+ cache: true
+ - name: Resolve package dependencies
+ run: flutter pub get
+ # `flutter analyze` walks the whole tree, example/ included, so the
+ # example has to be resolved too or every package: URI in it fails.
+ - name: Resolve example dependencies
+ run: flutter pub get
+ working-directory: example
+ - name: Analyze
+ run: flutter analyze --fatal-infos
+
+ test:
+ name: test (${{ matrix.channel }})
+ runs-on: ubuntu-latest
+ continue-on-error: ${{ matrix.channel == 'beta' }}
+ strategy:
+ fail-fast: false
+ matrix:
+ channel: [stable, beta]
+ steps:
+ - uses: actions/checkout@v4
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: ${{ matrix.channel }}
+ cache: true
+ - name: Resolve package dependencies
+ run: flutter pub get
+ - name: Run tests
+ run: flutter test --coverage
+ - name: Upload coverage
+ if: matrix.channel == 'stable'
+ uses: actions/upload-artifact@v4
+ with:
+ name: lcov
+ path: coverage/lcov.info
+ if-no-files-found: error
+
+ example:
+ name: example build (${{ matrix.channel }})
+ runs-on: ubuntu-latest
+ continue-on-error: ${{ matrix.channel == 'beta' }}
+ strategy:
+ fail-fast: false
+ matrix:
+ channel: [stable, beta]
+ steps:
+ - uses: actions/checkout@v4
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: ${{ matrix.channel }}
+ cache: true
+ # example/ carries lib/, test/ and pubspec.yaml only — the platform
+ # scaffolding is deliberately not checked in (see .pubignore), so it is
+ # regenerated here from the pinned Flutter version.
+ - name: Generate platform scaffolding
+ run: flutter create . --platforms=web
+ working-directory: example
+ - name: Resolve example dependencies
+ run: flutter pub get
+ working-directory: example
+ - name: Build example for web
+ run: flutter build web --release
+ working-directory: example
+
+ pana:
+ name: pana score
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: stable
+ cache: true
+ - name: Resolve package dependencies
+ run: flutter pub get
+ # pana validates `screenshots:` by converting them with libwebp
+ # (webpinfo / cwebp / dwebp / gif2webp / webpmux). pub.dev's own analysis
+ # environment has these; a bare runner does not, and every screenshot
+ # then fails with "No such file or directory", costing the 10 example and
+ # screenshot points and making the score unrepresentative.
+ - name: Install libwebp
+ run: sudo apt-get update && sudo apt-get install -y webp
+ - name: Install pana
+ run: dart pub global activate pana
+ - name: Score the package
+ # The package scores 160/160 today, so any lost point is a regression.
+ run: dart pub global run pana --no-warning --exit-code-threshold 0 .
+
+ publish-dry-run:
+ name: publish --dry-run
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: stable
+ cache: true
+ - name: Resolve package dependencies
+ run: flutter pub get
+ - name: Validate the publishable archive
+ run: flutter pub publish --dry-run
diff --git a/.gitignore b/.gitignore
index dc259f5..7df13f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,11 @@ pubspec.lock
# If you don't generate documentation locally you can remove this line.
doc/api/
+# Coverage reports, written by `flutter test --coverage` (which CI runs on
+# every push). Never committed, never published.
+coverage/
+*.lcov
+
# dotenv environment variables file
.env*
@@ -24,4 +29,8 @@ doc/api/
*.js.map
.flutter-plugins
-.flutter-plugins-dependencies
\ No newline at end of file
+.flutter-plugins-dependencies
+
+# IntelliJ / Android Studio project metadata
+.idea/
+*.iml
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 26d3352..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
deleted file mode 100644
index 7643783..0000000
--- a/.idea/codeStyles/Project.xml
+++ /dev/null
@@ -1,123 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- xmlns:android
-
- ^$
-
-
-
-
-
-
-
-
- xmlns:.*
-
- ^$
-
-
- BY_NAME
-
-
-
-
-
-
- .*:id
-
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- .*:name
-
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- name
-
- ^$
-
-
-
-
-
-
-
-
- style
-
- ^$
-
-
-
-
-
-
-
-
- .*
-
- ^$
-
-
- BY_NAME
-
-
-
-
-
-
- .*
-
- http://schemas.android.com/apk/res/android
-
-
- ANDROID_ATTRIBUTE_ORDER
-
-
-
-
-
-
- .*
-
- .*
-
-
- BY_NAME
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
deleted file mode 100644
index 79ee123..0000000
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/git_toolbox_prj.xml b/.idea/git_toolbox_prj.xml
deleted file mode 100644
index 02b915b..0000000
--- a/.idea/git_toolbox_prj.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml
deleted file mode 100644
index b6e6985..0000000
--- a/.idea/libraries/Dart_SDK.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/libraries/Flutter_Plugins.xml b/.idea/libraries/Flutter_Plugins.xml
deleted file mode 100644
index b0f6971..0000000
--- a/.idea/libraries/Flutter_Plugins.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index f4f6d79..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index c72ae90..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 35eb1dd..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.pubignore b/.pubignore
new file mode 100644
index 0000000..58f5aa1
--- /dev/null
+++ b/.pubignore
@@ -0,0 +1,58 @@
+# Files kept out of the published pub.dev archive.
+#
+# NOTE: when a .pubignore exists, pub uses it INSTEAD of the .gitignore in the
+# same directory, so everything the root .gitignore excludes has to be repeated
+# here. Nested .gitignore files (example/.gitignore) still apply as usual.
+
+# ---------------------------------------------------------------- build output
+.dart_tool/
+.packages
+build/
+pubspec.lock
+doc/api/
+.env*
+*.dart.js
+*.info.json
+*.js
+*.js_
+*.js.deps
+*.js.map
+.flutter-plugins
+.flutter-plugins-dependencies
+
+# -------------------------------------------------------------- coverage output
+# `flutter test --coverage` runs on every push (see .github/workflows/ci.yaml)
+# and writes coverage/lcov.info. `pub publish` does not warn about it, so it
+# has to be excluded explicitly or it ships in the archive.
+coverage/
+*.lcov
+
+# -------------------------------------------------------- screenshot assets
+# Only the three images named by `screenshots:` in pubspec.yaml have to be in
+# the archive. The README references every image by absolute GitHub URL, so the
+# rest would be dead weight in a package users download.
+doc/orbs.webp
+doc/families.png
+doc/button-states-dark.png
+
+# ------------------------------------------------------------------ test data
+# 610 KB of upstream golden vectors that only CI needs; the orb golden test
+# cannot run without them, so it is left out of the archive as well. Both are
+# in git and run on every push — see .github/workflows/ci.yaml.
+test/fixtures/
+test/orbs/orb_golden_test.dart
+
+# ------------------------------------------------- example platform scaffolding
+# example/lib, example/test and example/pubspec.yaml must stay in the archive:
+# pub.dev awards points for "provides an example". The per-platform runner
+# projects are pure boilerplate and are regenerated by `flutter create .`.
+example/android/
+example/ios/
+example/linux/
+example/macos/
+example/web/
+example/windows/
+
+# ------------------------------------------------------------------------ IDE
+.idea/
+*.iml
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c54f6f2..294894e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,42 +1,193 @@
+# Changelog
+
+All notable changes to this project are documented in this file. The format is
+based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
+project adheres to [semantic versioning](https://semver.org/spec/v2.0.0.html).
+
+## 1.1.0
+
+Requires **Dart >=3.4.0 / Flutter >=3.22.0**. See the
+[Migration Guide](https://github.com/itsarvinddev/loading_icon_button/blob/master/MIGRATION.md#10x--110)
+— nothing else in this release is required, and no public member was removed.
+
+### Added
+
+- `ThinkingOrb`, a family of animated indicators for AI and agent interfaces,
+ with nine `OrbState` animations (`working`, `searching`, `solving`,
+ `listening`, `connecting`, `weaving`, `composing`, `breathing`, `shaping`),
+ two size tiers, a shared clock, `TickerMode` awareness and a static frame
+ under reduced motion. Exported alongside `OrbTheme` and `kOrbSemanticLabels`.
+- `LoadingIndicator`, a sealed type with `.circular()`, `.orb()`, `.widget()`
+ and `.builder()` constructors, plus `LoadingProgressStyle`
+ (`indicator`, `fill`, `both`). Accepted by `LoadingButton.indicator` and by a
+ new `indicator` argument on every Material loading button.
+- `LoadingButtonController` and `LoadingButtonValue` for driving a button from
+ outside the widget tree: `start`, `setProgress`, `success`, `error`, `reset`,
+ `setEnabled`, `setActionState`, `press` and `startCooldown`.
+- `LoadingButtonSizing`: `legacy` (the default, unchanged 200x50 geometry),
+ `.intrinsic({constraints})`, `.expand({height})` and
+ `.fixed({width, height})`.
+- `LoadingButtonColors` with `.fromScheme()`, `.traffic()` and `.legacy`, and
+ `LoadingButtonColorStrategy` (`legacy` by default, or `material3` to derive
+ every state colour from the ambient `ColorScheme`).
+- `LoadingButtonThemeData`, a `ThemeExtension`, and the `LoadingButtonTheme`
+ inherited widget, for per-subtree and per-brightness defaults.
+- `LoadingButton` arguments: `buttonStyle`, `sizing`, `controller`,
+ `indicator`, `progress`, `progressStyle`, `colors`, `colorStrategy`,
+ `enabled`, `debounce`, `cooldown`, `focusNode`, `autofocus`, `tooltip`,
+ `transitionBuilder`, `onSuccess` and `onFailure(Object, StackTrace)`.
+- `LoadingButtonState` is now public, exposing `press()` and `currentState` for
+ use through a `GlobalKey`.
+- Matching `LoadingButtonBuilder` methods for all of the above.
+- `llms.txt`, a compact machine-readable API digest for AI coding assistants,
+ together with a README section of copy-pasteable prompts. Assistants trained
+ on the 0.0.x API confidently emit parameters that were removed in 1.0.0
+ (`iconData`, `successIcon`, `showBox`, `loaderSize`, `animateOnTap`) and the
+ pre-1.0.2 `ButtonState`; the digest lists each one with its replacement.
+
+### Changed
+
+- **The declared SDK floor is now Dart >=3.4.0 / Flutter >=3.22.0.** This is a
+ correction, not a drop in support: the package has used
+ `WidgetStatesController` (Flutter 3.22+) since 1.0.0, so the previously
+ declared `flutter: ">=1.17.0"` had been fiction for several releases. Older
+ SDKs got a confusing compile error inside the package instead of a clean
+ `pub` resolution message; they now get the resolution message.
+- `LoadingButton` no longer uses `rxdart` internally; state is a plain
+ `ValueNotifier`.
+- The default loading, success and error widgets no longer hard-code
+ `Colors.white`; they inherit the button's resolved foreground colour, which
+ every Material button publishes through `IconTheme`. The rendering is
+ identical under `LoadingButtonColorStrategy.legacy` (whose foreground is
+ white anyway) and fixes an invisible white-on-light-container icon under
+ `material3` and on outlined and text buttons.
+- The README screenshots are now real renders of the real widgets, generated by
+ `tool/generate_screenshots.dart` and packed by `tool/pack_animations.sh`.
+ The previous `generated-image.png` was a 2.5 MB promotional graphic rather
+ than a screenshot, and the code in it (`LoadingButton(icon:, onTap:)`, an
+ `AutoLoadingButton` class) matched no release of this package. It has been
+ removed; the published archive dropped from 2 MB to 879 KB.
+- `onPressed` now runs immediately on tap. It used to wait for an awaited
+ haptic round trip and a ~200 ms scale animation first.
+- Haptic feedback is fired and forgotten rather than awaited, which also stops
+ widget tests of a press from hanging on a test binding.
+- `onStateChanged` is called only for real state changes; it no longer fires a
+ spurious `ActionState.idle` when the button is first built.
+- Screen readers announce a `LoadingButton` once: the duplicate
+ `Semantics(button: true)` wrapper is gone, and transient state text is
+ announced through a `liveRegion` label on the child instead.
+- `ActionState.disabled` is now reachable, via the `enabled` argument or a
+ controller.
+- `ButtonStateExtension` is renamed `ActionStateExtension`, finishing the 1.0.2
+ rename. Member access (`state.isLoading` and friends) is unaffected, since
+ extension members resolve by member name, not by extension name; only
+ explicit extension overrides need updating.
+- `ActionStateExtension.isInteractive` now means "a press would be accepted"
+ (`!loading && !disabled`) instead of being an exact duplicate of `isIdle`.
+ Use `isIdle` for the old meaning.
+- **`ArgonTimerButton.loader` is now typed `Widget Function(int time)?`**, up
+ from `Function(int time)?`. This is a **source-breaking** narrowing: a closure
+ whose inferred return type was `dynamic` (for example one with a non-`Widget`
+ branch, or an untyped `=>` body the analyzer resolved to `dynamic`) no longer
+ assigns. Annotate the closure's return type as `Widget` — the value was always
+ used in a `child:` position, so no runtime behaviour changes.
+- **`ArgonButton` with a null `loader` now shows a default spinner** while busy.
+ It previously rendered nothing at all: `loader` was passed straight into a
+ `child:` position, where null is legal, so the button animated down to a pill
+ and sat there empty. If you relied on the empty pill, pass
+ `loader: const SizedBox.shrink()`.
+
+### Fixed
+
+- A throwing callback on an `*AutoLoadingButton` no longer leaves the button a
+ dead spinner. `doPress` and `doLongPress` share one implementation that
+ clears the loading flag on both the success and the failure path; tap-path
+ errors are reported via `FlutterError.reportError`, and callers that await
+ `doPress()` receive the error instead.
+- A fast double tap can no longer run `LoadingButton.onPressed` twice; the
+ press path is latched synchronously before the first `await`.
+- The success/error reset is a cancellable `Timer` instead of an uncancellable
+ `Future.delayed`, so a disposed button no longer fires a stale reset.
+- `LoadingButton` reads `MediaQuery.sizeOf`, so it no longer rebuilds on every
+ unrelated `MediaQuery` change.
+- `ArgonButton` no longer force-unwraps its nullable `onTap`; a null `onTap`
+ renders the button disabled instead of throwing on the first tap. The escaping
+ `startLoading`/`stopLoading` closures and the animation callbacks are
+ `mounted`-guarded.
+- `ArgonTimerButton` no longer force-unwraps its nullable `loader`, which threw
+ as soon as the countdown started when no `loader` was given.
+- `ArgonTimerButton.startTimer` throws an `ArgumentError` instead of a bare
+ `String`.
+- `buildChildWithIC` no longer pushes its label off the end of the row when the
+ icon is null, and its gap no longer adds vertical padding.
+
+### Deprecated
+
+All of these keep working until 2.0.0.
+
+- `LoadingButton.onError` and `LoadingButtonBuilder.onError` — use `onFailure`,
+ which also receives the `StackTrace`.
+- `LoadingButtonConfig` — use `LoadingButtonThemeData` with
+ `LoadingButtonTheme`, or as a `ThemeExtension` on `ThemeData`. It is still
+ read as the last fallback, so existing code is unaffected.
+- The top-level helpers in `icon_button.dart` — `IconButtonLoading`,
+ `buildChildWithIcon`, `buildChildWithIC` and `buildText`. Compose a `Row` or
+ a `Text` yourself.
+
+## 1.0.3
+
+### Fixed
+
+- A widget disposed while `onPressed` was still running no longer throws; the
+ success and error transitions are `mounted`-guarded
+ ([#8](https://github.com/itsarvinddev/loading_icon_button/pull/8)).
+
## 1.0.2
-- ButtonState into ActionState
+### Changed
+
+- **Breaking:** `ButtonState` is renamed `ActionState`, with no deprecated
+ alias. Replace `ButtonState` with `ActionState` (leave `ArgonButtonState` and
+ `LoadingButtonState` alone).
## 1.0.1
-- URL updated
+- URLs updated.
## 1.0.0
-- Full refactor of the package
-- Added new material buttons
-- Checkout the [Migration Guide](https://github.com/itsarvinddev/loading_icon_button/blob/main/MIGRATION.md) for more details
+- Full refactor of the package.
+- Added new material buttons.
+- See the
+ [Migration Guide](https://github.com/itsarvinddev/loading_icon_button/blob/master/MIGRATION.md#00x--10x)
+ for details.
## 0.0.7
-- Dependencies updated
+- Dependencies updated.
## 0.0.6
-- Dependencies updated
+- Dependencies updated.
## 0.0.5
-- Readme updated
+- Readme updated.
## 0.0.4
-- New Argon buttons added inspired by [argon_buttons_flutter](https://pub.dev/packages/argon_buttons_flutter)
-- Dependencies updated
+- New Argon buttons added, inspired by
+ [argon_buttons_flutter](https://pub.dev/packages/argon_buttons_flutter).
+- Dependencies updated.
## 0.0.3
-- Example application android bug fixed
-- Text alignment fixed
+- Example application android bug fixed.
+- Text alignment fixed.
## 0.0.2
-- Readme updated
+- Readme updated.
## 0.0.1
diff --git a/MIGRATION.md b/MIGRATION.md
index 993c8ca..4fc13a6 100644
--- a/MIGRATION.md
+++ b/MIGRATION.md
@@ -1,529 +1,441 @@
-# Migration Guide for LoadingButton v1.0.0
+# Migration guide
-This guide will help you migrate from the previous version of LoadingButton to the new v1.0.0 with improved API, better error handling, and enhanced features.
+`loading_icon_button` — how to move between major versions of this package.
-## Breaking Changes Overview
-
-### 1. Import Changes
-
-**Before (v0.x):**
+The package name has always been **`loading_icon_button`**, and every public
+symbol comes from a single library:
```dart
-import 'package:loading_button/loading_button.dart';
+import 'package:loading_icon_button/loading_icon_button.dart';
```
-**After (v1.0.0):**
+> **Correction.** An earlier revision of this guide told readers to import and
+> depend on `loading_button`. That is a *different*, unrelated package on
+> pub.dev. If you followed it, change the dependency back to
+> `loading_icon_button`. The same revision used `ButtonState` throughout; that
+> enum has been called [`ActionState`](#102--buttonstate-is-now-actionstate)
+> since 1.0.2.
-```dart
-import 'package:loading_button/loading_button.dart';
-// All classes are now exported from the main library file
-```
+| You are on | Read |
+| --- | --- |
+| 0.0.x | [0.0.x → 1.0.x](#00x--10x), then [1.0.x → 1.1.0](#10x--110) |
+| 1.0.0 – 1.0.3 | [1.0.x → 1.1.0](#10x--110) |
+| 1.1.0 | [What 2.0.0 will change](#what-200-will-change) |
-### 2. Constructor Changes
+---
-**Before (v0.x):**
+## 0.0.x → 1.0.x
-```dart
-LoadingButton(
- onPressed: () async {
- // your code
- },
- child: Text('Submit'),
- // Direct style properties
- color: Colors.blue,
- textColor: Colors.white,
- borderRadius: 8.0,
- elevation: 1.0,
-)
-```
+1.0.0 was a full rewrite of `LoadingButton` plus a large set of new widgets.
+It is the only release in the package's history with wide source-breaking
+changes.
-**After (v1.0.0):**
+### The shape of the change
-```dart
-LoadingButton(
- onPressed: () async {
- // your code
- },
- child: Text('Submit'),
- // Consolidated style object
- style: LoadingButtonStyle(
- backgroundColor: Colors.blue,
- foregroundColor: Colors.white,
- borderRadius: 8.0,
- elevation: 1.0,
- ),
-)
-```
+In 0.0.x, `LoadingButton` was an animation shell that you drove yourself:
-### 3. Button Type Specification
+* `controller` was **required** — a `LoadingButtonController` that you
+ constructed, held, and called `start()` / `stop()` / `success()` / `error()` /
+ `reset()` on.
+* `onPressed` was a plain `VoidCallback`, invoked *after* the squeeze animation
+ finished, and the button had no idea whether your work succeeded. Reaching
+ `success` or `error` was entirely up to your calls on the controller.
+* Every visual knob was a top-level constructor argument (`primaryColor`,
+ `successColor`, `borderRadius`, `elevation`, `showBox`, …).
-**Before (v0.x):**
+In 1.0.x, the button owns the state machine and derives it from a future:
-```dart
-// Separate constructors
-LoadingButton.elevated(...)
-LoadingButton.outlined(...)
-LoadingButton.text(...)
-```
+* There is **no `controller` argument at all** — the class
+ `LoadingButtonController` was removed in 1.0.0. (A new, unrelated
+ `LoadingButtonController` was introduced in
+ [1.1.0](#optional-what-110-adds); it is a
+ `ValueNotifier`, not the 0.0.x listener bag.)
+* `onPressed` is an `AsyncCallback` (`Future Function()`). The button goes
+ to `loading` for the life of the future, then to `success`, or to `error` if
+ it throws.
+* Visual knobs moved into a single `LoadingButtonStyle`.
+* The Material button family (`type: ButtonType.elevated | filled | outlined |
+ text | icon`) replaced the hand-rolled `ElevatedButton` shell.
-**After (v1.0.0):**
+### Before / after
```dart
-// Single constructor with type parameter
-LoadingButton(
- type: ButtonType.elevated,
- // ... other properties
-)
+// 0.0.x
+final controller = LoadingButtonController();
-// OR use builder pattern
-LoadingButtonBuilder.elevated()
- .child(Text('Submit'))
- .build()
-```
-
-## Step-by-Step Migration
-
-### Step 1: Update Dependencies
-
-Update your `pubspec.yaml`:
-
-```yaml
-dependencies:
- loading_button: ^1.0.0
-```
-
-### Step 2: Update Imports
-
-No changes needed if you were importing from the main library file.
-
-### Step 3: Migrate Button Creation
-
-#### Basic Button Migration
-
-**Before:**
-
-```dart
LoadingButton(
- onPressed: _handleSubmit,
- child: Text('Submit'),
- color: Colors.blue,
- textColor: Colors.white,
- loadingColor: Colors.lightBlue,
+ controller: controller,
+ onPressed: () async {
+ controller.start();
+ try {
+ await submit();
+ controller.success();
+ } catch (_) {
+ controller.error();
+ }
+ },
+ child: const Text('Submit'),
+ iconData: Icons.send,
+ primaryColor: Colors.blue,
successColor: Colors.green,
errorColor: Colors.red,
- borderRadius: 8.0,
- elevation: 1.0,
- width: 200,
- height: 50,
+ borderRadius: 25,
+ elevation: 5,
+ width: 225,
+ height: 55,
)
```
-**After:**
-
```dart
+// 1.0.x
LoadingButton(
- onPressed: _handleSubmit,
- child: Text('Submit'),
- style: LoadingButtonStyle(
+ type: ButtonType.elevated,
+ onPressed: () async => submit(), // throwing is how you reach `error`
+ child: const Text('Submit'),
+ style: const LoadingButtonStyle(
backgroundColor: Colors.blue,
- foregroundColor: Colors.white,
- loadingBackgroundColor: Colors.lightBlue,
successBackgroundColor: Colors.green,
errorBackgroundColor: Colors.red,
- borderRadius: 8.0,
- elevation: 1.0,
- ),
- width: 200,
- height: 50,
-)
-```
-
-#### Elevated Button Migration
-
-**Before:**
-
-```dart
-LoadingButton.elevated(
- onPressed: _handleSubmit,
- child: Text('Submit'),
- color: Colors.blue,
- textColor: Colors.white,
-)
-```
-
-**After:**
-
-```dart
-LoadingButton(
- type: ButtonType.elevated,
- onPressed: _handleSubmit,
- child: Text('Submit'),
- style: LoadingButtonStyle(
- backgroundColor: Colors.blue,
- foregroundColor: Colors.white,
- ),
-)
-```
-
-#### Outlined Button Migration
-
-**Before:**
-
-```dart
-LoadingButton.outlined(
- onPressed: _handleSubmit,
- child: Text('Submit'),
- borderColor: Colors.blue,
- borderWidth: 1.0,
- textColor: Colors.blue,
-)
-```
-
-**After:**
-
-```dart
-LoadingButton(
- type: ButtonType.outlined,
- onPressed: _handleSubmit,
- child: Text('Submit'),
- style: LoadingButtonStyle(
- borderColor: Colors.blue,
- borderWidth: 1.0,
- foregroundColor: Colors.blue,
+ borderRadius: 25,
+ elevation: 5,
),
+ width: 225,
+ height: 55,
+ onError: (error) => report(error), // see the 1.1.0 deprecations table
)
```
-### Step 4: Migrate Custom Widgets
-
-#### Loading Widget Migration
-
-**Before:**
-
-```dart
-LoadingButton(
- onPressed: _handleSubmit,
- child: Text('Submit'),
- loadingChild: CircularProgressIndicator(),
-)
-```
-
-**After:**
+### Argument map
+
+| 0.0.x argument | 1.0.x equivalent |
+| --- | --- |
+| `controller` (required) | Removed. The future returned by `onPressed` drives the state. |
+| `onPressed` (`VoidCallback`) | `onPressed` (`AsyncCallback?`) |
+| `child`, `iconData` | `child` only — compose your own `Row` of an `Icon` and a `Text`. |
+| `primaryColor` | `style.backgroundColor` |
+| `valueColor`, `iconColor` | `style.foregroundColor` |
+| `successColor` | `style.successBackgroundColor` |
+| `errorColor` | `style.errorBackgroundColor` |
+| `disabledColor` | `style.disabledBackgroundColor` / `style.disabledForegroundColor` |
+| `shadowColor` | `style.shadowColor` |
+| `borderRadius` | `style.borderRadius` |
+| `elevation` | `style.elevation` |
+| `width`, `height` | `width`, `height` (unchanged) |
+| `duration` | `animationDuration` |
+| `resetDuration` | `successDuration` and `errorDuration` (separate windows) |
+| `resetAfterDuration` | `resetAfterDuration` (now defaults to `true`) |
+| `successIcon`, `failedIcon` | `successWidget`, `errorWidget` (full widgets, not `IconData`) |
+| `loaderSize`, `loaderStrokeWidth` | `loadingWidget` (supply your own indicator) |
+| `animateOnTap`, `showBox`, `spaceBetween`, `curve`, `completionCurve`, `completionDuration` | Removed. |
+| — | `loadingText`, `successText`, `errorText` (also used as accessible labels) |
+| — | `onStateChanged`, `onError`, `enableHapticFeedback`, `type` |
+
+### Also new in 1.0.0
+
+* `ElevatedLoadingButton`, `FilledLoadingButton`, `OutlinedLoadingButton` and
+ `TextLoadingButton` — Material buttons driven by an `isLoading` flag. There is
+ no `IconLoadingButton`; use `IconAutoLoadingButton` for an icon button.
+* `ElevatedAutoLoadingButton`, `FilledAutoLoadingButton`,
+ `OutlinedAutoLoadingButton`, `TextAutoLoadingButton` and
+ `IconAutoLoadingButton` — the same buttons, loading for as long as an async
+ callback runs.
+* `LoadingButtonBuilder` — a fluent builder over `LoadingButton`.
+* `LoadingButtonConfig` — a global defaults singleton
+ ([deprecated in 1.1.0](#deprecations-in-110)).
+* `ButtonState` gained a `disabled` value, plus `ButtonStateExtension`
+ (`isIdle`, `isLoading`, …).
+
+`ArgonButton` and `ArgonTimerButton` were not changed in 1.0.0 beyond becoming
+`part` files of the single library; their arguments are the same.
+
+### SDK floor in 1.0.0
+
+`sdk: ">=2.15.1 <4.0.0"` became `sdk: ">=2.17.0 <4.0.0"`. The declared
+`flutter: ">=1.17.0"` was left alone — see
+[the 1.1.0 SDK correction](#the-one-hard-requirement-the-sdk-floor) for why
+that number was already wrong.
+
+### 1.0.1
+
+URLs in the README and `pubspec.yaml` only. No code change.
+
+### 1.0.2 — `ButtonState` is now `ActionState`
+
+The enum was renamed. **No deprecated alias was kept**, so this was a
+source-breaking change shipped in a patch release. Every value keeps its name:
```dart
-LoadingButton(
- onPressed: _handleSubmit,
- child: Text('Submit'),
- loadingWidget: CircularProgressIndicator(),
-)
-```
-
-#### Success and Error Widgets
-
-**Before:**
+// 1.0.1 and earlier
+onStateChanged: (ButtonState state) {
+ if (state == ButtonState.loading) { /* … */ }
+}
-```dart
-LoadingButton(
- onPressed: _handleSubmit,
- child: Text('Submit'),
- successChild: Icon(Icons.check),
- errorChild: Icon(Icons.error),
-)
+// 1.0.2 and later
+onStateChanged: (ActionState state) {
+ if (state == ActionState.loading) { /* … */ }
+}
```
-**After:**
+A find-and-replace of `ButtonState` → `ActionState` is sufficient, with two
+exceptions that must be left alone: `ArgonButtonState` (a different enum, for
+`ArgonButton`) and `LoadingButtonState` (the `State` class). In 1.0.2 the
+extension was still called `ButtonStateExtension`; it is
+[renamed in 1.1.0](#the-actionstate-extension-was-renamed).
-```dart
-LoadingButton(
- onPressed: _handleSubmit,
- child: Text('Submit'),
- successWidget: Icon(Icons.check),
- errorWidget: Icon(Icons.error),
-)
-```
+### 1.0.3
-### Step 5: Migrate State Callbacks
+A `mounted` guard so that a widget disposed while `onPressed` was still running
+no longer throws (`setState`/state emission on a defunct `State`). Fix only, no
+API change.
-**Before:**
+---
-```dart
-LoadingButton(
- onPressed: _handleSubmit,
- child: Text('Submit'),
- onStateChanged: (state) {
- print('State: $state');
- },
-)
-```
+## 1.0.x → 1.1.0
-**After:**
+**The only thing you are required to do is be on Dart 3.4 / Flutter 3.22 or
+newer.** Everything else in this section is either optional or a behaviour
+change you get for free. No public member was removed or renamed in 1.1.0,
+with the single narrow exception of
+[the extension rename](#the-actionstate-extension-was-renamed), and one
+parameter type was narrowed:
+[`ArgonTimerButton.loader`](#argontimerbuttonloader-is-typed-more-tightly).
-```dart
-LoadingButton(
- onPressed: _handleSubmit,
- child: Text('Submit'),
- onStateChanged: (ButtonState state) {
- print('State: $state');
- // state is now a proper enum
- },
-)
-```
+### The one hard requirement: the SDK floor
-### Step 6: Migrate Error Handling
-
-**Before:**
+```yaml
+environment:
+ sdk: ">=3.4.0 <4.0.0"
+ flutter: ">=3.22.0"
+```
+
+**This is a correction, not a removal of support.** The `*LoadingButton`
+widgets have used `WidgetStatesController` since 1.0.0, and that API landed in
+Flutter 3.22 / Dart 3.4. The `flutter: ">=1.17.0"` the package declared had
+therefore been fiction for several releases: the package could not build on
+anything close to that floor.
+
+What actually changed is *how you find out*. Before 1.1.0, a project on an
+older SDK resolved the package happily and then failed deep inside
+`package:loading_icon_button` with `Undefined class 'WidgetStatesController'`
+— an error that looks like a bug in the package. From 1.1.0 on, `pub` refuses
+the resolution up front and tells you the real constraint. No configuration
+that previously *worked* has stopped working.
+
+If `pub get` now reports that `loading_icon_button` requires a newer SDK, your
+project was already outside the range where this package compiled. Upgrade
+Flutter to 3.22 or later, or pin `loading_icon_button: 1.0.3`.
+
+### Deprecations in 1.1.0
+
+Everything below still works exactly as before. Each is annotated
+`@Deprecated` so the analyzer points at it, and **each keeps working until
+2.0.0**, when it will be removed.
+
+| Deprecated | Write instead | Notes |
+| --- | --- | --- |
+| `LoadingButton.onError` — `Function(dynamic)?` | `LoadingButton.onFailure` — `void Function(Object error, StackTrace stackTrace)?` | Both fire when `onPressed` throws, and both are called if you supply both. `onFailure` also receives the stack trace, which `onError` discarded. |
+| `LoadingButtonBuilder.onError(…)` | `LoadingButtonBuilder.onFailure(…)` | Same signature change as above. |
+| `LoadingButtonConfig` (the whole class) | `LoadingButtonThemeData` + `LoadingButtonTheme`, or a `ThemeExtension` on `ThemeData` | A process-wide mutable singleton cannot vary by subtree or by brightness, and its `reset()` does not rebuild anything. Still read as the **last** fallback, after widget arguments and after the theme, so existing code is unaffected. |
+| `IconButtonLoading` | Compose a `Row`/`Wrap` of an `Icon` and a `Text` | Unused inside the package. |
+| `buildChildWithIcon(…)` | Same — compose the row yourself | Top-level function that leaks into every importing library's namespace. |
+| `buildChildWithIC(…)` | Same | As above. |
+| `buildText(text, style)` | `Text(text, style: style)` | As above. |
+
+Moving off `onError`:
```dart
+// before
LoadingButton(
- onPressed: () async {
- try {
- await _handleSubmit();
- } catch (e) {
- // Handle error manually
- }
- },
- child: Text('Submit'),
+ onPressed: submit,
+ onError: (error) => report(error),
+ child: const Text('Submit'),
)
-```
-
-**After:**
-```dart
+// after
LoadingButton(
- onPressed: _handleSubmit, // Can throw directly
- child: Text('Submit'),
- onError: (error) {
- // Automatic error handling
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text('Error: $error')),
- );
- },
+ onPressed: submit,
+ onFailure: (Object error, StackTrace stackTrace) => report(error, stackTrace),
+ child: const Text('Submit'),
)
```
-### Step 7: Migrate Duration Settings
-
-**Before:**
+Moving off `LoadingButtonConfig`:
```dart
-LoadingButton(
- onPressed: _handleSubmit,
- child: Text('Submit'),
- animationDuration: Duration(milliseconds: 300),
- resetDuration: Duration(seconds: 2),
+// before — anywhere, once, at startup
+LoadingButtonConfig().defaultAnimationDuration = const Duration(milliseconds: 500);
+LoadingButtonConfig().defaultSuccessDuration = const Duration(seconds: 1);
+LoadingButtonConfig().enableHapticFeedback = false;
+
+// after — as a ThemeExtension, so it resolves per brightness
+MaterialApp(
+ theme: ThemeData(
+ extensions: const >[
+ LoadingButtonThemeData(
+ animationDuration: Duration(milliseconds: 500),
+ successDuration: Duration(seconds: 1),
+ enableHapticFeedback: false,
+ ),
+ ],
+ ),
)
-```
-**After:**
-
-```dart
-LoadingButton(
- onPressed: _handleSubmit,
- child: Text('Submit'),
- animationDuration: Duration(milliseconds: 300),
- successDuration: Duration(seconds: 2),
- errorDuration: Duration(seconds: 2),
+// …or for one subtree only
+LoadingButtonTheme(
+ data: const LoadingButtonThemeData(enableHapticFeedback: false),
+ child: checkoutFlow,
)
```
-## New Features in v1.0.0
+`LoadingButtonThemeData.of(context)` resolves in this order: the nearest
+`LoadingButtonTheme` widget, then the `ThemeExtension` on `ThemeData`, then an
+empty instance.
-### 1. Builder Pattern (Recommended)
+### The `ActionState` extension was renamed
-```dart
-// New fluent API
-LoadingButtonBuilder.elevated()
- .onPressed(_handleSubmit)
- .child(Text('Submit'))
- .loadingText('Processing...')
- .successText('Done!')
- .errorText('Failed!')
- .width(200)
- .height(50)
- .style(LoadingButtonStyle(
- backgroundColor: Colors.blue,
- borderRadius: 25,
- ))
- .onError((error) => _handleError(error))
- .build()
-```
+`ButtonStateExtension` is now `ActionStateExtension` — finishing the 1.0.2
+rename. No alias is kept behind it, deliberately: a second extension declaring
+the same members on the same enum would make every `state.isLoading` call site
+an `ambiguous_extension_member_access` compile error in *your* code, and
+`typedef ButtonStateExtension = ActionStateExtension;` is not legal Dart
+(extensions are not types).
-### 2. Global Configuration
+In practice this breaks nothing, because extension members resolve by member
+name on the receiver's type, not by the extension's name. `state.isIdle`,
+`state.isLoading`, `state.isSuccess`, `state.isError` and `state.isDisabled`
+all keep compiling unchanged. Only an explicit extension override —
+`ButtonStateExtension(state).isIdle` — has to be rewritten as
+`ActionStateExtension(state).isIdle`.
-```dart
-// Set global defaults
-LoadingButtonConfig().defaultAnimationDuration = Duration(milliseconds: 500);
-LoadingButtonConfig().enableHapticFeedback = true;
-LoadingButtonConfig().defaultLoadingWidget = MyCustomSpinner();
-```
+One member changed meaning:
-### 3. Enhanced State Management
+| Member | Before 1.1.0 | From 1.1.0 |
+| --- | --- | --- |
+| `isInteractive` | `state == idle` — an exact duplicate of `isIdle` | `state != loading && state != disabled`, so a button resting in `success` or `error` correctly reports as pressable |
-```dart
-LoadingButton(
- onPressed: _handleSubmit,
- child: Text('Submit'),
- onStateChanged: (ButtonState state) {
- switch (state) {
- case ButtonState.idle:
- // Handle idle state
- break;
- case ButtonState.loading:
- // Handle loading state
- break;
- case ButtonState.success:
- // Handle success state
- break;
- case ButtonState.error:
- // Handle error state
- break;
- case ButtonState.disabled:
- // Handle disabled state
- break;
- }
- },
-)
-```
+If you relied on the old meaning, use `isIdle`, which is unchanged.
-### 4. Accessibility Improvements
+### `ArgonTimerButton.loader` is typed more tightly
-```dart
-// Automatic accessibility support
-LoadingButton(
- onPressed: _handleSubmit,
- child: Text('Submit'),
- loadingText: 'Processing your request', // Used for screen readers
- successText: 'Request completed successfully',
- errorText: 'Request failed, please try again',
-)
-```
-
-### 5. Responsive Design
-
-```dart
-// Automatic tablet support
-LoadingButton(
- onPressed: _handleSubmit,
- child: Text('Submit'),
- // Automatically adjusts size for tablets
-)
-```
-
-## Migration Checklist
-
-- [ ] Update package version in `pubspec.yaml`
-- [ ] Replace direct style properties with `LoadingButtonStyle` object
-- [ ] Update button type specification (if using typed constructors)
-- [ ] Change `loadingChild` to `loadingWidget`
-- [ ] Change `successChild` to `successWidget`
-- [ ] Change `errorChild` to `errorWidget`
-- [ ] Update state callback parameter type to `ButtonState`
-- [ ] Move error handling to `onError` callback
-- [ ] Update duration property names
-- [ ] Test all button states and animations
-- [ ] Verify accessibility features work correctly
+This is the one signature change in 1.1.0, and it is source-breaking for a
+narrow case.
-## Common Issues and Solutions
+| | Type |
+| --- | --- |
+| Up to 1.0.3 | `final Function(int time)? loader;` |
+| From 1.1.0 | `final Widget Function(int time)? loader;` |
-### Issue 1: Style Properties Not Working
-
-**Problem:** Direct style properties like `color`, `textColor` not working.
-
-**Solution:** Wrap all style properties in `LoadingButtonStyle`:
+The value has always been used in a `child:` position, so the tighter type only
+writes down what the widget already needed — nothing about the rendered result
+changes. What does change is assignability. The old bare `Function(int time)`
+placed no constraint at all on the return type, so anything compiled; the new
+type requires a non-nullable `Widget`.
```dart
-// Wrong
-LoadingButton(
- color: Colors.blue,
- textColor: Colors.white,
-)
-
-// Correct
-LoadingButton(
- style: LoadingButtonStyle(
- backgroundColor: Colors.blue,
- foregroundColor: Colors.white,
- ),
-)
-```
-
-### Issue 2: Constructor Not Found
-
-**Problem:** `LoadingButton.elevated()` constructor not found.
-
-**Solution:** Use type parameter or builder pattern:
+// Still fine — returns a Widget on every path.
+loader: (int time) => Text('$time'),
+
+// Compiled before. Now: "The returned type 'Text?' isn't returnable
+// from a 'Widget' function".
+loader: (int time) => time > 0 ? Text('$time') : null,
+
+// Compiled before. Now: "The body might complete normally, causing
+// 'null' to be returned, but the return type, 'Widget', is a
+// potentially non-nullable type".
+loader: (int time) {
+ debugPrint('$time');
+},
+
+// Also affected: any torn-off function or typedef whose declared return
+// type is dynamic, void, or a nullable Widget.
+```
+
+The fix is to return a non-nullable `Widget` on every path — `const
+SizedBox.shrink()` for the "show nothing" case. Annotating the closure as
+`Widget Function(int time)` will point the analyzer at whichever path does not.
+
+### Behaviour changes
+
+None of these change a signature; all of them are observable at runtime.
+
+| What you will notice | Why it changed |
+| --- | --- |
+| A callback that throws on an `*AutoLoadingButton` no longer leaves the button stuck as a spinner. | `doPress`/`doLongPress` shared no code, and only the success path cleared the pending-future flag. A throwing callback left that flag set forever, so the button stayed disabled and spinning, every awaiter of `doPress()` hung, and the error surfaced as an unhandled async error. Both paths now clear it. Tap-path failures are reported through `FlutterError.reportError` with package context; if you call `doPress()` yourself, you receive the error on the returned future and can handle it normally. |
+| A fast double tap on `LoadingButton` runs `onPressed` once, not twice. | The old guard read the current state, then awaited haptic feedback and a 200 ms press animation *before* moving to `loading`. Two taps inside that window both passed the guard. There is now a synchronous latch set before the first `await`. |
+| `onPressed` runs immediately on tap instead of roughly 200 ms later. | The press path used to `await` the scale animation's forward and reverse legs (100 ms each) before invoking your callback. The button now enters `loading` and starts your work straight away. |
+| Haptic feedback is fired and forgotten rather than awaited. | `await HapticFeedback.lightImpact()` delayed the user's callback by a platform-channel round trip — and on a test binding the channel never completes, which made any widget test of a press hang. |
+| `onStateChanged` no longer fires a spurious `ActionState.idle` when the button is first built. | State used to flow through a `BehaviorSubject` seeded with `idle`, so subscribing in `initState` replayed that seed as if it were a transition. `onStateChanged` is now called only for real changes. |
+| Screen readers announce the button once, not twice. | The button was wrapped in its own `Semantics(button: true, …)` node on top of the Material button's own node, producing a duplicate. The transient state text (`loadingText` / `successText` / `errorText`) now rides on the child as a `liveRegion` label instead, so state changes are announced without a second button node. |
+| A disposed `LoadingButton` cancels its pending reset instead of firing it. | The success/error reset was a `Future.delayed` that could not be cancelled; it is now a `Timer` cancelled in `dispose` and re-armed on each state change. |
+| `ArgonButton` with a null `onTap` renders as disabled instead of throwing. | `ArgonButton.onTap` was force-unwrapped as `widget.onTap!(…)` in the tap handler despite being nullable, so the first tap on a button without an `onTap` threw. Escaping `startLoading`/`stopLoading` closures and the timer callbacks are now `mounted`-guarded too. |
+| `ArgonButton` with a null `loader` shows a default spinner instead of nothing. | The old code did **not** crash here: `loader` went straight into a `child:` position, where null is legal, so a button with no `loader` animated down to a pill and showed an empty pill for the whole busy window. A spinner is the more useful default. To keep the old empty pill, pass `loader: const SizedBox.shrink()`. |
+| `ArgonTimerButton` with a null `loader` shows a default spinner instead of throwing. | This is the one that crashed: the countdown builder called `widget.loader!(secondsLeft)`, so any `ArgonTimerButton` without a `loader` threw as soon as the timer started. |
+| `ArgonTimerButton.startTimer` throws `ArgumentError` on a bad timer value. | It used to `throw` a bare `String`, which cannot be caught as an `Exception` and prints poorly. |
+
+If a widget test of yours asserted any of the old behaviours — a double-fired
+`onPressed`, the leading `idle` callback, two semantics button nodes — it will
+need updating. That is the full list of test-visible changes.
+
+### Optional: what 1.1.0 adds
+
+Nothing here is required; the defaults are unchanged from 1.0.x.
+
+* **`LoadingButtonController`** — a `ValueNotifier` with
+ `start` / `setProgress` / `success` / `error` / `reset` / `setEnabled` /
+ `setActionState` / `press` / `startCooldown`. Pass it as
+ `LoadingButton.controller` to drive the button from outside the tree.
+* **`LoadingButtonSizing`** — `.legacy` (the default: the old fixed 200×50 box,
+ widened to 240 above 600 logical pixels), `.intrinsic({constraints})`,
+ `.expand({height})`, `.fixed({width, height})`.
+* **`LoadingIndicator`** — `.circular()`, `.orb()`, `.widget()`, `.builder()`,
+ accepted by `LoadingButton.indicator` and by the `indicator` argument now on
+ the Material loading buttons.
+* **`ThinkingOrb`** — nine animated `OrbState` indicators (`working`,
+ `searching`, `solving`, `listening`, `connecting`, `weaving`, `composing`,
+ `breathing`, `shaping`), reduced-motion aware.
+* **Determinate progress** — `LoadingButton.progress` (0.0–1.0) with
+ `LoadingProgressStyle.indicator | fill | both`.
+* **`LoadingButtonColors` / `LoadingButtonColorStrategy`** — keep `.legacy`
+ colours or opt into `.material3`, which derives everything from the ambient
+ `ColorScheme` and is safe in dark mode.
+* **Press control** — `enabled`, `debounce`, `cooldown`.
+* **Passthroughs** — `buttonStyle` (a plain Material `ButtonStyle`),
+ `focusNode`, `autofocus`, `tooltip`, `transitionBuilder`, `onSuccess`.
```dart
-// Option 1: Type parameter
LoadingButton(
- type: ButtonType.elevated,
- // ...
+ onPressed: () async => upload(),
+ child: const Text('Upload'),
+ sizing: const LoadingButtonSizing.intrinsic(),
+ colorStrategy: LoadingButtonColorStrategy.material3,
+ indicator: const LoadingIndicator.orb(state: OrbState.working),
+ debounce: const Duration(milliseconds: 300),
+ onFailure: (Object error, StackTrace stack) => report(error, stack),
)
-
-// Option 2: Builder pattern
-LoadingButtonBuilder.elevated()
- // ...
- .build()
-```
-
-### Issue 3: State Callback Type Error
-
-**Problem:** State parameter type error in `onStateChanged`.
-
-**Solution:** Use proper enum type:
-
-```dart
-// Wrong
-onStateChanged: (state) {
- if (state == 'loading') { ... }
-}
-
-// Correct
-onStateChanged: (ButtonState state) {
- if (state == ButtonState.loading) { ... }
-}
```
-### Issue 4: Animation Not Smooth
-
-**Problem:** Animations appear jerky or not smooth.
-
-**Solution:** Use global configuration or specify duration:
-
-```dart
-// Global setting
-LoadingButtonConfig().defaultAnimationDuration = Duration(milliseconds: 300);
-
-// Or per button
-LoadingButton(
- animationDuration: Duration(milliseconds: 300),
- // ...
-)
-```
+---
-## Benefits of Migration
+## What 2.0.0 will change
-1. **Better Error Handling**: Automatic error catching and handling
-2. **Improved Performance**: Optimized animations and memory usage
-3. **Enhanced Accessibility**: Built-in screen reader support
-4. **Cleaner API**: Consistent naming and structure
-5. **Better Maintainability**: Separated concerns and modular design
-6. **Responsive Design**: Automatic tablet support
-7. **Type Safety**: Strong typing with null safety
-8. **Customization**: More styling options and configurations
+Announced now so you can adopt the new defaults early and upgrade without a
+diff. Nothing below is in effect in 1.1.0.
-## Getting Help
+| Change | How to adopt it today | How to keep the old behaviour in 2.0.0 |
+| --- | --- | --- |
+| `LoadingButton.sizing` defaults to `LoadingButtonSizing.intrinsic()` instead of `.legacy`, and `.legacy` is removed. | Pass `sizing: const LoadingButtonSizing.intrinsic()`, or set `sizing` on your `LoadingButtonThemeData`. | Pass an explicit `LoadingButtonSizing.fixed(width: 200, height: 50)` (the legacy tablet widening will not be reproducible). |
+| `LoadingButton.colorStrategy` defaults to `LoadingButtonColorStrategy.material3` instead of `.legacy`. | Pass `colorStrategy: LoadingButtonColorStrategy.material3`, or set it on the theme. | Pass `colorStrategy: LoadingButtonColorStrategy.legacy`, or supply explicit `colors`. |
+| Every member deprecated in 1.1.0 is removed. | Work through [the deprecations table](#deprecations-in-110); the analyzer lists them all. | Not available — migrate before upgrading. |
-If you encounter issues during migration:
+Both default flips are visual, not source-breaking: buttons will size to their
+content rather than to a fixed 200×50 box, and state colours will come from
+your `ColorScheme` rather than from `primaryColor` plus hardcoded green and
+red.
-1. Check the [API documentation](https://pub.dev/packages/loading_icon_button)
-2. Look at the [example app](https://github.com/itsarvinddev/loading_icon_button/tree/master/example)
-3. Open an issue on [GitHub](https://github.com/itsarvinddev/loading_icon_button/issues)
+---
-## Final Notes
+## Getting help
-- The migration is designed to be straightforward with minimal breaking changes
-- Most changes are simple property renames and restructuring
-- The new API is more consistent and easier to use
-- All previous functionality is preserved with enhancements
-- Consider using the builder pattern for new implementations as it provides better IntelliSense support and cleaner code
+* [API documentation](https://pub.dev/documentation/loading_icon_button/latest/)
+* [Example app](https://github.com/itsarvinddev/loading_icon_button/tree/master/example)
+* [Issue tracker](https://github.com/itsarvinddev/loading_icon_button/issues)
diff --git a/README.md b/README.md
index 6678c0a..1edd324 100644
--- a/README.md
+++ b/README.md
@@ -6,409 +6,1263 @@
[](https://github.com/itsarvinddev/loading_icon_button/blob/master/LICENSE)
[](https://x.com/itsarvinddev)
-A comprehensive Flutter package providing customizable loading buttons with icons, text, and smooth animations. Features multiple button types including `LoadingButton`, `AutoLoadingButton`, and `ArgonButton` with Material Design compliance.
+
+
+Left to right: a spinner, a `ThinkingOrb` indicator, and determinate progress shown as a
+background fill — each running, then succeeding. Underneath, all nine orb states.
+
+Loading buttons for Flutter with a real state machine: idle, loading, success, error, disabled.
+Drive one from a callback, from a plain `bool`, or from a `LoadingButtonController` anywhere in your
+app. Every button can wear a **`ThinkingOrb`** — one of nine hand-tuned animated indicators built
+for AI and agent interfaces — instead of a spinner. Pure Dart, zero dependencies, every Flutter
+platform.
+
+## Contents
+
+- [Installation](#installation)
+- [Quick start](#quick-start)
+- [Which button?](#which-button)
+- [LoadingButton](#loadingbutton)
+- [Thinking orbs](#thinking-orbs)
+- [The AutoLoadingButton family](#the-autoloadingbutton-family)
+- [The XxxLoadingButton family](#the-xxxloadingbutton-family)
+- [ArgonButton and ArgonTimerButton](#argonbutton-and-argontimerbutton)
+- [Accessibility](#accessibility)
+- [Testing](#testing)
+- [Platform support](#platform-support)
+- [Deprecations](#deprecations)
+- [LLM and AI assistant support](#llm-and-ai-assistant-support)
+- [Attribution](#attribution)
+- [Contributing](#contributing)
+- [License and issues](#license-and-issues)
-
+## Installation
-## Features
+```yaml
+dependencies:
+ loading_icon_button: ^1.1.0
+```
-- ✅ **Multiple Button Types**: `LoadingButton`, `AutoLoadingButton`, and `ArgonButton`
-- ✅ **Material Design**: Full Material Design compliance with Material 3 support
-- ✅ **Customizable States**: Idle, Loading, Success, Error states with smooth transitions
-- ✅ **Icon Support**: Built-in icon support with customizable icons for each state
-- ✅ **Cross-Platform**: Works on Android, iOS, Linux, macOS, web, and Windows
-- ✅ **Accessible**: Full accessibility support with semantic labels
-- ✅ **Responsive**: Automatic responsive design for tablets and mobile devices
-- ✅ **Zero Boilerplate**: Easy to use with minimal configuration
-- ✅ **Customizable Animations**: Configurable animation durations and effects
+```dart
+import 'package:loading_icon_button/loading_icon_button.dart';
+```
-## Installation
+One import gives you everything. Requires Dart `>=3.4.0` and Flutter `>=3.22.0`.
-Add this to your `pubspec.yaml` file:
+## Quick start
-```yaml
-dependencies:
- loading_icon_button: ^latest
+```dart
+LoadingButton(
+ onPressed: () async => submit(),
+ onSuccess: () => debugPrint('done'),
+ onFailure: (Object error, StackTrace stack) => report(error, stack),
+ child: const Text('Submit'),
+)
```
-Then run:
+The button runs `onPressed`, shows an indicator until the returned future settles, flashes success
+or error, then returns to idle. `onPressed` is an `AsyncCallback` — it must be `() async { … }`.
+
+## Which button?
+
+Four families, distinguished by **who owns the loading state**. Pick a row:
+
+| Family | Loading state owned by | Success / error phases | Reach for it when |
+| ----------------------------------------------------------------------- | ------------------------------------------ | ---------------------- | ------------------------------------------------------- |
+| [`LoadingButton`](#loadingbutton) | The widget, or a `LoadingButtonController` | Yes | You want the full idle → loading → success/error cycle |
+| [`XxxAutoLoadingButton`](#the-autoloadingbutton-family) | The widget, while the future runs | No | You want a stock Material button that spins while busy |
+| [`XxxLoadingButton`](#the-xxxloadingbutton-family) | **You**, via an `isLoading` flag | No | The state already lives in your bloc/provider/notifier |
+| [`ArgonButton` / `ArgonTimerButton`](#argonbutton-and-argontimerbutton) | You, via callbacks | No | You want the collapse-into-a-pill look, or a countdown |
+
+
-```bash
-flutter pub get
+One labelled row per widget, at rest. `ArgonButton` is the odd one out: it animates its own
+width down to a pill, while the rest render standard Material buttons.
+
+The first three families share the same [`LoadingIndicator`](#indicators) type (orbs included) and
+take their defaults from the same [`LoadingButtonTheme`](#theming). `ArgonButton` is standalone: it
+takes a plain `loader` widget and reads neither.
+
+## LoadingButton
+
+The widget for the full cycle. It owns a five-state machine and renders one of the five Material
+button types for it.
+
+### The state machine
+
+`ActionState` has five values:
+
+```
+idle ──press──▶ loading ──┬─▶ success ──┐
+ └─▶ error ──┴─▶ idle (after successDuration / errorDuration)
+
+disabled ◀── enabled: false, controller.setEnabled(false), or an active cooldown
```
-## Quick Start
+
+
+The `elevated`, `filled`, `outlined` and `text` button types in every state, light theme
+(`ButtonType.icon` is an `IconButton`, so it is not in the grid). The lavender loading container and
+the green/red terminal states are a `LoadingButtonColors` layered over
+`LoadingButtonColorStrategy.material3` — see [Colours](#colours).
-### Import
+
+
+The same matrix in dark. State colours come from the ambient `ColorScheme`, so contrast holds in
+both brightnesses.
```dart
-import 'package:loading_icon_button/loading_icon_button.dart';
+LoadingButton(
+ onPressed: () async => submit(),
+ successText: 'Saved',
+ errorText: 'Could not save',
+ resetAfterDuration: true,
+ onStateChanged: (ActionState state) => debugPrint(state.name),
+ child: const Text('Save'),
+)
+```
+
+Rules worth knowing:
+
+- A press is accepted **only** from `idle`, and a synchronous latch closes before the first `await`,
+ so a double tap cannot run `onPressed` twice.
+- If `onPressed` throws, the button goes to `error` and calls `onFailure(error, stack)`. If neither
+ `onFailure` nor the deprecated `onError` is set, the error is forwarded to
+ `FlutterError.reportError` rather than swallowed.
+- `resetAfterDuration: false` parks the button in its terminal state. A parked button is not
+ pressable; `controller.reset()` is the only way out.
+- `onStateChanged` fires on every transition except the initial `idle`.
+
+`ActionState` carries the predicates `isIdle`, `isLoading`, `isSuccess`, `isError`, `isDisabled` and
+`isInteractive` (every state except `loading` and `disabled`). `LoadingButton` is stricter than
+`isInteractive`: it accepts a press only from `idle`, and renders disabled while it rests in
+`success` or `error`. For "is this tappable right now", use `isIdle`.
+
+### LoadingButtonController
+
+`LoadingButtonController` is a `ValueNotifier`, so one object drives the button
+and renders the rest of your UI. No `GlobalKey` required.
+
+```dart
+final LoadingButtonController controller = LoadingButtonController();
+
+LoadingButton(
+ controller: controller,
+ onPressed: _upload,
+ progressStyle: LoadingProgressStyle.both,
+ child: const Text('Upload'),
+);
+
+ValueListenableBuilder(
+ valueListenable: controller,
+ builder: (BuildContext context, LoadingButtonValue value, _) => Text(
+ value.isDeterminate ? '${(value.progress! * 100).round()}%' : value.state.name,
+ ),
+);
+
+// from anywhere
+controller.setProgress(0.4);
```
-### Basic Usage
+| Command | Effect |
+| ----------------------------------- | ------------------------------------------------------------------- |
+| `start({progress})` | Move to `loading`, optionally determinate |
+| `setProgress(double?)` | Report `0.0..1.0`; `null` restores the indeterminate indicator |
+| `success()` | Move to `success` |
+| `error([error, stackTrace])` | Move to `error`, recording both |
+| `reset()` | Back to `idle`, clearing progress and the last error |
+| `setEnabled(bool)` | Toggle `disabled` (a no-op mid-run) |
+| `setActionState(state, {progress})` | Escape hatch for states the named commands don't cover |
+| `press()` | Run every attached button's `onPressed`, honouring each press latch |
+| `startCooldown(Duration)` | Hold `disabled` for a window, then return to `idle` |
+
+Readable state: `state`, `progress`, `lastError`, `lastStackTrace`, `isCoolingDown`,
+`cooldownRemaining`, `isAttached`, `attachmentCount`.
+
+`LoadingButtonValue` bundles `state`, `progress`, `error` and `stackTrace` into one immutable value,
+so a listener can never observe an `error` paired with stale progress.
+
+One controller may drive several buttons at once; they all mirror the same value. Because they share
+it, `press()` starts exactly one run rather than one per button: the first button to accept the press
+moves the shared value to `loading`, and the rest decline. Attach one button per controller when that
+matters. Dispose it like any `ChangeNotifier`.
+
+### Sizing
+
+**The default is still the legacy geometry**: a fixed **200 × 50** box, silently widened to
+**240 × 50** when the window is wider than 600 logical pixels. That is what every release before
+1.1.0 did, and it stays the default so upgrading moves nothing.
+
+It is almost never what you want. Opt into content sizing:
```dart
LoadingButton(
- child: const Text('Login with Apple'),
- iconData: Icons.apple,
- onPressed: () => _handleLogin(),
+ onPressed: () async => submit(),
+ sizing: const LoadingButtonSizing.intrinsic(),
+ child: const Text('Sizes to its content'),
)
```
-## Button Types
+| Sizing | Behaviour |
+| ---------------------------------------------- | ------------------------------------------------------------------------- |
+| `LoadingButtonSizing.legacy` *(default)* | Fixed 200×50; 240×50 above a 600px-wide window |
+| `LoadingButtonSizing.intrinsic({constraints})` | Sizes to content like a normal `ElevatedButton`, animating between states |
+| `LoadingButtonSizing.expand({height})` | Fills the parent's width |
+| `LoadingButtonSizing.fixed({width, height})` | Exactly this size; a null dimension is left to content |
+
+The explicit `width` / `height` parameters still win over whatever `sizing` computes. Set it once for
+a whole app through [the theme](#theming).
-### 1. LoadingButton
+> 2.0.0 will make `intrinsic` the default and remove `legacy`.
-The main button with full customization options:
+### Colours
+
+Two strategies:
+
+| `LoadingButtonColorStrategy` | Behaviour |
+| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `legacy` *(default)* | `Theme.of(context).primaryColor` background, unconditional white foreground, hardcoded green/red. Not dark-mode safe, and it paints a filled background onto text and outlined buttons. |
+| `material3` | Colours come from the ambient `ColorScheme`; container/on-container role pairs guarantee contrast in both brightnesses, and the idle state is left to your own `ButtonStyle`. |
```dart
LoadingButton(
- child: const Text('Submit'),
- iconData: Icons.send,
- onPressed: () async {
- // Your async operation
- await Future.delayed(Duration(seconds: 2));
- },
- duration: Duration(milliseconds: 300),
- successColor: Colors.green,
- errorColor: Colors.red,
- successIcon: Icons.check,
- failedIcon: Icons.error,
+ onPressed: () async => submit(),
+ colorStrategy: LoadingButtonColorStrategy.material3,
+ child: const Text('Scheme colours'),
)
```
-### 2. AutoLoadingButton (Material Design)
+`LoadingButtonColors` overrides individual states. A `null` field means "leave this state to the
+button's own `ButtonStyle`".
+
+```dart
+LoadingButtonColors.fromScheme(Theme.of(context).colorScheme); // M3 role pairs
+LoadingButtonColors.traffic(Theme.of(context).brightness); // tone-mapped green/red
+LoadingButtonColors.legacy; // exactly the old colours
+
+const LoadingButtonColors(
+ loading: Color(0xFF303F9F),
+ onLoading: Colors.white,
+ success: Color(0xFF1B5E20),
+ onSuccess: Colors.white,
+ error: Color(0xFFB3261E),
+ onError: Colors.white,
+);
+```
+
+> 2.0.0 will default to `material3`.
-A Material Design compliant button with automatic loading states:
+For geometry, elevation, padding and text style there are two layers: a standard Material
+`buttonStyle`, applied first, and then the package-specific `style` (`LoadingButtonStyle`) plus the
+state colours on top.
```dart
-ElevatedAutoLoadingButton(
+LoadingButton(
+ type: ButtonType.filled,
+ onPressed: () async => submit(),
+ buttonStyle: FilledButton.styleFrom(
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
+ ),
+ style: const LoadingButtonStyle(borderRadius: 24, elevation: 2),
+ child: const Text('Styled'),
+)
+```
+
+### Theming
+
+`LoadingButtonThemeData` supplies defaults for every loading button beneath it. Its `indicator`
+reaches all three Material families, not just `LoadingButton`; the rest of the fields are
+`LoadingButton`'s. (`ArgonButton` reads none of it.)
+
+```dart
+const LoadingButtonThemeData(
+ indicator: LoadingIndicator.orb(state: OrbState.working),
+ sizing: LoadingButtonSizing.intrinsic(),
+ colorStrategy: LoadingButtonColorStrategy.material3,
+ successWidget: Icon(Icons.check_rounded),
+ errorWidget: Icon(Icons.error_outline_rounded),
+ animationDuration: Duration(milliseconds: 250),
+ successDuration: Duration(seconds: 2),
+ errorDuration: Duration(seconds: 3),
+ enableHapticFeedback: true,
+ progressStyle: LoadingProgressStyle.fill,
+ debounce: Duration(milliseconds: 400),
+ cooldown: Duration(seconds: 1),
+)
+```
+
+Install it as a `ThemeExtension` on your `ThemeData` — resolved per brightness, and lerped across
+theme animations — or with a `LoadingButtonTheme` widget around a subtree.
+
+```dart
+MaterialApp(
+ theme: ThemeData(
+ extensions: const >[
+ LoadingButtonThemeData(sizing: LoadingButtonSizing.intrinsic()),
+ ],
+ ),
+ home: const HomePage(),
+)
+```
+
+Resolution order, highest first:
+
+1. The widget's own arguments
+2. The ambient `LoadingButtonThemeData`
+3. The deprecated `LoadingButtonConfig` singleton
+4. Package defaults
+
+That ambient data is the nearest `LoadingButtonTheme` widget if there is one, and otherwise the
+`ThemeData` extension. The two are **not** merged — a `LoadingButtonTheme` widget replaces the
+extension wholesale for its subtree, so put every field you still want into it.
+
+Read it back with `LoadingButtonThemeData.of(context)`, or `LoadingButtonTheme.maybeOf(context)` for
+just the widget-level one.
+
+> `LoadingButtonConfig` — the process-wide singleton from earlier releases — is deprecated but still
+> honoured. It cannot vary by subtree or by brightness, its `reset()` cannot reach already-built
+> widgets, and its default widgets hardcode white. Move those values into a
+> `LoadingButtonThemeData`.
+
+### Determinate progress
+
+Pass `progress` in `0.0..1.0` (or `null` for indeterminate) and choose how it is shown:
+
+| `LoadingProgressStyle` | Rendering |
+| ----------------------- | ---------------------------------------------------------------- |
+| `indicator` *(default)* | The loading indicator becomes determinate |
+| `fill` | The background fills left to right, clipped to the button's shape |
+| `both` | Both at once |
+
+```dart
+LoadingButton(
+ progress: _progress,
+ progressStyle: LoadingProgressStyle.fill,
onPressed: () async {
- // Automatically shows loading state
- await _performOperation();
- // Automatically shows success/error state
+ for (int i = 0; i <= 100; i += 10) {
+ if (!mounted) return;
+ setState(() => _progress = i / 100);
+ await Future.delayed(const Duration(milliseconds: 80));
+ }
},
- child: const Text('Auto Loading'),
- style: ButtonStyle(
- backgroundColor: WidgetStateProperty.all(Colors.blue),
- foregroundColor: WidgetStateProperty.all(Colors.white),
- borderRadius: WidgetStateProperty.all(BorderRadius.circular(8.0)),
- ),
+ child: const Text('Upload'),
)
```
-### 3. LoadingButton
+When a `controller` is attached, `controller.progress` wins over the `progress` argument. Orbs are
+indeterminate by design, so with `LoadingIndicator.orb()` prefer `LoadingProgressStyle.fill`.
-Enhanced Material Design button with advanced features:
+### Indicators
+
+```dart
+const LoadingIndicator.circular(size: 20, strokeWidth: 2, color: Colors.white);
+const LoadingIndicator.orb(state: OrbState.listening, size: 22, speed: 1.2);
+const LoadingIndicator.widget(Icon(Icons.hourglass_top));
+LoadingIndicator.builder(
+ (BuildContext context, double? progress) =>
+ Text(progress == null ? '…' : '${(progress * 100).round()}%'),
+);
+```
+
+On `LoadingButton` the widget shown while loading is resolved in this order:
+
+`loadingWidget` → `loadingText` → `indicator` → the theme's indicator → the package default (a
+`CircularProgressIndicator`).
+
+Note the second entry: **`loadingText` replaces the indicator rather than labelling it.** A
+`LoadingButton` given a `loadingText` shows that text alone — no spinner, no orb — so setting both
+`loadingText` and `indicator` renders only the text. The same shape applies to
+`successWidget`/`successText` and `errorWidget`/`errorText`. To show an indicator *and* a caption,
+build the pair yourself and pass it as `loadingWidget`:
```dart
LoadingButton(
- type: ButtonType.elevated,
- onPressed: () => _simulateLogin(),
- child: const Row(
+ onPressed: () async => submit(),
+ loadingWidget: const Row(
mainAxisSize: MainAxisSize.min,
- children: [
- Icon(Icons.login),
+ children: [
+ SizedBox.square(
+ dimension: 18,
+ child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
+ ),
SizedBox(width: 8),
- Text('Login'),
+ Text('Submitting…'),
],
),
- style: const LoadingButtonStyle(
- backgroundColor: Colors.blue,
- foregroundColor: Colors.white,
- borderRadius: 8,
- ),
- loadingText: 'Logging in...',
- successText: 'Welcome!',
- errorText: 'Login failed',
- onError: (error) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text('Login error: $error'),
- backgroundColor: Colors.red,
- ),
- );
- },
-),
+ child: const Text('Submit'),
+)
+```
+
+### Presses: debounce, cooldown, enabling and focus
+
+```dart
+LoadingButton(
+ onPressed: () async => resend(),
+ debounce: const Duration(milliseconds: 500),
+ cooldown: const Duration(seconds: 30),
+ enabled: _formValid,
+ tooltip: _formValid ? null : 'Fill in every field first',
+ focusNode: _focusNode,
+ autofocus: true,
+ child: const Text('Resend code'),
+)
```
-### 4. ArgonButton
+- `debounce` ignores a tap arriving within that window of the previously **accepted** tap.
+- `cooldown` holds the button `disabled` for that long after a run finishes and its success/error
+ window has elapsed, then returns it to `idle`. `controller.isCoolingDown` and
+ `controller.cooldownRemaining` let you render a countdown. Cooldown rides on the reset timer, so it
+ does nothing when `resetAfterDuration: false` — drive the wait with
+ `controller.startCooldown(duration)` instead.
+- `enabled: false` puts the button in `ActionState.disabled`, which is also reachable via
+ `controller.setEnabled(false)`.
-A specialized button with advanced loader customization:
+### Custom transitions
+
+The default is a cross fade over `animationDuration`. Replace it wholesale:
```dart
-ArgonButton(
- height: 50,
- width: 350,
- borderRadius: 5.0,
- color: Color(0xFF7866FE),
- child: Text(
- "Continue",
- style: TextStyle(
- color: Colors.white,
- fontSize: 18,
- fontWeight: FontWeight.w700,
- ),
+LoadingButton(
+ onPressed: () async => submit(),
+ transitionBuilder: (Widget child, Animation animation) =>
+ ScaleTransition(scale: animation, child: child),
+ child: const Text('Scales between states'),
+)
+```
+
+### Reaching the state directly
+
+A `LoadingButtonController` is usually the better tool, but `LoadingButtonState` is public:
+
+```dart
+final GlobalKey key = GlobalKey();
+
+LoadingButton(key: key, onPressed: () async => submit(), child: const Text('Submit'));
+
+// elsewhere
+if (key.currentState!.currentState.isIdle) {
+ await key.currentState!.press();
+}
+```
+
+### Fluent builder
+
+```dart
+LoadingButtonBuilder.filled()
+ .onPressed(() async => submit())
+ .child(const Text('Submit'))
+ .indicator(const LoadingIndicator.orb(state: OrbState.working))
+ .sizing(const LoadingButtonSizing.intrinsic())
+ .colorStrategy(LoadingButtonColorStrategy.material3)
+ .successText('Submitted')
+ .onFailure((Object error, StackTrace stack) => report(error, stack))
+ .build()
+```
+
+Factories: `.elevated()`, `.filled()`, `.outlined()`, `.text()`, `.icon()`.
+
+A builder is **mutable and single-use**. Create one, configure it, build once. Two consequences of
+that mutability:
+
+- A setter called *after* `build()` does not affect the widget that was already built — `build()`
+ snapshots the configuration, so late changes are silently lost.
+- If you called `.key(…)`, building twice returns two different widgets carrying that same key, which
+ is an error if both are mounted at once. With no `.key(…)` call both widgets get a null key and
+ mounting both is legal.
+
+### LoadingButton properties
+
+| Property | Type | Default |
+| ---------------------- | ----------------------------------------------------------- | -------------------- |
+| `child` | `Widget?` | `null` |
+| `onPressed` | `AsyncCallback?` | `null` |
+| `type` | `ButtonType` | `elevated` |
+| `loadingWidget` | `Widget?` | `null` |
+| `successWidget` | `Widget?` | `null` |
+| `errorWidget` | `Widget?` | `null` |
+| `loadingText` | `String?` — **replaces** the indicator, see † | `null` |
+| `successText` | `String?` — **replaces** `successWidget`'s slot, see † | `null` |
+| `errorText` | `String?` — **replaces** `errorWidget`'s slot, see † | `null` |
+| `indicator` | `LoadingIndicator?` | theme, then circular |
+| `progress` | `double?` | `null` |
+| `progressStyle` | `LoadingProgressStyle?` | `indicator` |
+| `controller` | `LoadingButtonController?` | `null` |
+| `sizing` | `LoadingButtonSizing?` | `legacy` |
+| `width` / `height` | `double?` | `null` |
+| `style` | `LoadingButtonStyle?` | `null` |
+| `buttonStyle` | `ButtonStyle?` | `null` |
+| `colors` | `LoadingButtonColors?` | from the strategy |
+| `colorStrategy` | `LoadingButtonColorStrategy?` | `legacy` |
+| `animationDuration` | `Duration?` | `300ms` |
+| `successDuration` | `Duration?` | `2s` |
+| `errorDuration` | `Duration?` | `2s` |
+| `resetAfterDuration` | `bool` | `true` |
+| `enabled` | `bool` | `true` |
+| `debounce` | `Duration?` | none |
+| `cooldown` | `Duration?` | none |
+| `enableHapticFeedback` | `bool?` | `true` |
+| `focusNode` | `FocusNode?` | `null` |
+| `autofocus` | `bool` | `false` |
+| `tooltip` | `String?` | `null` |
+| `transitionBuilder` | `Widget Function(Widget, Animation)?` | cross fade |
+| `onSuccess` | `VoidCallback?` | `null` |
+| `onFailure` | `void Function(Object, StackTrace)?` | `null` |
+| `onStateChanged` | `void Function(ActionState)?` | `null` |
+| `onError` | `void Function(dynamic)?` — **deprecated**, use `onFailure` | `null` |
+
+† Each of `loadingText` / `successText` / `errorText` **replaces** the widget for that state rather
+than captioning it, and doubles as the state's accessible label. See
+[Indicators](#indicators) for the full resolution order and how to show both.
+
+`ButtonType` is `elevated`, `filled`, `outlined`, `text` or `icon`.
+
+## Thinking orbs
+
+`ThinkingOrb` is an animated indicator for AI and agent interfaces: filled circles over a rotated,
+z-sorted 3D point field, redrawn every frame by one `CustomPainter`. No shaders, no blurs, no image
+assets. It is a Dart port of the [thinking-orbs](#attribution) engine, and each state is a separate
+hand-tuned design rather than a variation on one loop.
+
+```dart
+const ThinkingOrb(state: OrbState.searching, size: 64)
+```
+
+
+
+All nine states animating at `size: 64`, in the order of the table below. Every mounted orb reads
+the same clock, which is why they stay in step.
+
+### The nine states
+
+| `OrbState` | What it depicts | Default label |
+| ------------ | ------------------------------------------------------------ | ------------- |
+| `working` | Particles running on tilted orbits | Working |
+| `searching` | A scan meridian sweeping a dotted globe | Searching |
+| `solving` | Bands scrambling in quarter turns, then clicking back solved | Solving |
+| `listening` | A waveform rolling through the latitude rings | Listening |
+| `connecting` | A constellation wiring itself, packets running the edges | Connecting |
+| `weaving` | Three strands plaiting around the sphere | Weaving |
+| `composing` | An undulating multi-band sash | Composing |
+| `breathing` | A face-on ring, slowly morphing | Thinking |
+| `shaping` | A dotted outline morphing circle → triangle → square | Shaping |
+
+The default labels are the screen-reader descriptions in `kOrbSemanticLabels`:
+
+```dart
+kOrbSemanticLabels[OrbState.breathing]; // 'Thinking'
+```
+
+### Two size tiers
+
+Each state ships **two** tunings — different dot counts, dot radii and speeds — because a 64px design
+scaled down to 20px turns to mush. The tier is picked from `size` alone:
+
+| Tier | Applies when | Tuned at |
+| ------ | ------------- | -------- |
+| inline | `size < 40` | 20 px |
+| avatar | `size >= 40` | 64 px |
+
+
+
+The two tunings compared: in each cell, the 64px avatar design with the same state's 20px inline
+design beside it. Left to right, top to bottom: `working`, `searching`, `solving`, `listening`,
+`connecting`, `weaving`, `composing`, `breathing`, `shaping`. Single frames of continuous animations —
+run the [example gallery](example/) to see them move.
+
+The threshold is exported as `kOrbTierBreakpoint` (`40`). You never select a tier yourself — just
+pick the size you want.
+
+```dart
+const Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ThinkingOrb(state: OrbState.connecting, size: 18), // inline tuning
+ SizedBox(width: 8),
+ Text('Connecting to the agent…'),
+ ],
+)
+```
+
+### Theming an orb
+
+Orbs are monochrome by default and take their ink from the ambient `Theme` brightness.
+
+```dart
+const ThinkingOrb(
+ state: OrbState.composing,
+ size: 32,
+ theme: OrbTheme.dark, // auto (default) | dark | light
+ color: Color(0xFF7866FE), // optional tint; null keeps it monochrome
+ speed: 1.5, // multiplies the state's own tuned speed
+ paused: false,
+ semanticLabel: 'Drafting your reply',
+)
+```
+
+`OrbTheme.auto` follows `Theme.of(context).brightness`. `OrbTheme.dark` means *light ink for a dark
+surface*; `OrbTheme.light` means *dark ink for a light surface*.
+
+### Motion, reduced motion and the shared clock
+
+- Every mounted orb reads **the same static clock**, so several on screen stay in step no matter when
+ each one mounted.
+- Animation stops automatically when the orb's route is not current (via `TickerMode`), and when
+ `paused: true`.
+- When the platform asks for reduced motion (`MediaQuery.disableAnimationsOf`), the ticker stops and
+ a **representative still frame** is painted, so the orb still reads as itself instead of vanishing.
+
+### Orbs inside buttons
+
+Every Material button in the package accepts a `LoadingIndicator` (`ArgonButton` takes a plain
+`loader` widget instead — put a `ThinkingOrb` straight into it):
+
+```dart
+LoadingButton(
+ onPressed: () async => submit(),
+ // LoadingButton does not inherit the button's icon theme — see below.
+ indicator: const LoadingIndicator.orb(
+ state: OrbState.working,
+ size: 18,
+ color: Colors.white,
),
- loader: Container(
- padding: EdgeInsets.all(10),
- child: CircularProgressIndicator(
- color: Colors.white,
- ),
+ child: const Text('Ask the agent'),
+)
+
+ElevatedAutoLoadingButton.icon(
+ onPressed: () async => submit(),
+ indicator: const LoadingIndicator.orb(state: OrbState.working),
+ loadingLabel: const Text('Sending…'),
+ icon: const Icon(Icons.send),
+ label: const Text('Send'),
+)
+```
+
+**Where the defaults come from.** An orb with no `size` or `color` reads the ambient `IconTheme`. In
+the `*AutoLoadingButton` and `*LoadingButton` families the indicator is built *inside* the Material
+button, so that `IconTheme` is the one the button itself installs: the orb picks up the button's
+resolved foreground colour and icon size automatically, in both brightnesses.
+
+`LoadingButton` is the exception. It builds its indicator from its own `State`'s context, which sits
+**above** the Material button, so `IconTheme.of(context)` there resolves `ThemeData.iconTheme`
+(black87 at 24 logical pixels in a default light theme) rather than the button's foreground. Give a
+`LoadingButton` indicator an explicit `color:` and `size:`, as the first snippet does — otherwise a
+light-theme button that declares a white foreground will show dark dots at the wrong size.
+
+When an orb is used as a button indicator its own semantic label is suppressed — the button already
+announces "Loading", and a second label would double-announce.
+
+### Orbs app-wide
+
+Switch every Material button in a subtree over in one line, with a `LoadingButtonTheme` widget or a
+`ThemeData` extension (the extension can also differ between light and dark):
+
+```dart
+LoadingButtonTheme(
+ data: const LoadingButtonThemeData(
+ indicator: LoadingIndicator.orb(state: OrbState.working),
),
- onTap: (startLoading, stopLoading, btnState) {
- if (btnState == ButtonState.Idle) {
- startLoading();
- // Perform your operation
- doNetworkRequest().then((_) {
- stopLoading();
- });
- }
- },
+ child: home,
)
```
-## Builder Pattern (Recommended)
+See [Theming](#theming) for the full field list and resolution order.
+
+### ⚠️ `pumpAndSettle` and orbs
-Use the fluent builder pattern for cleaner code:
+An orb animates **continuously**. `tester.pumpAndSettle()` will never settle while one is mounted —
+it throws after its timeout. Drive the clock explicitly, or mount the orb paused:
```dart
-LoadingButtonBuilder.elevated()
- .onPressed(_handleSubmit)
- .child(Text('Submit'))
- .loadingText('Processing...')
- .successText('Done!')
- .errorText('Failed!')
- .style(LoadingButtonStyle(
- backgroundColor: Colors.blue,
- borderRadius: 25,
- ))
- .build()
+// NOT pumpAndSettle:
+await tester.pump(const Duration(milliseconds: 100));
+
+// or freeze it:
+await tester.pumpWidget(const MaterialApp(
+ home: ThinkingOrb(state: OrbState.working, size: 64, paused: true),
+));
+await tester.pumpAndSettle(); // fine — the ticker is stopped
```
-## Properties
+The same applies to any button showing `LoadingIndicator.orb()` while it is loading.
-### LoadingButton Properties
+## The AutoLoadingButton family
-| Property | Type | Default | Description |
-| -------------------- | --------------- | -------------- | ----------------------------------------- |
-| `child` | `Widget` | required | The widget to display when button is idle |
-| `iconData` | `IconData?` | `null` | Icon to display alongside text |
-| `onPressed` | `VoidCallback?` | `null` | Callback when button is pressed |
-| `duration` | `Duration` | `300ms` | Animation duration |
-| `loaderSize` | `double` | `20.0` | Size of the loading indicator |
-| `animateOnTap` | `bool` | `true` | Whether to animate on tap |
-| `resetAfterDuration` | `bool` | `true` | Auto-reset after success/error |
-| `errorColor` | `Color?` | `Colors.red` | Color for error state |
-| `successColor` | `Color?` | `Colors.green` | Color for success state |
-| `successIcon` | `IconData?` | `Icons.check` | Icon for success state |
-| `failedIcon` | `IconData?` | `Icons.error` | Icon for error state |
-| `iconColor` | `Color?` | `null` | Color for icons |
-| `showBox` | `bool` | `true` | Whether to show button container |
+Thin wrappers over the stock Material buttons. They enter the loading state for exactly as long as
+the async callback runs, then leave it — on **both** the success and the failure path. No success or
+error phase.
-### AutoLoadingButton Properties
+| Widget | Extra constructors |
+| --------------------------- | -------------------------------------- |
+| `ElevatedAutoLoadingButton` | `.icon` |
+| `FilledAutoLoadingButton` | `.icon`, `.tonal`, `.tonalIcon` |
+| `OutlinedAutoLoadingButton` | `.icon` |
+| `TextAutoLoadingButton` | `.icon` |
+| `IconAutoLoadingButton` | `.filled`, `.filledTonal`, `.outlined` |
-| Property | Type | Default | Description |
-| ----------- | --------------- | -------- | ----------------------- |
-| `onPressed` | `VoidCallback?` | `null` | Async callback function |
-| `child` | `Widget` | required | Button content |
-| `style` | `ButtonStyle?` | `null` | Button styling |
+```dart
+ElevatedAutoLoadingButton(
+ onPressed: () async => submit(),
+ loadingLabel: const Text('Submitting…'),
+ child: const Text('Submit'),
+)
+
+FilledAutoLoadingButton.tonal(
+ onPressed: () async => submit(),
+ loadingLabel: const Text('Working…'),
+ child: const Text('Tonal'),
+)
-### ArgonButton Properties
+// IconAutoLoadingButton has no child to label, so wrap it to keep an
+// accessible name while the indicator is showing.
+Semantics(
+ label: 'Search',
+ child: IconAutoLoadingButton.filledTonal(
+ onPressed: () async => submit(),
+ indicator: const LoadingIndicator.orb(state: OrbState.searching, size: 20),
+ icon: const Icon(Icons.search),
+ ),
+)
+```
-| Property | Type | Default | Description |
-| -------------- | ------------------------------------------- | ------------- | ------------------------------- |
-| `height` | `double` | `50.0` | Button height |
-| `width` | `double` | `350.0` | Button width |
-| `borderRadius` | `double` | `5.0` | Border radius |
-| `color` | `Color` | `Colors.blue` | Button background color |
-| `child` | `Widget` | required | Button content |
-| `loader` | `Widget?` | `null` | Custom loader widget |
-| `onTap` | `Function(Function, Function, ButtonState)` | required | Tap callback with state control |
+Shared parameters, beyond the ones the underlying Material button already takes:
-## Examples
+| Parameter | Meaning |
+| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `onPressed` | `AsyncCallback?` — the button is busy until this future settles |
+| `onLongPress` | `AsyncCallback?` — same treatment |
+| `loadingIcon` | Replaces the indicator outright |
+| `indicator` | A `LoadingIndicator`; falls back to the theme, then a spinner |
+| `loadingLabel` | Text beside the indicator (not on `IconAutoLoadingButton`, which has no label slot). Omit it and the button shrinks to just the indicator — **and loses its accessible name for the whole loading window**, since the indicator replaces the child. Pass it, or wrap the button in your own `Semantics`. |
+| `switchDuration` | The `AnimatedSize` transition as the button resizes (`kThemeAnimationDuration`) |
-### Basic Login Button
+`IconAutoLoadingButton` additionally takes `selectedIcon` / `selectedLoadingIcon` alongside
+`isSelected`, and has no `loadingLabel`, `switchDuration` or `onLongPress`.
-```dart
-class LoginScreen extends StatefulWidget {
- @override
- _LoginScreenState createState() => _LoginScreenState();
-}
+### Triggering one from outside
-class _LoginScreenState extends State {
+Every widget in this family has a state class extending `AutoLoadingButtonState`, with `doPress()`
+and `doLongPress()`. Both return the future of the run, so a caller that awaits gets the error
+instead of it going unobserved:
- void _handleLogin() async {
- try {
- // Simulate login process
- await Future.delayed(Duration(seconds: 2));
+```dart
+final key = GlobalKey>();
- // Simulate random success/failure
- if (DateTime.now().millisecondsSinceEpoch % 2 == 0) {
- throw Exception('Login failed');
- }
- } catch (e) {
- throw Exception('Login failed');
- }
- }
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- body: Center(
- child: LoadingButton(
- onPressed: _handleLogin,
- child: Text('Login'),
- iconData: Icons.login,
- successIcon: Icons.check_circle,
- failedIcon: Icons.error_outline,
- ),
- ),
- );
- }
+ElevatedAutoLoadingButton(
+ key: key,
+ onPressed: () async => submit(),
+ child: const Text('Submit'),
+);
+
+// elsewhere
+try {
+ await key.currentState?.doPress();
+} catch (error) {
+ showError(error);
}
```
-### File Upload with Progress
+Calling `doPress()` while a run is already in flight returns the **existing** future rather than
+starting a second run. On the tap path — where the future is discarded — a throwing callback is
+routed to `FlutterError.reportError` with package context.
+
+## The XxxLoadingButton family
+
+The same Material buttons, but **you** own the flag. Use these when the loading state already lives
+in a bloc, a provider or a notifier.
+
+`ElevatedLoadingButton`, `FilledLoadingButton`, `OutlinedLoadingButton` and `TextLoadingButton`, each
+with `.icon` (and `FilledLoadingButton` also with `.tonal` / `.tonalIcon`).
```dart
-class FileUploadButton extends StatelessWidget {
+ElevatedLoadingButton(
+ isLoading: _isLoading,
+ onPressed: _submit,
+ loadingLabel: const Text('Submitting…'),
+ child: const Text('Submit'),
+)
+
+FilledLoadingButton.tonalIcon(
+ isLoading: _isLoading,
+ onPressed: _submit,
+ indicator: const LoadingIndicator.orb(state: OrbState.working),
+ loadingLabel: const Text('Running…'),
+ icon: const Icon(Icons.bolt),
+ label: const Text('Run'),
+)
+
+OutlinedLoadingButton(
+ isLoading: _isLoading,
+ loadingClickable: true, // stays tappable while loading — e.g. to cancel
+ onPressed: _submit,
+ loadingLabel: const Text('Cancel'),
+ child: const Text('Cancellable'),
+)
+```
+
+`isLoading`, `onPressed` and `child` (or `icon` + `label`) are required. `onPressed` here is a plain
+`VoidCallback?`, not an `AsyncCallback`. Unless `loadingClickable` is true, the button is disabled
+while `isLoading`.
+
+Every snippet above passes `loadingLabel`. That is deliberate: the indicator replaces the child, so
+without a `loadingLabel` the button has no accessible name while busy.
+
+There is no `IconLoadingButton`; use `IconAutoLoadingButton` for an icon button.
- Future _uploadFile() async {
- _isLoading = true;
- // Simulate file upload
- await Future.delayed(Duration(seconds: 3));
+## ArgonButton and ArgonTimerButton
- // Simulate random success/failure
- if (Random().nextBool()) {
- throw Exception('Upload failed');
+`ArgonButton` animates its width down to a pill and shows a loader inside it. You drive it with the
+`startLoading` / `stopLoading` callbacks handed to `onTap`:
+
+```dart
+ArgonButton(
+ height: 50,
+ width: 350,
+ borderRadius: 5,
+ color: const Color(0xFF7866FE),
+ loader: const Padding(
+ padding: EdgeInsets.all(10),
+ child: ThinkingOrb(state: OrbState.working, size: 28),
+ ),
+ onTap: (Function startLoading, Function stopLoading, ArgonButtonState btnState) {
+ if (btnState == ArgonButtonState.idle) {
+ startLoading();
+ doNetworkRequest().whenComplete(() => stopLoading());
}
- }
-
- @override
- Widget build(BuildContext context) {
- return LoadingButton(
- onPressed: _uploadFile,
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(Icons.cloud_upload),
- SizedBox(width: 8),
- Text('Upload File'),
- ],
- ),
- );
- }
-}
+ },
+ child: const Text('Continue', style: TextStyle(color: Colors.white, fontSize: 18)),
+)
```
-### Custom Styled Button
+`ArgonButtonState` has two values, `idle` and `busy`. A `null` `onTap` disables the button. Both
+`startLoading` and `stopLoading` are safe to call after the button has been disposed — a request
+completing after the user navigated away is the normal case.
+
+`ArgonTimerButton` is the countdown variant: `loader` is a `Widget Function(int seconds)` builder, and
+`onTap` receives a `startTimer` function that takes the number of seconds.
```dart
-LoadingButtonBuilder.filled()
- .onPressed(_handleSubmit)
- .child(Text('Custom Button'))
- .style(LoadingButtonStyle(
- backgroundColor: Colors.deepPurple,
- foregroundColor: Colors.white,
- loadingBackgroundColor: Colors.deepPurple.shade300,
- successBackgroundColor: Colors.green,
- errorBackgroundColor: Colors.red,
- borderRadius: 30,
- elevation: 8,
- padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
- ))
- .loadingText('Processing...')
- .successText('Success!')
- .errorText('Try Again')
- .build()
-```
-
-## Global Configuration
-
-Set global defaults for all buttons:
-
-```dart
-void main() {
- // Configure global settings
- LoadingButtonConfig().defaultAnimationDuration = Duration(milliseconds: 400);
- LoadingButtonConfig().enableHapticFeedback = true;
- LoadingButtonConfig().defaultLoadingWidget = MyCustomSpinner();
-
- runApp(MyApp());
-}
+ArgonTimerButton(
+ height: 50,
+ width: 350,
+ borderRadius: 5,
+ color: const Color(0xFF7866FE),
+ initialTimer: 0,
+ loader: (int seconds) => Text(
+ 'Resend in $seconds s',
+ style: const TextStyle(color: Colors.white, fontSize: 18),
+ ),
+ onTap: (Function startTimer, ArgonButtonState? btnState) {
+ if (btnState == ArgonButtonState.idle) {
+ resend();
+ startTimer(30);
+ }
+ },
+ child: const Text('Send code', style: TextStyle(color: Colors.white, fontSize: 18)),
+)
```
-## Accessibility
+`startTimer` throws an `ArgumentError` if handed a non-positive duration. Set `initialTimer` to start
+the countdown as soon as the button mounts.
-The package includes full accessibility support:
+## Accessibility
-- **Semantic Labels**: Automatic screen reader support
-- **Focus Management**: Proper keyboard navigation
-- **State Announcements**: Screen reader announces state changes
-- **High Contrast**: Supports high contrast mode
+### What `LoadingButton` does
+
+The button semantics below are **`LoadingButton`'s alone**. The `*AutoLoadingButton` and
+`*LoadingButton` families are thinner wrappers and publish no semantics of their own — read
+[Things to handle yourself](#things-to-handle-yourself) before relying on anything here for those.
+(The orb and tooltip items are the exceptions: they apply wherever a `ThinkingOrb` or a `tooltip` is
+used.)
+
+- **Real Material buttons underneath.** `LoadingButton` renders an `ElevatedButton`, `FilledButton`,
+ `OutlinedButton`, `TextButton` or `IconButton`, so it inherits the button role, keyboard
+ activation, focus highlight and minimum tap target from the framework. `focusNode` and `autofocus`
+ are passed through.
+- **Transient state announcements.** In `loading`, `success` and `error` the child is wrapped in a
+ `Semantics` node with `liveRegion: true`, so a screen reader announces the change without the user
+ refocusing. The label is `loadingText` / `successText` / `errorText`, falling back to `'Loading'`,
+ `'Success'` and `'Error'`.
+- **No duplicate semantics.** The idle state adds no label of its own, so your `child` provides the
+ button's accessible name. Earlier releases published a second `Semantics` button node here, which
+ read the button out twice.
+- **Orb labels.** `ThinkingOrb` publishes a per-state label from `kOrbSemanticLabels`. Override it
+ with `semanticLabel`, or pass `''` to hide the orb from accessibility tools when a nearby label
+ already describes it — which is what `LoadingIndicator.orb()` does inside a button.
+- **Reduced motion.** `ThinkingOrb` honours `MediaQuery.disableAnimationsOf` and paints a
+ representative still frame instead of animating.
+- **Tooltips.** `LoadingButton.tooltip` and `IconAutoLoadingButton.tooltip` add a `Tooltip`, which is
+ itself exposed to screen readers.
+- **Haptics** are opt-out via `enableHapticFeedback` on the button or the theme.
```dart
LoadingButton(
- onPressed: _handleSubmit,
- child: Text('Submit'),
- // Accessibility labels
- loadingText: 'Processing your request',
- successText: 'Request completed successfully',
- errorText: 'Request failed, please try again',
+ onPressed: () async => submit(),
+ loadingText: 'Submitting your order',
+ successText: 'Order placed',
+ errorText: 'Order failed, try again',
+ tooltip: 'Place the order',
+ child: const Text('Place order'),
)
```
-## Migration Guide
+### Things to handle yourself
+
+- **The other two families publish no loading semantics.** The `*AutoLoadingButton` and
+ `*LoadingButton` families add no `Semantics` node and no `liveRegion`; nothing above applies to
+ them. Worse, because the indicator *replaces* the child, the button's accessible name disappears
+ for the whole loading window: a button that reads `label: "Send"` while idle exposes no label at
+ all while busy. Passing `loadingLabel` is what keeps a name there — the label sits beside the
+ indicator and becomes the button's accessible name while loading. If you need a name that differs
+ from the visible label (or none is visible, as on an `IconAutoLoadingButton`), wrap the button in
+ your own `Semantics`:
+
+ ```dart
+ Semantics(
+ label: 'Refresh',
+ child: IconAutoLoadingButton(
+ onPressed: () async => refresh(),
+ icon: const Icon(Icons.refresh),
+ tooltip: 'Refresh',
+ ),
+ )
+ ```
+
+- **Localisation.** The fallback labels are English literals. Pass your own localised `loadingText` /
+ `successText` / `errorText` and `semanticLabel`.
+- **Text scaling.** The default `LoadingButtonSizing.legacy` is a fixed box that does **not** grow
+ with the platform text scale, so long labels at large scales will clip. Use
+ `LoadingButtonSizing.intrinsic()` if your labels must scale.
+- **Contrast.** There is no high-contrast-specific handling. The default
+ `LoadingButtonColorStrategy.legacy` paints unconditional white on `Theme.primaryColor`, which is
+ not dark-mode safe; switch to `material3` (or supply `LoadingButtonColors.fromScheme`) for
+ contrast-safe role pairs in both brightnesses.
+
+## Testing
+
+```dart
+testWidgets('button reaches success', (WidgetTester tester) async {
+ final LoadingButtonController controller = LoadingButtonController();
+ addTearDown(controller.dispose);
+
+ await tester.pumpWidget(MaterialApp(
+ home: Scaffold(
+ body: LoadingButton(
+ controller: controller,
+ animationDuration: const Duration(milliseconds: 1),
+ onPressed: () async {},
+ child: const Text('Submit'),
+ ),
+ ),
+ ));
+
+ await tester.tap(find.text('Submit'));
+ await tester.pump(); // apply the state change
+ await tester.pump(const Duration(milliseconds: 16)); // run out the cross fade
-### From v0.0.X to v1.0.X
+ expect(controller.state, ActionState.success);
+});
+```
-- Update import statements
-- Replace deprecated properties with new equivalents
-- Use `LoadingButtonStyle` for styling instead of individual properties
+- **Never `pumpAndSettle` around an orb.** It animates forever. Use `tester.pump(duration)`, or mount
+ with `paused: true`.
+- **Collapse the cross fade.** `animationDuration: Duration(milliseconds: 1)` keeps two children from
+ overlapping in the `AnimatedSwitcher`, so your finders match exactly one widget.
+- **Assert on the controller, not the pixels.** `controller.state`, `controller.progress` and
+ `controller.lastError` are the stable surface.
+- **Haptics are fire-and-forget.** The button never awaits the platform channel, so a press resolves
+ normally on a test binding. `enableHapticFeedback: false` avoids the calls entirely.
+- **Success and error windows** are real timers. Advance past `successDuration` / `errorDuration` to
+ observe the return to idle, or set `resetAfterDuration: false`.
-See the [Migration Guide](https://pub.dev/packages/loading_icon_button/changelog) for detailed instructions.
+## Platform support
-## Platform Support
+Pure Dart and Flutter — no platform channels beyond `HapticFeedback`, no plugins, no native code, no
+third-party dependencies. Orbs are drawn with `CustomPainter` (no shaders, no image assets), so they
+render identically everywhere.
| Platform | Support |
| -------- | ------- |
-| Android | ✅ |
-| iOS | ✅ |
-| Linux | ✅ |
-| macOS | ✅ |
-| Web | ✅ |
-| Windows | ✅ |
+| Android | ✅ |
+| iOS | ✅ |
+| Linux | ✅ |
+| macOS | ✅ |
+| Web | ✅ |
+| Windows | ✅ |
-## Contributing
+Haptic feedback is a no-op on platforms that do not provide it.
-Contributions are welcome! Please feel free to submit a Pull Request.
+## Deprecations
-### Development Setup
+Everything below still works and still compiles; each has a direct replacement.
-1. Clone the repository
-2. Run `flutter pub get`
-3. Run `flutter test` to ensure tests pass
-4. Make your changes
-5. Add tests for new features
-6. Submit a pull request
+| Deprecated | Replacement |
+| ----------------------------------------- | ------------------------------------------------ |
+| `LoadingButton.onError` | `onFailure(Object error, StackTrace stack)` |
+| `LoadingButtonConfig` | `LoadingButtonThemeData` + `LoadingButtonTheme` |
+| `IconButtonLoading` | Compose your own `Row` of an `Icon` and a `Text` |
+| `buildChildWithIcon` / `buildChildWithIC` | Same |
+| `buildText` | `Text(text, style: style)` |
+
+```dart
+// before
+LoadingButton(onPressed: () async => submit(), onError: (dynamic e) => showError(e as Object), child: const Text('Old'));
+
+// after
+LoadingButton(onPressed: () async => submit(), onFailure: (Object e, StackTrace s) => report(e, s), child: const Text('New'));
+```
+
+Two other 1.1.0 changes worth knowing:
+
+- The `ButtonStateExtension` extension is now `ActionStateExtension`. Extension members resolve by
+ member name, so `state.isLoading` and friends keep compiling unchanged; only an explicit extension
+ override (`ButtonStateExtension(state).isIdle`) needs updating.
+- `ActionState.isInteractive` used to be an exact duplicate of `isIdle`. It now means "a press would
+ be accepted" — true for everything except `loading` and `disabled`. Use `isIdle` for the old
+ meaning.
-## Issues and Feedback
+See [MIGRATION.md](https://github.com/itsarvinddev/loading_icon_button/blob/master/MIGRATION.md) and
+the [changelog](https://pub.dev/packages/loading_icon_button/changelog).
-Please file issues and feedback on the [GitHub Issues](https://github.com/itsarvinddev/loading_icon_button/issues) page.
+## LLM and AI assistant support
-## License
+If you write Flutter with Claude, Copilot, Cursor or similar, start here. **This package needs it
+more than most:** the README shipped before 1.1.0 still documented the 0.0.x API — `iconData`,
+`successIcon`, `failedIcon`, `showBox`, `loaderSize`, `animateOnTap` — which the 1.0.0 rewrite had
+already removed, alongside spellings that never existed in any release (`ButtonState.Idle`, a bare
+`AutoLoadingButton` class). Either way none of it compiles against 1.1.0, so assistants trained on
+that README write code that does not build. The antidote is below.
-This project is licensed under the MIT License - see the [LICENSE](https://github.com/itsarvinddev/loading_icon_button/blob/master/LICENSE) file for details.
+### Point your assistant at `llms.txt`
-## Acknowledgments
+[`llms.txt`](https://raw.githubusercontent.com/itsarvinddev/loading_icon_button/master/llms.txt) is a
+compact, machine-readable digest of the whole public API: every parameter, type, default, the nine
+orb states, the testing caveats, and a list of the hallucinated symbols with their real
+replacements. Paste this into your assistant:
-Special thanks to the Flutter community and contributors who made this package possible.
+```text
+Read https://raw.githubusercontent.com/itsarvinddev/loading_icon_button/master/llms.txt and use it as the API reference for loading_icon_button.
+```
+
+If your tool cannot fetch URLs, drop the file into the repo (many assistants pick up a root
+`llms.txt` automatically) or paste the block below.
+
+### Pasteable context block
+
+Short enough to paste into any chat. These are the facts assistants get wrong most often:
+
+```text
+loading_icon_button 1.1.0 — import 'package:loading_icon_button/loading_icon_button.dart';
+Facts to respect (the pre-1.1.0 README was wrong; ignore it):
+1. The state enum is ActionState {idle, loading, success, error, disabled} — lowerCamelCase.
+ There is no ButtonState and no ButtonState.Idle.
+2. LoadingButton.onPressed is AsyncCallback? (Future Function()), so it must be
+ `() async { ... }`. On ElevatedLoadingButton/FilledLoadingButton/etc. onPressed is a plain
+ VoidCallback? instead.
+3. There is no AutoLoadingButton class. Use ElevatedAutoLoadingButton, FilledAutoLoadingButton,
+ OutlinedAutoLoadingButton, TextAutoLoadingButton or IconAutoLoadingButton.
+4. These parameters do not exist: iconData, successIcon, failedIcon, iconColor, showBox,
+ loaderSize, animateOnTap, duration, errorColor, successColor. Use successWidget/errorWidget
+ (or successText/errorText), indicator:, animationDuration:, colors: and sizing:.
+5. loadingText/successText/errorText REPLACE the state's widget; they do not caption it.
+ To show a spinner plus a caption, build a Row and pass it as loadingWidget.
+6. LoadingButton's default sizing is a fixed 200x50 box and its default colours are not
+ dark-mode safe. New code should pass sizing: LoadingButtonSizing.intrinsic() and
+ colorStrategy: LoadingButtonColorStrategy.material3.
+7. ThinkingOrb animates forever, so tester.pumpAndSettle() never settles while one is mounted.
+ Use tester.pump(duration) or mount with paused: true.
+```
+
+### Task prompts
+
+Copy the one that matches your job. Each names the exact symbols to use and what to avoid.
+
+**Add a loading submit button to an existing form**
+
+```text
+In my Flutter app, replace the submit button on with loading_icon_button's LoadingButton
+(import 'package:loading_icon_button/loading_icon_button.dart').
+
+Requirements:
+- onPressed is an AsyncCallback: `onPressed: () async => ...` calling my existing submit function.
+- Pass sizing: const LoadingButtonSizing.intrinsic() so it sizes to its content, and
+ colorStrategy: LoadingButtonColorStrategy.material3 so success/error colours come from the
+ ColorScheme.
+- Use successText: and errorText: for the terminal states, and
+ onFailure: (Object error, StackTrace stack) { ... } for error reporting.
+- Use enabled: to disable it while the form is invalid, plus tooltip: to explain why.
+- child: is the idle label.
+
+Do NOT use: iconData, successIcon, failedIcon, showBox, loaderSize, animateOnTap, duration,
+errorColor, successColor, ButtonState, or the deprecated onError — none of those exist or are
+current. Do not add a second Semantics node around it; LoadingButton publishes its own live-region
+label from loadingText/successText/errorText.
+```
+
+**Show determinate upload progress driven from outside the widget**
+
+```text
+Using loading_icon_button, wire my existing upload function to a LoadingButton that shows real
+progress.
+
+- Create a LoadingButtonController in the State, dispose it in dispose().
+- Pass it as LoadingButton(controller: controller, ...) and call controller.setProgress(fraction)
+ from the upload function as bytes complete (0.0..1.0; pass null to go back to indeterminate).
+- Set progressStyle: LoadingProgressStyle.both so the indicator goes determinate AND the
+ background fills.
+- Render a percentage label next to the button with
+ ValueListenableBuilder(valueListenable: controller, ...), reading
+ value.isDeterminate and value.progress.
+- On failure call controller.error(error, stackTrace); on success controller.success().
+
+Notes: when a controller is attached, controller.progress wins over the progress: argument, so do
+not pass both. LoadingButtonValue fields are state, progress, error, stackTrace. Do not invent a
+`percent` or `value` parameter on LoadingButton.
+```
+
+**Put a thinking orb in a chat/agent UI and switch the whole app over**
+
+```text
+Using loading_icon_button, add an animated ThinkingOrb to my chat UI and make every loading button
+in the app use an orb instead of a spinner.
+
+- While the agent is streaming, show ThinkingOrb(state: OrbState.working, size: 64) in the message
+ list. Use OrbState.searching while it is calling a search tool and OrbState.composing while it is
+ drafting. The nine states are working, searching, solving, listening, connecting, weaving,
+ composing, breathing, shaping.
+- For an inline orb beside text use size: 18 — sizes below kOrbTierBreakpoint (40) get a separate
+ tuning designed for small sizes. Do not scale a 64px orb down with Transform.
+- Switch the app over by adding a ThemeExtension:
+ ThemeData(extensions: const >[
+ LoadingButtonThemeData(indicator: LoadingIndicator.orb(state: OrbState.working)),
+ ])
+ That covers LoadingButton and every *AutoLoadingButton / *LoadingButton beneath it.
+- Any explicit indicator on a LoadingButton needs its own size: and color:, because LoadingButton
+ builds its indicator above the Material button and so does not inherit the button's IconTheme.
+
+Orbs are indeterminate by design: if a button also reports progress, use
+LoadingProgressStyle.fill. Do not pass a `duration` or `loaderSize`; the parameters are state,
+size, theme (OrbTheme.auto/dark/light), speed, paused, color and semanticLabel.
+```
+
+**Migrate code written against the old or hallucinated API**
+
+```text
+This file was written against an old/incorrect loading_icon_button API and no longer compiles
+against 1.1.0. Fix it without changing behaviour. Apply these replacements:
+
+- ButtonState -> ActionState, and capitalised values -> lowerCamelCase
+ (ButtonState.Idle -> ActionState.idle, and likewise loading, success, error, disabled).
+- A bare `AutoLoadingButton` -> ElevatedAutoLoadingButton (or the Filled/Outlined/Text/Icon
+ variant that matches the surrounding style).
+- iconData: Icons.x -> put an Icon in child:, or switch to the `.icon` constructor with
+ icon: and label:.
+- successIcon:/failedIcon: -> successWidget: Icon(...) / errorWidget: Icon(...).
+- errorColor:, successColor: -> colors: LoadingButtonColors(success: ..., error: ...).
+- iconColor: -> style: LoadingButtonStyle(foregroundColor: ...), or the standard buttonStyle:.
+ LoadingButtonColors has no icon slot; its onLoading/onSuccess/onError are per-state foregrounds.
+- showBox: -> remove; control geometry with sizing: (LoadingButtonSizing).
+- loaderSize: -> indicator: LoadingIndicator.circular(size: N).
+- duration: -> animationDuration:.
+- animateOnTap: -> remove; there is no equivalent.
+- onError: (dynamic e) -> onFailure: (Object error, StackTrace stack).
+- A synchronous onPressed on LoadingButton -> make it `() async { ... }`.
+- ButtonStateExtension(state).isIdle -> state.isIdle.
+
+Then confirm it compiles with `flutter analyze`. Do not add any parameter you have not verified
+exists; check llms.txt at
+https://raw.githubusercontent.com/itsarvinddev/loading_icon_button/master/llms.txt if unsure.
+```
+
+**Write widget tests for a screen that uses these buttons**
+
+```text
+Write widget tests for , which uses loading_icon_button.
+
+Rules for this package:
+- NEVER call tester.pumpAndSettle() if a ThinkingOrb or a LoadingIndicator.orb() is mounted — an
+ orb animates continuously and pumpAndSettle throws on timeout. Use
+ await tester.pump(const Duration(milliseconds: 100)), or mount the orb with paused: true.
+- Build the button with animationDuration: const Duration(milliseconds: 1) so the AnimatedSwitcher
+ cross fade does not leave two children mounted; otherwise finders match two widgets.
+- Assert on a LoadingButtonController (controller.state, controller.progress, controller.lastError)
+ rather than on rendered pixels, and addTearDown(controller.dispose).
+- successDuration/errorDuration are real Timers: pump past them to observe the return to idle, or
+ set resetAfterDuration: false to park the button in its terminal state.
+- Pass enableHapticFeedback: false to avoid platform-channel calls.
+- ActionState values are idle, loading, success, error, disabled.
+
+Cover: tapping reaches ActionState.success; a throwing callback reaches ActionState.error and calls
+onFailure; a double tap runs the callback only once.
+```
+
+**Add an accessible icon-only loading button**
+
+```text
+Using loading_icon_button, add an icon-only button that shows a loading indicator while an async
+refresh runs.
+
+- Use IconAutoLoadingButton (there is no IconLoadingButton). Variants: the default, .filled,
+ .filledTonal and .outlined.
+- onPressed is an AsyncCallback: `onPressed: () async => refresh()`.
+- IconAutoLoadingButton has NO loadingLabel, and its indicator replaces the icon while loading, so
+ the button has no accessible name during the whole loading window. Wrap it in
+ Semantics(label: 'Refresh', child: ...) and also pass tooltip: 'Refresh'.
+- To use an orb instead of a spinner, pass
+ indicator: const LoadingIndicator.orb(state: OrbState.working, size: 20).
+```
+
+### Common mistakes, and the fix
+
+| Mistake | Fix |
+| ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
+| Passing a synchronous `onPressed` to `LoadingButton` | It is an `AsyncCallback`: `onPressed: () async { … }`. The `*LoadingButton` family takes a `VoidCallback?`. |
+| Expecting `loadingText` to caption the spinner | It replaces it. Build a `Row` and pass it as `loadingWidget`. |
+| Writing `ButtonState.Idle` | `ActionState.idle`. |
+| Reaching for `AutoLoadingButton` or `IconLoadingButton` | Neither exists. Use `ElevatedAutoLoadingButton` / `IconAutoLoadingButton`. |
+| `pumpAndSettle()` in a test with an orb on screen | `tester.pump(duration)`, or mount the orb with `paused: true`. |
+| Wondering why the button is 200×50 and clips long labels | That is `LoadingButtonSizing.legacy`, the default. Pass `LoadingButtonSizing.intrinsic()`. |
+| Dark-mode text coming out white-on-white | That is `LoadingButtonColorStrategy.legacy`, the default. Pass `material3`. |
+| An orb inside a `LoadingButton` rendering dark and oversized | `LoadingButton` builds its indicator above the Material button, so set `size:` and `color:` explicitly. |
+| Omitting `loadingLabel` on an `*AutoLoadingButton` | The button loses its accessible name while busy. Pass it, or wrap in `Semantics`. |
+| Passing both `progress:` and a `controller` | The controller wins. Drive it with `controller.setProgress()` only. |
+
+## Attribution
+
+The `ThinkingOrb` engine is a Dart port of **thinking-orbs**, MIT licensed:
+
+- Original concept and implementation: **[thinking-orbs](https://github.com/Jakubantalik/thinking-orbs)**
+ by Jakub Antalik.
+- Flutter port that this work builds on: **[thinking-orbs](https://github.com/iamEtornam/thinking-orbs)**
+ by Bright Sunu.
+
+Our Dart port is verified against upstream's published **golden vectors** — the exact dot positions,
+radii and depths each state produces at given times — so the animations match the originals
+numerically, not just by eye.
+
+Other credits:
- [Contributors](https://github.com/itsarvinddev/loading_icon_button/graphs/contributors)
-- Inspired by [rounded_loading_button](https://pub.dev/packages/rounded_loading_button)
-- Material Design guidelines
+- The Argon buttons are inspired by
+ [argon_buttons_flutter](https://pub.dev/packages/argon_buttons_flutter)
+- `LoadingButton` was originally inspired by
+ [rounded_loading_button](https://pub.dev/packages/rounded_loading_button)
+
+## Contributing
+
+Contributions are welcome.
+
+1. Clone the repository
+2. `flutter pub get`
+3. `flutter test`
+4. Make your change, with tests
+5. `dart analyze` and `dart format .`
+6. Open a pull request
+
+## License and issues
+
+MIT — see [LICENSE](https://github.com/itsarvinddev/loading_icon_button/blob/master/LICENSE).
-**Package:** [loading_icon_button](https://pub.dev/packages/loading_icon_button)
-**Repository:** [GitHub](https://github.com/itsarvinddev/loading_icon_button)
-**Issues:** [Report Issues](https://github.com/itsarvinddev/loading_icon_button/issues)
+File issues on the
+[GitHub issue tracker](https://github.com/itsarvinddev/loading_icon_button/issues). The package lives
+on [pub.dev](https://pub.dev/packages/loading_icon_button) and the source on
+[GitHub](https://github.com/itsarvinddev/loading_icon_button).
diff --git a/analysis_options.yaml b/analysis_options.yaml
index a5744c1..7ea21a5 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -1,4 +1,56 @@
+# Analysis configuration for the published library.
+#
+# The rule set is deliberately proportionate rather than maximal: a maximal set
+# produces 557 violations in lib/, most of them stylistic. What is enabled here
+# is everything that either found a real defect or costs nothing to keep clean,
+# and whose remaining violations could be fixed without touching the frozen
+# lib/src/orbs/ subtree. See test/analysis_options.yaml for the test-only
+# relaxations.
include: package:flutter_lints/flutter.yaml
+analyzer:
+ exclude:
+ - build/**
+ language:
+ # No implicit downcasts from dynamic, no implicitly raw or dynamic types.
+ # strict-casts caught a genuine bug: ArgonTimerButton.loader was typed
+ # `Function(int)?`, so it returned dynamic into a Widget position.
+ strict-casts: true
+ strict-inference: true
+ strict-raw-types: true
+
+linter:
+ rules:
+ # --- correctness ---------------------------------------------------------
+ - avoid_dynamic_calls
+ - avoid_slow_async_io
+ - cancel_subscriptions
+ - close_sinks
+ - literal_only_boolean_expressions
+ - no_adjacent_strings_in_list
+ - test_types_in_equals
+ - throw_in_finally
+ # This package hands user callbacks to timers and animation controllers;
+ # a dropped Future is the exact shape of the bug fixed in AutoLoadingButton.
+ - unawaited_futures
+ - unnecessary_await_in_return
+
+ # --- API hygiene ---------------------------------------------------------
+ - always_declare_return_types
+ - avoid_print
+ - use_enums
+ - use_setters_to_change_properties
+ - use_string_in_part_of_directives
+
+ # --- widget performance --------------------------------------------------
+ - prefer_const_constructors
+ - prefer_const_constructors_in_immutables
+ - prefer_const_literals_to_create_immutables
+ - prefer_final_locals
+
+ # --- consistency ---------------------------------------------------------
+ - prefer_single_quotes
+ - sort_pub_dependencies
+
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
diff --git a/doc/button-states-dark.png b/doc/button-states-dark.png
new file mode 100644
index 0000000..0bd8d5d
Binary files /dev/null and b/doc/button-states-dark.png differ
diff --git a/doc/button-states-light.png b/doc/button-states-light.png
new file mode 100644
index 0000000..c1da079
Binary files /dev/null and b/doc/button-states-light.png differ
diff --git a/doc/families.png b/doc/families.png
new file mode 100644
index 0000000..b40eea3
Binary files /dev/null and b/doc/families.png differ
diff --git a/doc/hero.webp b/doc/hero.webp
new file mode 100644
index 0000000..403c774
Binary files /dev/null and b/doc/hero.webp differ
diff --git a/doc/orbs.webp b/doc/orbs.webp
new file mode 100644
index 0000000..bf27ad0
Binary files /dev/null and b/doc/orbs.webp differ
diff --git a/doc/thinking-orbs.png b/doc/thinking-orbs.png
new file mode 100644
index 0000000..d8902f4
Binary files /dev/null and b/doc/thinking-orbs.png differ
diff --git a/example/.gitignore b/example/.gitignore
index 0fa6b67..5ce609b 100644
--- a/example/.gitignore
+++ b/example/.gitignore
@@ -1,3 +1,19 @@
+# Generated platform scaffolding.
+#
+# The gallery is pure Dart, so the runner projects are not checked in: they go
+# stale, they differ per Flutter version, and `pub get` rewrites generated
+# files inside them (windows/flutter/generated_plugins.cmake), which makes
+# `flutter pub publish` complain that the working tree is dirty.
+#
+# Recreate whichever ones you need with:
+# flutter create . --platforms=android,ios,web,macos,linux,windows
+android/
+ios/
+linux/
+macos/
+web/
+windows/
+
# Miscellaneous
*.class
*.log
@@ -8,6 +24,7 @@
.buildlog/
.history
.svn/
+migrate_working_dir/
# IntelliJ related
*.iml
@@ -22,25 +39,16 @@
# Flutter/Dart/Pub related
**/doc/api/
-**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
-.packages
.pub-cache/
.pub/
/build/
-
-# Web related
-lib/generated_plugin_registrant.dart
+pubspec.lock
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
-
-# Android Studio will place build artifacts here
-/android/app/debug
-/android/app/profile
-/android/app/release
diff --git a/example/.metadata b/example/.metadata
index fd70cab..5b76eb5 100644
--- a/example/.metadata
+++ b/example/.metadata
@@ -1,10 +1,14 @@
# This file tracks properties of this Flutter project.
-# Used by Flutter tool to assess capabilities and perform upgrades etc.
+# Used by the Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
+#
+# No `migration:` section is recorded here on purpose: the platform runner
+# directories are generated rather than checked in (see README.md), so there is
+# nothing per-platform for `flutter migrate` to track.
version:
- revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b
- channel: stable
+ revision: "d3b14c876900e553bc736ca19295fc09e3853e8e"
+ channel: "stable"
project_type: app
diff --git a/example/README.md b/example/README.md
index a135626..2f36023 100644
--- a/example/README.md
+++ b/example/README.md
@@ -1,16 +1,104 @@
-# example
+# loading_icon_button gallery
-A new Flutter project.
+A multi-page Material 3 gallery for every public widget in
+[`loading_icon_button`](../), in light and dark.
-## Getting Started
+| Page | What it covers |
+| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Overview** | One live sample of each family, and how to swap spinners for orbs globally. |
+| **LoadingButton** | All five `ButtonType`s, success/failure, `LoadingButtonController`, determinate progress, `LoadingButtonSizing`, `LoadingButtonColors`/`LoadingButtonColorStrategy`, `buttonStyle` vs `LoadingButtonStyle`, `LoadingIndicator`, debounce/cooldown/enabled, `LoadingButtonBuilder`, and `LoadingButtonTheme`. |
+| **Auto-loading** | `ElevatedAutoLoadingButton` and friends (including `.icon`, `.tonal`, `.tonalIcon` and all four `IconAutoLoadingButton` variants), `AutoLoadingButtonState.doPress()`, and the flag-driven `XxxLoadingButton` twins. |
+| **Argon** | `ArgonButton` and `ArgonTimerButton`: collapse shape, easing, custom loaders, countdowns. |
+| **Thinking orbs** | All nine `OrbState`s at both tuned size tiers, with theme, tint, speed and pause controls. |
-This project is a starting point for a Flutter application.
+Deprecated API (`LoadingButtonConfig`, `IconButtonLoading`, `buildChildWithIcon`,
+`buildChildWithIC`, `buildText`, `LoadingButton.onError`) is deliberately absent —
+the gallery only shows what you should reach for in new code.
-A few resources to get you started if this is your first Flutter project:
+## Running it
-- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
-- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
+```sh
+cd example
+flutter create . --platforms=macos # or android,ios,web,linux,windows
+flutter run
+```
-For help getting started with Flutter, view our
-[online documentation](https://flutter.dev/docs), which offers tutorials,
-samples, guidance on mobile development, and a full API reference.
+The first step is only needed once per checkout, and only for the platforms you
+actually want. See "Platform folders" below for why it isn't done for you.
+
+Everything else works without it:
+
+```sh
+flutter pub get
+flutter analyze
+flutter test
+```
+
+## Platform folders
+
+`android/`, `ios/`, `linux/`, `macos/`, `web/` and `windows/` are **not** checked
+in, and are listed in `.gitignore`. Three reasons:
+
+1. They were stale. The committed runners were generated years before the
+ current Flutter version and had drifted from what `flutter create` produces.
+2. They were incomplete. Only four of the package's six supported platforms had
+ a runner at all.
+3. `windows/flutter/generated_plugins.cmake` is a **generated** file that
+ `flutter pub get` rewrites. Keeping it in git meant every `pub get` dirtied
+ the working tree, which made `flutter pub publish` refuse to package a clean
+ tree.
+
+Nothing about the gallery is platform-specific — no plugins, no native code, no
+third-party dependencies at all — so `flutter create .` regenerates a correct,
+current runner for whichever platforms you want in a couple of seconds.
+
+It will try to edit two tracked files on the way. It rewrites `.metadata` to add
+a `migration:` section for the platforms you asked for, and it appends a
+`- /**` line to the `analyzer: exclude:` block of
+`analysis_options.yaml` for any platform it scaffolds. All six platform excludes
+are pre-listed in the checked-in `analysis_options.yaml` for exactly that
+reason, so today only `.metadata` actually changes — but revert both, in case a
+future Flutter adds a target that is not on the list. Either edit is local
+scaffolding bookkeeping, never something to commit:
+
+```sh
+git checkout -- .metadata analysis_options.yaml
+```
+
+before you commit anything else.
+
+CI does exactly this — see the `example` job in
+[`.github/workflows/ci.yaml`](../.github/workflows/ci.yaml), which runs
+`flutter create . --platforms=web` and then `flutter build web --release` on
+every push.
+
+## Dependencies
+
+Only `loading_icon_button` itself, via a path dependency, plus `flutter_lints`
+for analysis. No `google_fonts`, no `phosphor_flutter`: the gallery uses Material
+icons and the platform's default type ramp, so `flutter pub get` here pulls
+nothing from the network beyond the SDK's own packages.
+
+## Layout
+
+```
+lib/
+ main.dart app shell: themes, the light/dark toggle, navigation
+ gallery.dart page/section chrome shared by the demos
+ pages/
+ overview_page.dart
+ loading_button_page.dart
+ auto_loading_page.dart
+ argon_page.dart
+ orbs_page.dart
+test/
+ widget_test.dart drives the real app: navigation, theming, each family
+```
+
+## A note on tests
+
+A `ThinkingOrb` animates continuously, and the gallery mounts orbs on four of
+its five pages, so `tester.pumpAndSettle()` will never return. `widget_test.dart`
+drives time with explicit `tester.pump(duration)` calls instead — the same
+advice the package gives its own users. Mounting an orb with `paused: true` is
+the other way out.
diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml
index 61b6c4d..cb317ef 100644
--- a/example/analysis_options.yaml
+++ b/example/analysis_options.yaml
@@ -1,29 +1,25 @@
-# This file configures the analyzer, which statically analyzes Dart code to
-# check for errors, warnings, and lints.
-#
-# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
-# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
-# invoked from the command line by running `flutter analyze`.
-
-# The following line activates a set of recommended lints for Flutter apps,
-# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
+analyzer:
+ exclude:
+ - build/**
+ # The per-platform runner directories are generated by `flutter create .`
+ # rather than checked in (see README.md). All six are listed here even
+ # though none is present in git: `flutter create` appends an exclude for
+ # any platform it scaffolds, and finding them already here is what stops
+ # it from editing this tracked file.
+ - android/**
+ - ios/**
+ - linux/**
+ - macos/**
+ - web/**
+ - windows/**
+ language:
+ strict-casts: true
+ strict-raw-types: true
+
linter:
- # The lint rules applied to this project can be customized in the
- # section below to disable rules from the `package:flutter_lints/flutter.yaml`
- # included above or to enable additional rules. A list of all available lints
- # and their documentation is published at
- # https://dart-lang.github.io/linter/lints/index.html.
- #
- # Instead of disabling a lint rule for the entire project in the
- # section below, it can also be suppressed for a single line of code
- # or a specific dart file by using the `// ignore: name_of_lint` and
- # `// ignore_for_file: name_of_lint` syntax on the line or in the file
- # producing the lint.
rules:
- # avoid_print: false # Uncomment to disable the `avoid_print` rule
- # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
-
-# Additional information about this file can be found at
-# https://dart.dev/guides/language/analysis-options
+ # The gallery fires and forgets a lot of animations; make every one of
+ # those deliberate rather than accidental.
+ - unawaited_futures
diff --git a/example/android/.gitignore b/example/android/.gitignore
deleted file mode 100644
index 6f56801..0000000
--- a/example/android/.gitignore
+++ /dev/null
@@ -1,13 +0,0 @@
-gradle-wrapper.jar
-/.gradle
-/captures/
-/gradlew
-/gradlew.bat
-/local.properties
-GeneratedPluginRegistrant.java
-
-# Remember to never publicly share your keystore.
-# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
-key.properties
-**/*.keystore
-**/*.jks
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
deleted file mode 100644
index 5fe3c92..0000000
--- a/example/android/app/build.gradle
+++ /dev/null
@@ -1,68 +0,0 @@
-def localProperties = new Properties()
-def localPropertiesFile = rootProject.file('local.properties')
-if (localPropertiesFile.exists()) {
- localPropertiesFile.withReader('UTF-8') { reader ->
- localProperties.load(reader)
- }
-}
-
-def flutterRoot = localProperties.getProperty('flutter.sdk')
-if (flutterRoot == null) {
- throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
-}
-
-def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
-if (flutterVersionCode == null) {
- flutterVersionCode = '1'
-}
-
-def flutterVersionName = localProperties.getProperty('flutter.versionName')
-if (flutterVersionName == null) {
- flutterVersionName = '1.0'
-}
-
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
-
-android {
- compileSdkVersion flutter.compileSdkVersion
-
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
-
- kotlinOptions {
- jvmTarget = '1.8'
- }
-
- sourceSets {
- main.java.srcDirs += 'src/main/kotlin'
- }
-
- defaultConfig {
- // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
- applicationId "com.example.example"
- minSdkVersion flutter.minSdkVersion
- targetSdkVersion flutter.targetSdkVersion
- versionCode flutterVersionCode.toInteger()
- versionName flutterVersionName
- }
-
- buildTypes {
- release {
- // TODO: Add your own signing config for the release build.
- // Signing with the debug keys for now, so `flutter run --release` works.
- signingConfig signingConfigs.debug
- }
- }
-}
-
-flutter {
- source '../..'
-}
-
-dependencies {
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
-}
diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml
deleted file mode 100644
index c208884..0000000
--- a/example/android/app/src/debug/AndroidManifest.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
deleted file mode 100644
index 3f41384..0000000
--- a/example/android/app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt
deleted file mode 100644
index e793a00..0000000
--- a/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.example.example
-
-import io.flutter.embedding.android.FlutterActivity
-
-class MainActivity: FlutterActivity() {
-}
diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml
deleted file mode 100644
index f74085f..0000000
--- a/example/android/app/src/main/res/drawable-v21/launch_background.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml
deleted file mode 100644
index 304732f..0000000
--- a/example/android/app/src/main/res/drawable/launch_background.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index db77bb4..0000000
Binary files a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index 17987b7..0000000
Binary files a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index 09d4391..0000000
Binary files a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index d5f1c8d..0000000
Binary files a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 4d6372e..0000000
Binary files a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml
deleted file mode 100644
index 3db14bb..0000000
--- a/example/android/app/src/main/res/values-night/styles.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml
deleted file mode 100644
index d460d1e..0000000
--- a/example/android/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml
deleted file mode 100644
index c208884..0000000
--- a/example/android/app/src/profile/AndroidManifest.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/example/android/build.gradle b/example/android/build.gradle
deleted file mode 100644
index 4256f91..0000000
--- a/example/android/build.gradle
+++ /dev/null
@@ -1,31 +0,0 @@
-buildscript {
- ext.kotlin_version = '1.6.10'
- repositories {
- google()
- mavenCentral()
- }
-
- dependencies {
- classpath 'com.android.tools.build:gradle:4.1.0'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- }
-}
-
-allprojects {
- repositories {
- google()
- mavenCentral()
- }
-}
-
-rootProject.buildDir = '../build'
-subprojects {
- project.buildDir = "${rootProject.buildDir}/${project.name}"
-}
-subprojects {
- project.evaluationDependsOn(':app')
-}
-
-task clean(type: Delete) {
- delete rootProject.buildDir
-}
diff --git a/example/android/gradle.properties b/example/android/gradle.properties
deleted file mode 100644
index 94adc3a..0000000
--- a/example/android/gradle.properties
+++ /dev/null
@@ -1,3 +0,0 @@
-org.gradle.jvmargs=-Xmx1536M
-android.useAndroidX=true
-android.enableJetifier=true
diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index bc6a58a..0000000
--- a/example/android/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-#Fri Jun 23 08:50:38 CEST 2017
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
diff --git a/example/android/settings.gradle b/example/android/settings.gradle
deleted file mode 100644
index 44e62bc..0000000
--- a/example/android/settings.gradle
+++ /dev/null
@@ -1,11 +0,0 @@
-include ':app'
-
-def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
-def properties = new Properties()
-
-assert localPropertiesFile.exists()
-localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
-
-def flutterSdkPath = properties.getProperty("flutter.sdk")
-assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
-apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
diff --git a/example/ios/.gitignore b/example/ios/.gitignore
deleted file mode 100644
index 7a7f987..0000000
--- a/example/ios/.gitignore
+++ /dev/null
@@ -1,34 +0,0 @@
-**/dgph
-*.mode1v3
-*.mode2v3
-*.moved-aside
-*.pbxuser
-*.perspectivev3
-**/*sync/
-.sconsign.dblite
-.tags*
-**/.vagrant/
-**/DerivedData/
-Icon?
-**/Pods/
-**/.symlinks/
-profile
-xcuserdata
-**/.generated/
-Flutter/App.framework
-Flutter/Flutter.framework
-Flutter/Flutter.podspec
-Flutter/Generated.xcconfig
-Flutter/ephemeral/
-Flutter/app.flx
-Flutter/app.zip
-Flutter/flutter_assets/
-Flutter/flutter_export_environment.sh
-ServiceDefinitions.json
-Runner/GeneratedPluginRegistrant.*
-
-# Exceptions to above rules.
-!default.mode1v3
-!default.mode2v3
-!default.pbxuser
-!default.perspectivev3
diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist
deleted file mode 100644
index 7c56964..0000000
--- a/example/ios/Flutter/AppFrameworkInfo.plist
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- en
- CFBundleExecutable
- App
- CFBundleIdentifier
- io.flutter.flutter.app
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- App
- CFBundlePackageType
- FMWK
- CFBundleShortVersionString
- 1.0
- CFBundleSignature
- ????
- CFBundleVersion
- 1.0
- MinimumOSVersion
- 12.0
-
-
diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig
deleted file mode 100644
index ec97fc6..0000000
--- a/example/ios/Flutter/Debug.xcconfig
+++ /dev/null
@@ -1,2 +0,0 @@
-#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
-#include "Generated.xcconfig"
diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig
deleted file mode 100644
index c4855bf..0000000
--- a/example/ios/Flutter/Release.xcconfig
+++ /dev/null
@@ -1,2 +0,0 @@
-#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
-#include "Generated.xcconfig"
diff --git a/example/ios/Podfile b/example/ios/Podfile
deleted file mode 100644
index 279576f..0000000
--- a/example/ios/Podfile
+++ /dev/null
@@ -1,41 +0,0 @@
-# Uncomment this line to define a global platform for your project
-# platform :ios, '12.0'
-
-# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
-ENV['COCOAPODS_DISABLE_STATS'] = 'true'
-
-project 'Runner', {
- 'Debug' => :debug,
- 'Profile' => :release,
- 'Release' => :release,
-}
-
-def flutter_root
- generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
- unless File.exist?(generated_xcode_build_settings_path)
- raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
- end
-
- File.foreach(generated_xcode_build_settings_path) do |line|
- matches = line.match(/FLUTTER_ROOT\=(.*)/)
- return matches[1].strip if matches
- end
- raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
-end
-
-require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
-
-flutter_ios_podfile_setup
-
-target 'Runner' do
- use_frameworks!
- use_modular_headers!
-
- flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
-end
-
-post_install do |installer|
- installer.pods_project.targets.each do |target|
- flutter_additional_ios_build_settings(target)
- end
-end
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
deleted file mode 100644
index 60c2143..0000000
--- a/example/ios/Podfile.lock
+++ /dev/null
@@ -1,23 +0,0 @@
-PODS:
- - Flutter (1.0.0)
- - path_provider_foundation (0.0.1):
- - Flutter
- - FlutterMacOS
-
-DEPENDENCIES:
- - Flutter (from `Flutter`)
- - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
-
-EXTERNAL SOURCES:
- Flutter:
- :path: Flutter
- path_provider_foundation:
- :path: ".symlinks/plugins/path_provider_foundation/darwin"
-
-SPEC CHECKSUMS:
- Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
- path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
-
-PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011
-
-COCOAPODS: 1.16.2
diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj
deleted file mode 100644
index 631bc1e..0000000
--- a/example/ios/Runner.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,552 +0,0 @@
-// !$*UTF8*$!
-{
- archiveVersion = 1;
- classes = {
- };
- objectVersion = 54;
- objects = {
-
-/* Begin PBXBuildFile section */
- 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
- 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
- 574BD75C55BC31170568F9F3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D26F33BD83685E6E2585561 /* Pods_Runner.framework */; };
- 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
- 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
- 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
- 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
-/* End PBXBuildFile section */
-
-/* Begin PBXCopyFilesBuildPhase section */
- 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
- isa = PBXCopyFilesBuildPhase;
- buildActionMask = 2147483647;
- dstPath = "";
- dstSubfolderSpec = 10;
- files = (
- );
- name = "Embed Frameworks";
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXCopyFilesBuildPhase section */
-
-/* Begin PBXFileReference section */
- 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
- 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
- 2A07A68672188F6E64CA83EB /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
- 32963A1693501C535D7689AB /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
- 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
- 4D26F33BD83685E6E2585561 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
- 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
- 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
- 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
- 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
- 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
- 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
- 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
- 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
- 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- B73D44562289EA243E1D1CA6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
-/* End PBXFileReference section */
-
-/* Begin PBXFrameworksBuildPhase section */
- 97C146EB1CF9000F007C117D /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 574BD75C55BC31170568F9F3 /* Pods_Runner.framework in Frameworks */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXFrameworksBuildPhase section */
-
-/* Begin PBXGroup section */
- 9740EEB11CF90186004384FC /* Flutter */ = {
- isa = PBXGroup;
- children = (
- 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
- 9740EEB21CF90195004384FC /* Debug.xcconfig */,
- 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
- 9740EEB31CF90195004384FC /* Generated.xcconfig */,
- );
- name = Flutter;
- sourceTree = "";
- };
- 97C146E51CF9000F007C117D = {
- isa = PBXGroup;
- children = (
- 9740EEB11CF90186004384FC /* Flutter */,
- 97C146F01CF9000F007C117D /* Runner */,
- 97C146EF1CF9000F007C117D /* Products */,
- D0BED3D7C4D0BC3CDAA7490E /* Pods */,
- B1E57D56F4BD9B7AA4439295 /* Frameworks */,
- );
- sourceTree = "";
- };
- 97C146EF1CF9000F007C117D /* Products */ = {
- isa = PBXGroup;
- children = (
- 97C146EE1CF9000F007C117D /* Runner.app */,
- );
- name = Products;
- sourceTree = "";
- };
- 97C146F01CF9000F007C117D /* Runner */ = {
- isa = PBXGroup;
- children = (
- 97C146FA1CF9000F007C117D /* Main.storyboard */,
- 97C146FD1CF9000F007C117D /* Assets.xcassets */,
- 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
- 97C147021CF9000F007C117D /* Info.plist */,
- 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
- 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
- 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
- 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
- );
- path = Runner;
- sourceTree = "";
- };
- B1E57D56F4BD9B7AA4439295 /* Frameworks */ = {
- isa = PBXGroup;
- children = (
- 4D26F33BD83685E6E2585561 /* Pods_Runner.framework */,
- );
- name = Frameworks;
- sourceTree = "";
- };
- D0BED3D7C4D0BC3CDAA7490E /* Pods */ = {
- isa = PBXGroup;
- children = (
- 2A07A68672188F6E64CA83EB /* Pods-Runner.debug.xcconfig */,
- 32963A1693501C535D7689AB /* Pods-Runner.release.xcconfig */,
- B73D44562289EA243E1D1CA6 /* Pods-Runner.profile.xcconfig */,
- );
- name = Pods;
- path = Pods;
- sourceTree = "";
- };
-/* End PBXGroup section */
-
-/* Begin PBXNativeTarget section */
- 97C146ED1CF9000F007C117D /* Runner */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
- buildPhases = (
- 9BCD0D2CEF73104AD3338240 /* [CP] Check Pods Manifest.lock */,
- 9740EEB61CF901F6004384FC /* Run Script */,
- 97C146EA1CF9000F007C117D /* Sources */,
- 97C146EB1CF9000F007C117D /* Frameworks */,
- 97C146EC1CF9000F007C117D /* Resources */,
- 9705A1C41CF9048500538489 /* Embed Frameworks */,
- 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
- 153A61AC7EDC8C0C4341A46A /* [CP] Embed Pods Frameworks */,
- );
- buildRules = (
- );
- dependencies = (
- );
- name = Runner;
- productName = Runner;
- productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
- productType = "com.apple.product-type.application";
- };
-/* End PBXNativeTarget section */
-
-/* Begin PBXProject section */
- 97C146E61CF9000F007C117D /* Project object */ = {
- isa = PBXProject;
- attributes = {
- LastUpgradeCheck = 1510;
- ORGANIZATIONNAME = "";
- TargetAttributes = {
- 97C146ED1CF9000F007C117D = {
- CreatedOnToolsVersion = 7.3.1;
- LastSwiftMigration = 1100;
- };
- };
- };
- buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
- compatibilityVersion = "Xcode 9.3";
- developmentRegion = en;
- hasScannedForEncodings = 0;
- knownRegions = (
- en,
- Base,
- );
- mainGroup = 97C146E51CF9000F007C117D;
- productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
- projectDirPath = "";
- projectRoot = "";
- targets = (
- 97C146ED1CF9000F007C117D /* Runner */,
- );
- };
-/* End PBXProject section */
-
-/* Begin PBXResourcesBuildPhase section */
- 97C146EC1CF9000F007C117D /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
- 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
- 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
- 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXResourcesBuildPhase section */
-
-/* Begin PBXShellScriptBuildPhase section */
- 153A61AC7EDC8C0C4341A46A /* [CP] Embed Pods Frameworks */ = {
- isa = PBXShellScriptBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- inputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
- );
- name = "[CP] Embed Pods Frameworks";
- outputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
- showEnvVarsInLog = 0;
- };
- 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
- isa = PBXShellScriptBuildPhase;
- alwaysOutOfDate = 1;
- buildActionMask = 2147483647;
- files = (
- );
- inputPaths = (
- "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
- );
- name = "Thin Binary";
- outputPaths = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
- };
- 9740EEB61CF901F6004384FC /* Run Script */ = {
- isa = PBXShellScriptBuildPhase;
- alwaysOutOfDate = 1;
- buildActionMask = 2147483647;
- files = (
- );
- inputPaths = (
- );
- name = "Run Script";
- outputPaths = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
- };
- 9BCD0D2CEF73104AD3338240 /* [CP] Check Pods Manifest.lock */ = {
- isa = PBXShellScriptBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- inputFileListPaths = (
- );
- inputPaths = (
- "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
- "${PODS_ROOT}/Manifest.lock",
- );
- name = "[CP] Check Pods Manifest.lock";
- outputFileListPaths = (
- );
- outputPaths = (
- "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
- showEnvVarsInLog = 0;
- };
-/* End PBXShellScriptBuildPhase section */
-
-/* Begin PBXSourcesBuildPhase section */
- 97C146EA1CF9000F007C117D /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
- 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXSourcesBuildPhase section */
-
-/* Begin PBXVariantGroup section */
- 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
- isa = PBXVariantGroup;
- children = (
- 97C146FB1CF9000F007C117D /* Base */,
- );
- name = Main.storyboard;
- sourceTree = "";
- };
- 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
- isa = PBXVariantGroup;
- children = (
- 97C147001CF9000F007C117D /* Base */,
- );
- name = LaunchScreen.storyboard;
- sourceTree = "";
- };
-/* End PBXVariantGroup section */
-
-/* Begin XCBuildConfiguration section */
- 249021D3217E4FDB00AE95B9 /* Profile */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- ENABLE_NS_ASSERTIONS = NO;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- GCC_C_LANGUAGE_STANDARD = gnu99;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
- MTL_ENABLE_DEBUG_INFO = NO;
- SDKROOT = iphoneos;
- SUPPORTED_PLATFORMS = iphoneos;
- TARGETED_DEVICE_FAMILY = "1,2";
- VALIDATE_PRODUCT = YES;
- };
- name = Profile;
- };
- 249021D4217E4FDB00AE95B9 /* Profile */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- ENABLE_BITCODE = NO;
- INFOPLIST_FILE = Runner/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
- SWIFT_VERSION = 5.0;
- VERSIONING_SYSTEM = "apple-generic";
- };
- name = Profile;
- };
- 97C147031CF9000F007C117D /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = dwarf;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_TESTABILITY = YES;
- GCC_C_LANGUAGE_STANDARD = gnu99;
- GCC_DYNAMIC_NO_PIC = NO;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_OPTIMIZATION_LEVEL = 0;
- GCC_PREPROCESSOR_DEFINITIONS = (
- "DEBUG=1",
- "$(inherited)",
- );
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
- MTL_ENABLE_DEBUG_INFO = YES;
- ONLY_ACTIVE_ARCH = YES;
- SDKROOT = iphoneos;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Debug;
- };
- 97C147041CF9000F007C117D /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- ENABLE_NS_ASSERTIONS = NO;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- GCC_C_LANGUAGE_STANDARD = gnu99;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
- MTL_ENABLE_DEBUG_INFO = NO;
- SDKROOT = iphoneos;
- SUPPORTED_PLATFORMS = iphoneos;
- SWIFT_COMPILATION_MODE = wholemodule;
- SWIFT_OPTIMIZATION_LEVEL = "-O";
- TARGETED_DEVICE_FAMILY = "1,2";
- VALIDATE_PRODUCT = YES;
- };
- name = Release;
- };
- 97C147061CF9000F007C117D /* Debug */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- ENABLE_BITCODE = NO;
- INFOPLIST_FILE = Runner/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- SWIFT_VERSION = 5.0;
- VERSIONING_SYSTEM = "apple-generic";
- };
- name = Debug;
- };
- 97C147071CF9000F007C117D /* Release */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- ENABLE_BITCODE = NO;
- INFOPLIST_FILE = Runner/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
- SWIFT_VERSION = 5.0;
- VERSIONING_SYSTEM = "apple-generic";
- };
- name = Release;
- };
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
- 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 97C147031CF9000F007C117D /* Debug */,
- 97C147041CF9000F007C117D /* Release */,
- 249021D3217E4FDB00AE95B9 /* Profile */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 97C147061CF9000F007C117D /* Debug */,
- 97C147071CF9000F007C117D /* Release */,
- 249021D4217E4FDB00AE95B9 /* Profile */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
-/* End XCConfigurationList section */
- };
- rootObject = 97C146E61CF9000F007C117D /* Project object */;
-}
diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 919434a..0000000
--- a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d9810..0000000
--- a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
deleted file mode 100644
index f9b0d7c..0000000
--- a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- PreviewsEnabled
-
-
-
diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
deleted file mode 100644
index 9c12df5..0000000
--- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ /dev/null
@@ -1,90 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 21a3cc1..0000000
--- a/example/ios/Runner.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d9810..0000000
--- a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
deleted file mode 100644
index f9b0d7c..0000000
--- a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- PreviewsEnabled
-
-
-
diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift
deleted file mode 100644
index b636303..0000000
--- a/example/ios/Runner/AppDelegate.swift
+++ /dev/null
@@ -1,13 +0,0 @@
-import UIKit
-import Flutter
-
-@main
-@objc class AppDelegate: FlutterAppDelegate {
- override func application(
- _ application: UIApplication,
- didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
- ) -> Bool {
- GeneratedPluginRegistrant.register(with: self)
- return super.application(application, didFinishLaunchingWithOptions: launchOptions)
- }
-}
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index d36b1fa..0000000
--- a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,122 +0,0 @@
-{
- "images" : [
- {
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "Icon-App-20x20@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "Icon-App-20x20@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-App-40x40@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-App-40x40@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-App-60x60@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-App-60x60@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "Icon-App-20x20@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "Icon-App-20x20@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-App-29x29@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-App-29x29@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-App-40x40@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-App-40x40@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-App-76x76@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-App-76x76@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "83.5x83.5",
- "idiom" : "ipad",
- "filename" : "Icon-App-83.5x83.5@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "1024x1024",
- "idiom" : "ios-marketing",
- "filename" : "Icon-App-1024x1024@1x.png",
- "scale" : "1x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
deleted file mode 100644
index dc9ada4..0000000
Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
deleted file mode 100644
index 28c6bf0..0000000
Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
deleted file mode 100644
index 2ccbfd9..0000000
Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
deleted file mode 100644
index f091b6b..0000000
Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
deleted file mode 100644
index 4cde121..0000000
Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
deleted file mode 100644
index d0ef06e..0000000
Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
deleted file mode 100644
index dcdc230..0000000
Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
deleted file mode 100644
index 2ccbfd9..0000000
Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
deleted file mode 100644
index c8f9ed8..0000000
Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
deleted file mode 100644
index a6d6b86..0000000
Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
deleted file mode 100644
index a6d6b86..0000000
Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
deleted file mode 100644
index 75b2d16..0000000
Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
deleted file mode 100644
index c4df70d..0000000
Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
deleted file mode 100644
index 6a84f41..0000000
Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
deleted file mode 100644
index d0e1f58..0000000
Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ
diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
deleted file mode 100644
index 0bedcf2..0000000
--- a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "filename" : "LaunchImage.png",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "LaunchImage@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "filename" : "LaunchImage@3x.png",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
deleted file mode 100644
index 9da19ea..0000000
Binary files a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ
diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
deleted file mode 100644
index 9da19ea..0000000
Binary files a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ
diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
deleted file mode 100644
index 9da19ea..0000000
Binary files a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ
diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
deleted file mode 100644
index 89c2725..0000000
--- a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Launch Screen Assets
-
-You can customize the launch screen with your own desired assets by replacing the image files in this directory.
-
-You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
deleted file mode 100644
index f2e259c..0000000
--- a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard
deleted file mode 100644
index f3c2851..0000000
--- a/example/ios/Runner/Base.lproj/Main.storyboard
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist
deleted file mode 100644
index 41603ef..0000000
--- a/example/ios/Runner/Info.plist
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName
- Example
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- example
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleSignature
- ????
- CFBundleVersion
- $(FLUTTER_BUILD_NUMBER)
- LSRequiresIPhoneOS
-
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UIViewControllerBasedStatusBarAppearance
-
- CADisableMinimumFrameDurationOnPhone
-
- UIApplicationSupportsIndirectInputEvents
-
-
-
diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h
deleted file mode 100644
index 308a2a5..0000000
--- a/example/ios/Runner/Runner-Bridging-Header.h
+++ /dev/null
@@ -1 +0,0 @@
-#import "GeneratedPluginRegistrant.h"
diff --git a/example/lib/gallery.dart b/example/lib/gallery.dart
new file mode 100644
index 0000000..b45205e
--- /dev/null
+++ b/example/lib/gallery.dart
@@ -0,0 +1,236 @@
+/// Small presentation helpers shared by every gallery page.
+///
+/// Nothing here is part of `loading_icon_button`; it is just the chrome the
+/// demos sit in, kept in one place so the pages stay about the package.
+library;
+
+import 'package:flutter/material.dart';
+
+/// Pretends to do work for [duration], then returns.
+Future fakeWork([
+ Duration duration = const Duration(milliseconds: 1200),
+]) =>
+ Future.delayed(duration);
+
+/// Pretends to do work for [duration], then throws.
+Future fakeFailure([
+ Duration duration = const Duration(milliseconds: 900),
+]) async {
+ await Future.delayed(duration);
+ throw const _DemoException('The server said no.');
+}
+
+class _DemoException implements Exception {
+ const _DemoException(this.message);
+
+ final String message;
+
+ @override
+ String toString() => message;
+}
+
+/// The body of a gallery page: a centred, scrolling column of [GallerySection]s.
+class GalleryPage extends StatelessWidget {
+ const GalleryPage({
+ super.key,
+ required this.title,
+ required this.blurb,
+ required this.children,
+ });
+
+ /// Page heading.
+ final String title;
+
+ /// One or two sentences under the heading.
+ final String blurb;
+
+ /// The sections, in order.
+ final List children;
+
+ @override
+ Widget build(BuildContext context) {
+ final TextTheme text = Theme.of(context).textTheme;
+ return ListView(
+ padding: const EdgeInsets.fromLTRB(20, 20, 20, 64),
+ children: [
+ Center(
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 820),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(title, style: text.headlineSmall),
+ const SizedBox(height: 6),
+ Text(
+ blurb,
+ style: text.bodyMedium?.copyWith(
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ const SizedBox(height: 20),
+ ...children,
+ ],
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+}
+
+/// A titled card wrapping one demo.
+class GallerySection extends StatelessWidget {
+ const GallerySection({
+ super.key,
+ required this.title,
+ this.subtitle,
+ this.code,
+ required this.child,
+ });
+
+ /// Section heading.
+ final String title;
+
+ /// What the demo is showing, in prose.
+ final String? subtitle;
+
+ /// The one line of API this section is really about.
+ final String? code;
+
+ /// The demo itself.
+ final Widget child;
+
+ @override
+ Widget build(BuildContext context) {
+ final ThemeData theme = Theme.of(context);
+ return Card(
+ margin: const EdgeInsets.only(bottom: 16),
+ clipBehavior: Clip.antiAlias,
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(title, style: theme.textTheme.titleMedium),
+ if (subtitle != null) ...[
+ const SizedBox(height: 4),
+ Text(
+ subtitle!,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ if (code != null) ...[
+ const SizedBox(height: 10),
+ CodeLine(code!),
+ ],
+ const SizedBox(height: 16),
+ child,
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+/// One line of monospaced Dart, scrollable sideways when it is too long.
+class CodeLine extends StatelessWidget {
+ const CodeLine(this.code, {super.key});
+
+ final String code;
+
+ @override
+ Widget build(BuildContext context) {
+ final ColorScheme scheme = Theme.of(context).colorScheme;
+ return Container(
+ width: double.infinity,
+ decoration: BoxDecoration(
+ color: scheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Text(
+ code,
+ style: TextStyle(
+ fontFamily: 'monospace',
+ fontFamilyFallback: const ['Courier'],
+ fontSize: 12.5,
+ height: 1.5,
+ color: scheme.onSurfaceVariant,
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+/// A run of demo widgets that wraps onto the next line when space runs out.
+class DemoWrap extends StatelessWidget {
+ const DemoWrap({
+ super.key,
+ required this.children,
+ this.alignment = WrapAlignment.start,
+ });
+
+ final List children;
+ final WrapAlignment alignment;
+
+ @override
+ Widget build(BuildContext context) => Wrap(
+ spacing: 12,
+ runSpacing: 12,
+ alignment: alignment,
+ crossAxisAlignment: WrapCrossAlignment.center,
+ children: children,
+ );
+}
+
+/// A labelled demo, for grids where the label matters as much as the widget.
+class LabelledDemo extends StatelessWidget {
+ const LabelledDemo({
+ super.key,
+ required this.label,
+ required this.child,
+ this.width = 180,
+ });
+
+ final String label;
+ final Widget child;
+ final double width;
+
+ @override
+ Widget build(BuildContext context) => SizedBox(
+ width: width,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ label,
+ style: Theme.of(context).textTheme.labelSmall?.copyWith(
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ const SizedBox(height: 6),
+ child,
+ ],
+ ),
+ );
+}
+
+/// Shows [message] in the nearest [ScaffoldMessenger], briefly.
+void showNote(BuildContext context, String message) {
+ ScaffoldMessenger.of(context)
+ ..clearSnackBars()
+ ..showSnackBar(
+ SnackBar(
+ content: Text(message),
+ behavior: SnackBarBehavior.floating,
+ width: 360,
+ duration: const Duration(seconds: 2),
+ ),
+ );
+}
diff --git a/example/lib/main.dart b/example/lib/main.dart
index eefcfd0..9b4dd8b 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,452 +1,191 @@
import 'package:flutter/material.dart';
import 'package:loading_icon_button/loading_icon_button.dart';
-void main() {
- runApp(const MyApp());
+import 'pages/argon_page.dart';
+import 'pages/auto_loading_page.dart';
+import 'pages/loading_button_page.dart';
+import 'pages/orbs_page.dart';
+import 'pages/overview_page.dart';
+
+void main() => runApp(const GalleryApp());
+
+/// The gallery shell: Material 3, a seeded colour scheme, and a light/dark
+/// toggle so every demo can be checked in both brightnesses.
+class GalleryApp extends StatefulWidget {
+ const GalleryApp({super.key});
+
+ @override
+ State createState() => _GalleryAppState();
}
-class MyApp extends StatelessWidget {
- const MyApp({Key? key}) : super(key: key);
+class _GalleryAppState extends State {
+ ThemeMode _themeMode = ThemeMode.system;
+
+ void _toggleBrightness(Brightness current) {
+ setState(() {
+ _themeMode =
+ current == Brightness.dark ? ThemeMode.light : ThemeMode.dark;
+ });
+ }
+
+ /// One theme builder for both brightnesses.
+ ///
+ /// Note the [LoadingButtonThemeData] extension: it sets package-wide
+ /// defaults that resolve per brightness and rebuild their dependents, which
+ /// is what replaced the old process-wide `LoadingButtonConfig` singleton.
+ /// Individual demos below still override these per widget.
+ ThemeData _theme(Brightness brightness) {
+ final ColorScheme scheme = ColorScheme.fromSeed(
+ seedColor: const Color(0xFF4C5DF4),
+ brightness: brightness,
+ );
+ return ThemeData(
+ useMaterial3: true,
+ colorScheme: scheme,
+ extensions: const >[
+ LoadingButtonThemeData(
+ // The modern defaults. `LoadingButtonSizing.legacy` (a fixed 200x50
+ // box) is still the package default for backwards compatibility;
+ // new apps should opt into intrinsic sizing like this.
+ sizing: LoadingButtonSizing.intrinsic(),
+ colorStrategy: LoadingButtonColorStrategy.material3,
+ animationDuration: Duration(milliseconds: 250),
+ ),
+ ],
+ );
+ }
@override
Widget build(BuildContext context) {
return MaterialApp(
- title: 'Loading Icon Button Demo',
- theme: ThemeData(
- primarySwatch: Colors.blue,
- visualDensity: VisualDensity.adaptivePlatformDensity,
- ),
- home: const LoadingButtonDemo(),
+ title: 'loading_icon_button gallery',
+ debugShowCheckedModeBanner: false,
+ themeMode: _themeMode,
+ theme: _theme(Brightness.light),
+ darkTheme: _theme(Brightness.dark),
+ home: GalleryHome(onToggleBrightness: _toggleBrightness),
);
}
}
-class LoadingButtonDemo extends StatefulWidget {
- const LoadingButtonDemo({Key? key}) : super(key: key);
+/// A gallery destination: its label, its icons and the page it shows.
+class _Destination {
+ const _Destination(this.label, this.icon, this.selectedIcon);
- @override
- _LoadingButtonDemoState createState() => _LoadingButtonDemoState();
+ final String label;
+ final IconData icon;
+ final IconData selectedIcon;
}
-class _LoadingButtonDemoState extends State {
- // Progress tracking for the upload simulation
- double _uploadProgress = 0.0;
- bool _isUploading = false;
+const List<_Destination> _destinations = <_Destination>[
+ _Destination('Overview', Icons.dashboard_outlined, Icons.dashboard),
+ _Destination(
+ 'LoadingButton', Icons.smart_button_outlined, Icons.smart_button),
+ _Destination('Auto-loading', Icons.autorenew_outlined, Icons.autorenew),
+ _Destination('Argon', Icons.swipe_left_outlined, Icons.swipe_left),
+ _Destination('Thinking orbs', Icons.blur_on_outlined, Icons.blur_on),
+];
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- appBar: AppBar(
- title: const Text('Loading Icon Button Demo'),
- elevation: 0,
- ),
- body: SingleChildScrollView(
- padding: const EdgeInsets.all(20),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- // 1. AutoLoadingButton (Material Design)
- _buildSectionHeader('1. AutoLoadingButton (Material)'),
- ElevatedAutoLoadingButton(
- onPressed: () => _simulateAutoOperation(),
- child: const Text('Auto Loading Button'),
- style: ButtonStyle(
- backgroundColor: WidgetStateProperty.all(Colors.purple),
- foregroundColor: WidgetStateProperty.all(Colors.white),
- shape: WidgetStateProperty.all(RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
- )),
- elevation: WidgetStateProperty.all(4),
- ),
- ),
- const SizedBox(height: 30),
-
- // 2. Material LoadingButton with Builder Pattern
- _buildSectionHeader('2. Material LoadingButton (Builder)'),
- LoadingButtonBuilder.filled()
- .onPressed(() => _simulateUpload())
- .child(const Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(Icons.cloud_upload),
- SizedBox(width: 8),
- Text('Upload File'),
- ],
- ))
- .style(LoadingButtonStyle(
- backgroundColor: Colors.indigo,
- foregroundColor: Colors.white,
- loadingBackgroundColor: Colors.indigo.shade300,
- successBackgroundColor: Colors.green,
- errorBackgroundColor: Colors.red,
- borderRadius: 8,
- elevation: 2,
- ))
- .loadingText('Uploading...')
- .successText('Uploaded!')
- .errorText('Failed!')
- .build(),
- const SizedBox(height: 30),
+/// The navigation shell. A rail on wide windows, a bottom bar on narrow ones.
+class GalleryHome extends StatefulWidget {
+ const GalleryHome({super.key, required this.onToggleBrightness});
- // 3. Elevated LoadingButton
- _buildSectionHeader('3. Elevated LoadingButton'),
- LoadingButton(
- type: ButtonType.elevated,
- onPressed: () => _simulateLogin(),
- child: const Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(Icons.login),
- SizedBox(width: 8),
- Text('Login'),
- ],
- ),
- style: const LoadingButtonStyle(
- backgroundColor: Colors.blue,
- foregroundColor: Colors.white,
- borderRadius: 8,
- ),
- loadingText: 'Logging in...',
- successText: 'Welcome!',
- errorText: 'Login failed',
- onError: (error) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text('Login error: $error'),
- backgroundColor: Colors.red,
- ),
- );
- },
- ),
- const SizedBox(height: 30),
-
- // 4. Outlined LoadingButton
- _buildSectionHeader('4. Outlined LoadingButton'),
- LoadingButtonBuilder.outlined()
- .onPressed(() => _simulateDownload())
- .child(const Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(Icons.download),
- SizedBox(width: 8),
- Text('Download'),
- ],
- ))
- .style(const LoadingButtonStyle(
- borderColor: Colors.blue,
- borderWidth: 2,
- foregroundColor: Colors.blue,
- borderRadius: 25,
- ))
- .loadingText('Downloading...')
- .successText('Downloaded!')
- .errorText('Failed!')
- .build(),
- const SizedBox(height: 30),
+ /// Called with the currently resolved brightness when the toggle is tapped.
+ final void Function(Brightness current) onToggleBrightness;
- // 5. Text Button Style
- _buildSectionHeader('5. Text Button Style'),
- LoadingButtonBuilder.text()
- .onPressed(() => _simulateTextAction())
- .child(const Text('Text Button'))
- .style(const LoadingButtonStyle(
- foregroundColor: Colors.deepOrange,
- padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
- ))
- .loadingText('Loading...')
- .successText('Done!')
- .errorText('Error!')
- .build(),
- const SizedBox(height: 30),
-
- // 6. Icon Button Style
- _buildSectionHeader('6. Icon Button Style'),
- LoadingButton(
- type: ButtonType.icon,
- onPressed: () => _simulateIconAction(),
- child: const Icon(Icons.favorite),
- style: const LoadingButtonStyle(
- backgroundColor: Colors.red,
- foregroundColor: Colors.white,
- iconSize: 24,
- ),
- loadingWidget: const SizedBox(
- width: 24,
- height: 24,
- child: CircularProgressIndicator(
- strokeWidth: 2,
- valueColor: AlwaysStoppedAnimation(Colors.white),
- ),
- ),
- successWidget: const Icon(Icons.check, color: Colors.white),
- errorWidget: const Icon(Icons.error, color: Colors.white),
- onError: (error) {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(
- content: Text('Icon action failed'),
- backgroundColor: Colors.red,
- ),
- );
- },
- ),
- const SizedBox(height: 30),
+ @override
+ State createState() => _GalleryHomeState();
+}
- // 7. Custom Styled Button
- _buildSectionHeader('7. Custom Styled Button'),
- LoadingButton(
- type: ButtonType.filled,
- onPressed: () => _simulateCustomAction(),
- child: const Text('Custom Button'),
- style: LoadingButtonStyle(
- backgroundColor: Colors.deepPurple,
- foregroundColor: Colors.white,
- loadingBackgroundColor: Colors.deepPurple.shade300,
- successBackgroundColor: Colors.green,
- errorBackgroundColor: Colors.red,
- borderRadius: 30,
- elevation: 8,
- padding:
- const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
- ),
- loadingText: 'Processing...',
- successText: 'Success!',
- errorText: 'Try Again',
- width: double.infinity,
- height: 60,
- onStateChanged: (state) {
- print('Button state changed to: $state');
- },
- ),
- const SizedBox(height: 30),
+class _GalleryHomeState extends State {
+ int _index = 0;
+
+ void _select(int index) => setState(() => _index = index);
+
+ Widget _page(int index) {
+ switch (index) {
+ case 1:
+ return const LoadingButtonPage();
+ case 2:
+ return const AutoLoadingPage();
+ case 3:
+ return const ArgonPage();
+ case 4:
+ return const OrbsPage();
+ case 0:
+ default:
+ return OverviewPage(onNavigate: _select);
+ }
+ }
- // 8. Submit Form Button
- _buildSectionHeader('8. Submit Form Button'),
- LoadingButton(
- type: ButtonType.elevated,
- onPressed: () => _simulateFormSubmit(),
- child: const Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(Icons.send),
- SizedBox(width: 8),
- Text('Submit Form'),
- ],
- ),
- style: const LoadingButtonStyle(
- backgroundColor: Colors.teal,
- foregroundColor: Colors.white,
- borderRadius: 12,
- elevation: 4,
- ),
- loadingWidget: const Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- SizedBox(
- width: 16,
- height: 16,
- child: CircularProgressIndicator(
- strokeWidth: 2,
- valueColor: AlwaysStoppedAnimation(Colors.white),
+ @override
+ Widget build(BuildContext context) {
+ final Brightness brightness = Theme.of(context).brightness;
+ final bool isDark = brightness == Brightness.dark;
+
+ return LayoutBuilder(
+ builder: (BuildContext context, BoxConstraints constraints) {
+ final bool wide = constraints.maxWidth >= 880;
+
+ final Widget body = Row(
+ children: [
+ if (wide)
+ NavigationRail(
+ selectedIndex: _index,
+ onDestinationSelected: _select,
+ labelType: NavigationRailLabelType.all,
+ destinations: [
+ for (final _Destination d in _destinations)
+ NavigationRailDestination(
+ icon: Icon(d.icon),
+ selectedIcon: Icon(d.selectedIcon),
+ label: Text(d.label),
),
- ),
- SizedBox(width: 8),
- Text('Submitting...'),
- ],
- ),
- successWidget: const Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(Icons.check_circle, size: 18),
- SizedBox(width: 8),
- Text('Submitted!'),
- ],
- ),
- errorWidget: const Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(Icons.error, size: 18),
- SizedBox(width: 8),
- Text('Failed!'),
- ],
- ),
- ),
- const SizedBox(height: 30),
-
- // 9. Button with Progress Indicator Below (0% to 100%)
- _buildSectionHeader('9. Button with Progress Indicator Below'),
- LoadingButton(
- type: ButtonType.elevated,
- onPressed: () => _simulateUploadWithProgress(),
- child: const Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(Icons.cloud_upload_outlined),
- SizedBox(width: 8),
- Text('Upload with Progress'),
- ],
- ),
- style: const LoadingButtonStyle(
- backgroundColor: Colors.green,
- foregroundColor: Colors.white,
- borderRadius: 12,
- elevation: 4,
- ),
- loadingWidget: CircularProgressIndicator(
- value: _uploadProgress, // Progress from 0.0 to 1.0
- strokeWidth: 4,
- valueColor: const AlwaysStoppedAnimation(Colors.green),
- backgroundColor: Colors.grey.shade300,
- ),
- successWidget: const Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(Icons.check_circle, size: 18),
- SizedBox(width: 8),
- Text('Uploaded!'),
],
),
- errorWidget: const Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(Icons.error, size: 18),
- SizedBox(width: 8),
- Text('Failed!'),
- ],
+ if (wide) const VerticalDivider(width: 1),
+ Expanded(
+ // A key per index so switching pages rebuilds from scratch
+ // rather than reusing the previous page's element tree.
+ child: KeyedSubtree(
+ key: ValueKey(_index),
+ child: _page(_index),
),
),
- const SizedBox(height: 40),
],
- ),
- ),
- );
- }
-
- Widget _buildSectionHeader(String title) {
- return Padding(
- padding: const EdgeInsets.only(bottom: 12),
- child: Text(
- title,
- style: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.w600,
- color: Colors.grey[700],
- ),
- ),
+ );
+
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(_destinations[_index].label),
+ actions: [
+ IconButton(
+ key: const Key('theme-toggle'),
+ tooltip:
+ isDark ? 'Switch to light theme' : 'Switch to dark theme',
+ icon: Icon(isDark ? Icons.light_mode : Icons.dark_mode),
+ onPressed: () => widget.onToggleBrightness(brightness),
+ ),
+ const SizedBox(width: 4),
+ ],
+ ),
+ body: SafeArea(child: body),
+ bottomNavigationBar: wide
+ ? null
+ : NavigationBar(
+ selectedIndex: _index,
+ onDestinationSelected: _select,
+ destinations: [
+ for (final _Destination d in _destinations)
+ NavigationDestination(
+ icon: Icon(d.icon),
+ selectedIcon: Icon(d.selectedIcon),
+ label: d.label,
+ ),
+ ],
+ ),
+ );
+ },
);
}
-
- // Simulation methods
- Future _simulateLogin() async {
- await Future.delayed(const Duration(seconds: 2));
-
- // Simulate random success/failure
- if (DateTime.now().millisecondsSinceEpoch % 3 == 0) {
- throw Exception('Invalid credentials');
- }
- // Success is automatic if no exception is thrown
- }
-
- Future _simulateAutoOperation() async {
- await Future.delayed(const Duration(seconds: 2));
-
- // Simulate random success/failure
- if (DateTime.now().millisecondsSinceEpoch % 3 == 0) {
- throw Exception('Auto operation failed');
- }
- // Success is automatic
- }
-
- Future _simulateUpload() async {
- await Future.delayed(const Duration(seconds: 3));
-
- // Simulate random success/failure
- if (DateTime.now().millisecondsSinceEpoch % 5 == 0) {
- throw Exception('Upload failed');
- }
- // Success is automatic
- }
-
- Future _simulateDownload() async {
- await Future.delayed(const Duration(milliseconds: 2000));
-
- // Simulate random success/failure
- if (DateTime.now().millisecondsSinceEpoch % 4 == 0) {
- throw Exception('Download failed');
- }
- // Success is automatic
- }
-
- Future _simulateTextAction() async {
- await Future.delayed(const Duration(milliseconds: 1000));
-
- // Simulate random success/failure
- if (DateTime.now().millisecondsSinceEpoch % 3 == 0) {
- throw Exception('Text action failed');
- }
- }
-
- Future _simulateIconAction() async {
- await Future.delayed(const Duration(milliseconds: 800));
-
- // Simulate random success/failure
- if (DateTime.now().millisecondsSinceEpoch % 4 == 0) {
- throw Exception('Icon action failed');
- }
- }
-
- Future _simulateCustomAction() async {
- await Future.delayed(const Duration(milliseconds: 1500));
-
- // Simulate random success/failure
- if (DateTime.now().millisecondsSinceEpoch % 3 == 0) {
- throw Exception('Custom action failed');
- }
- }
-
- Future _simulateFormSubmit() async {
- await Future.delayed(const Duration(seconds: 2));
-
- // Simulate random success/failure
- if (DateTime.now().millisecondsSinceEpoch % 4 == 0) {
- throw Exception('Form submission failed');
- }
- }
-
- Future _simulateUploadWithProgress() async {
- setState(() {
- _isUploading = true;
- _uploadProgress = 0.0;
- });
-
- try {
- // Simulate progress from 0% to 100% over 5 seconds
- const totalDuration = Duration(seconds: 5);
- const steps = 20; // Number of progress updates
- final stepDuration = totalDuration.inMilliseconds ~/ steps;
-
- for (int i = 1; i <= steps; i++) {
- await Future.delayed(Duration(milliseconds: stepDuration));
- setState(() {
- _uploadProgress = i / steps;
- });
- }
-
- // Simulate random failure after progress
- if (DateTime.now().millisecondsSinceEpoch % 3 == 0) {
- throw Exception('Upload failed after progress');
- }
- // Success is automatic if no exception
- } catch (e) {
- // The button will handle the error state automatically
- rethrow; // Rethrow to let the button catch it
- } finally {
- // Reset progress after completion (success or error)
- await Future.delayed(
- const Duration(seconds: 2)); // Wait for success/error display
- setState(() {
- _isUploading = false;
- _uploadProgress = 0.0;
- });
- }
- }
}
diff --git a/example/lib/pages/argon_page.dart b/example/lib/pages/argon_page.dart
new file mode 100644
index 0000000..918169d
--- /dev/null
+++ b/example/lib/pages/argon_page.dart
@@ -0,0 +1,315 @@
+import 'package:flutter/material.dart';
+import 'package:loading_icon_button/loading_icon_button.dart';
+
+import '../gallery.dart';
+
+/// [ArgonButton] collapses into its own loader; [ArgonTimerButton] does the
+/// same and counts down before it comes back.
+class ArgonPage extends StatefulWidget {
+ const ArgonPage({super.key});
+
+ @override
+ State createState() => _ArgonPageState();
+}
+
+class _ArgonPageState extends State {
+ bool _roundLoadingShape = true;
+ double _borderRadius = 24;
+
+ @override
+ Widget build(BuildContext context) {
+ return GalleryPage(
+ title: 'Argon buttons',
+ blurb: 'A width animation rather than a content swap: the button shrinks '
+ 'to a circle around its loader, then springs back.',
+ children: [
+ _basics(),
+ _shapeControls(),
+ _loaders(),
+ _timer(),
+ ],
+ );
+ }
+
+ ColorScheme get _scheme => Theme.of(context).colorScheme;
+
+ TextStyle get _labelStyle => TextStyle(
+ color: _scheme.onPrimary,
+ fontWeight: FontWeight.w600,
+ );
+
+ // ---------------------------------------------------------------- basics
+
+ Widget _basics() => GallerySection(
+ title: 'The shape of it',
+ subtitle: 'onTap hands you startLoading, stopLoading and the current '
+ 'state. Call startLoading, do the work, call stopLoading — both '
+ 'are safe to call after the button is gone.',
+ code: 'onTap: (startLoading, stopLoading, state) async { '
+ 'startLoading(); await save(); stopLoading(); }',
+ child: DemoWrap(
+ children: [
+ ArgonButton(
+ key: const Key('argon-basic'),
+ height: 50,
+ width: 220,
+ minWidth: 50,
+ borderRadius: 25,
+ color: _scheme.primary,
+ loader: Padding(
+ padding: const EdgeInsets.all(10),
+ child: CircularProgressIndicator(
+ strokeWidth: 2.5,
+ color: _scheme.onPrimary,
+ ),
+ ),
+ onTap: _run,
+ child: Text('Sign in', style: _labelStyle),
+ ),
+ ArgonButton(
+ height: 50,
+ width: 220,
+ minWidth: 50,
+ borderRadius: 12,
+ color: _scheme.tertiary,
+ elevation: 0,
+ borderSide: BorderSide(color: _scheme.outline),
+ loader: Padding(
+ padding: const EdgeInsets.all(10),
+ child: ThinkingOrb(
+ state: OrbState.weaving,
+ size: 30,
+ color: _scheme.onTertiary,
+ ),
+ ),
+ onTap: _run,
+ child: Text(
+ 'Orb loader',
+ style: TextStyle(
+ color: _scheme.onTertiary,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ArgonButton(
+ height: 50,
+ width: 220,
+ minWidth: 50,
+ borderRadius: 25,
+ disabledColor: _scheme.surfaceContainerHighest,
+ disabledTextColor: _scheme.onSurfaceVariant,
+ // A null onTap disables the button. Before 1.1.0 this threw.
+ onTap: null,
+ child: Text(
+ 'Disabled (onTap: null)',
+ style: TextStyle(color: _scheme.onSurfaceVariant),
+ ),
+ ),
+ ],
+ ),
+ );
+
+ Future _run(
+ Function startLoading,
+ Function stopLoading,
+ ArgonButtonState state,
+ ) async {
+ if (state != ArgonButtonState.idle) return;
+ startLoading();
+ await fakeWork(const Duration(milliseconds: 1600));
+ stopLoading();
+ }
+
+ // ---------------------------------------------------------------- shapes
+
+ Widget _shapeControls() => GallerySection(
+ title: 'Collapsed shape',
+ subtitle: 'roundLoadingShape animates the corner radius to a full '
+ 'circle while loading. Turn it off to keep the resting radius.',
+ code: 'ArgonButton(roundLoadingShape: false, borderRadius: 8)',
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Switch(
+ key: const Key('round-shape-switch'),
+ value: _roundLoadingShape,
+ onChanged: (bool value) =>
+ setState(() => _roundLoadingShape = value),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Text('roundLoadingShape: $_roundLoadingShape'),
+ ),
+ ],
+ ),
+ Row(
+ children: [
+ const Text('borderRadius'),
+ Expanded(
+ child: Slider(
+ value: _borderRadius,
+ max: 25,
+ divisions: 25,
+ label: _borderRadius.round().toString(),
+ onChanged: (double value) =>
+ setState(() => _borderRadius = value),
+ ),
+ ),
+ Text(_borderRadius.round().toString()),
+ ],
+ ),
+ const SizedBox(height: 8),
+ ArgonButton(
+ height: 56,
+ width: 260,
+ minWidth: 56,
+ borderRadius: _borderRadius,
+ roundLoadingShape: _roundLoadingShape,
+ color: _scheme.primary,
+ loader: Padding(
+ padding: const EdgeInsets.all(12),
+ child: CircularProgressIndicator(
+ strokeWidth: 2.5,
+ color: _scheme.onPrimary,
+ ),
+ ),
+ onTap: _run,
+ child: Text('Watch the corners', style: _labelStyle),
+ ),
+ ],
+ ),
+ );
+
+ // --------------------------------------------------------------- loaders
+
+ Widget _loaders() => GallerySection(
+ title: 'Timing and easing',
+ subtitle: 'animationDuration, curve and reverseCurve control the '
+ 'collapse; minWidth is how narrow it gets.',
+ code: 'ArgonButton(animationDuration: Duration(milliseconds: 900), '
+ 'curve: Curves.elasticOut)',
+ child: DemoWrap(
+ children: [
+ ArgonButton(
+ height: 50,
+ width: 200,
+ minWidth: 50,
+ borderRadius: 25,
+ animationDuration: const Duration(milliseconds: 900),
+ curve: Curves.easeOutBack,
+ reverseCurve: Curves.easeInBack,
+ color: _scheme.secondary,
+ loader: Padding(
+ padding: const EdgeInsets.all(10),
+ child: CircularProgressIndicator(
+ strokeWidth: 2.5,
+ color: _scheme.onSecondary,
+ ),
+ ),
+ onTap: _run,
+ child: Text(
+ 'Springy',
+ style: TextStyle(
+ color: _scheme.onSecondary,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ArgonButton(
+ height: 50,
+ width: 200,
+ minWidth: 160,
+ borderRadius: 25,
+ color: _scheme.primary,
+ loader: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ThinkingOrb(
+ state: OrbState.listening,
+ size: 24,
+ color: _scheme.onPrimary,
+ ),
+ const SizedBox(width: 8),
+ Text('Wait…', style: _labelStyle),
+ ],
+ ),
+ onTap: _run,
+ child: Text('Wide loader', style: _labelStyle),
+ ),
+ ],
+ ),
+ );
+
+ // ----------------------------------------------------------------- timer
+
+ Widget _timer() => GallerySection(
+ title: 'ArgonTimerButton',
+ subtitle: 'The countdown variant. startTimer(seconds) collapses the '
+ 'button and rebuilds the loader once a second with the seconds '
+ 'left; it throws an ArgumentError on a non-positive duration.',
+ code: 'onTap: (startTimer, state) => startTimer(10)',
+ child: DemoWrap(
+ children: [
+ ArgonTimerButton(
+ key: const Key('argon-timer'),
+ height: 50,
+ width: 220,
+ minWidth: 50,
+ borderRadius: 25,
+ color: _scheme.primary,
+ loader: (int seconds) => Text(
+ 'Resend in $seconds',
+ style: _labelStyle,
+ ),
+ onTap: (Function startTimer, ArgonButtonState? state) {
+ if (state == ArgonButtonState.idle) {
+ startTimer(10);
+ }
+ },
+ child: Text('Send code', style: _labelStyle),
+ ),
+ ArgonTimerButton(
+ height: 50,
+ width: 220,
+ minWidth: 160,
+ borderRadius: 12,
+ // Starts counting down the moment it is mounted.
+ initialTimer: 5,
+ color: _scheme.tertiary,
+ loader: (int seconds) => Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ThinkingOrb(
+ state: OrbState.breathing,
+ size: 22,
+ color: _scheme.onTertiary,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ '${seconds}s left',
+ style: TextStyle(
+ color: _scheme.onTertiary,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ],
+ ),
+ onTap: (Function startTimer, ArgonButtonState? state) {
+ if (state == ArgonButtonState.idle) {
+ startTimer(5);
+ }
+ },
+ child: Text(
+ 'initialTimer: 5',
+ style: TextStyle(
+ color: _scheme.onTertiary,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+}
diff --git a/example/lib/pages/auto_loading_page.dart b/example/lib/pages/auto_loading_page.dart
new file mode 100644
index 0000000..08a4982
--- /dev/null
+++ b/example/lib/pages/auto_loading_page.dart
@@ -0,0 +1,384 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:loading_icon_button/loading_icon_button.dart';
+
+import '../gallery.dart';
+
+/// The two wrapper families over the stock Material buttons:
+///
+/// * `XxxAutoLoadingButton` — busy for exactly as long as the async callback
+/// runs; you never touch a flag.
+/// * `XxxLoadingButton` — busy while the `isLoading` flag you pass is true.
+class AutoLoadingPage extends StatefulWidget {
+ const AutoLoadingPage({super.key});
+
+ @override
+ State createState() => _AutoLoadingPageState();
+}
+
+class _AutoLoadingPageState extends State {
+ /// Reaches an auto-loading button's state without a tap.
+ final GlobalKey>
+ _programmaticKey =
+ GlobalKey>();
+
+ final WidgetStatesController _statesController = WidgetStatesController();
+
+ bool _isLoading = false;
+ bool _bookmarked = false;
+
+ @override
+ void dispose() {
+ _statesController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return GalleryPage(
+ title: 'Auto-loading and flag-driven buttons',
+ blurb: 'Drop-in replacements for ElevatedButton, FilledButton, '
+ 'OutlinedButton, TextButton and IconButton that know how to be busy.',
+ children: [
+ _autoSection(),
+ _labelsSection(),
+ _iconSection(),
+ _programmaticSection(),
+ _flagSection(),
+ ],
+ );
+ }
+
+ // ------------------------------------------------------------------ auto
+
+ Widget _autoSection() => GallerySection(
+ title: 'Driven by the callback',
+ subtitle: 'The button enters the loading state on tap and leaves it '
+ 'when the returned future settles — including when it throws, '
+ 'which is the bug this family used to have.',
+ code: 'ElevatedAutoLoadingButton(onPressed: () async => api.save(), '
+ "child: Text('Save'))",
+ child: DemoWrap(
+ children: [
+ ElevatedAutoLoadingButton(
+ key: const Key('auto-elevated'),
+ onPressed: fakeWork,
+ child: const Text('Elevated'),
+ ),
+ ElevatedAutoLoadingButton.icon(
+ onPressed: fakeWork,
+ icon: const Icon(Icons.save_outlined),
+ label: const Text('Elevated icon'),
+ ),
+ FilledAutoLoadingButton(
+ onPressed: fakeWork,
+ child: const Text('Filled'),
+ ),
+ FilledAutoLoadingButton.icon(
+ onPressed: fakeWork,
+ icon: const Icon(Icons.send_outlined),
+ label: const Text('Filled icon'),
+ ),
+ FilledAutoLoadingButton.tonal(
+ onPressed: fakeWork,
+ child: const Text('Tonal'),
+ ),
+ FilledAutoLoadingButton.tonalIcon(
+ onPressed: fakeWork,
+ icon: const Icon(Icons.download_outlined),
+ label: const Text('Tonal icon'),
+ ),
+ OutlinedAutoLoadingButton(
+ onPressed: fakeWork,
+ child: const Text('Outlined'),
+ ),
+ OutlinedAutoLoadingButton.icon(
+ onPressed: fakeWork,
+ icon: const Icon(Icons.tune),
+ label: const Text('Outlined icon'),
+ ),
+ TextAutoLoadingButton(
+ onPressed: fakeWork,
+ child: const Text('Text'),
+ ),
+ TextAutoLoadingButton.icon(
+ onPressed: fakeWork,
+ icon: const Icon(Icons.link),
+ label: const Text('Text icon'),
+ ),
+ ElevatedAutoLoadingButton(
+ key: const Key('auto-throws'),
+ onPressed: fakeFailure,
+ child: const Text('Throws (recovers)'),
+ ),
+ const ElevatedAutoLoadingButton(
+ onPressed: null,
+ child: Text('Disabled'),
+ ),
+ ],
+ ),
+ );
+
+ // ---------------------------------------------------------------- labels
+
+ Widget _labelsSection() => GallerySection(
+ title: 'What the busy state looks like',
+ subtitle: 'By default the label is replaced by an indicator. Give it a '
+ 'loadingLabel to keep text beside the indicator, an indicator to '
+ 'choose what spins, or a loadingIcon to replace it outright.',
+ code: 'FilledAutoLoadingButton(indicator: LoadingIndicator.orb(), '
+ "loadingLabel: Text('Saving…'))",
+ child: DemoWrap(
+ children: [
+ FilledAutoLoadingButton(
+ onPressed: fakeWork,
+ loadingLabel: const Text('Saving…'),
+ child: const Text('With a label'),
+ ),
+ FilledAutoLoadingButton(
+ onPressed: fakeWork,
+ indicator: const LoadingIndicator.orb(
+ state: OrbState.searching,
+ size: 22,
+ ),
+ loadingLabel: const Text('Searching…'),
+ child: const Text('With an orb'),
+ ),
+ OutlinedAutoLoadingButton(
+ onPressed: fakeWork,
+ loadingIcon: const Icon(Icons.hourglass_top, size: 20),
+ child: const Text('With an icon'),
+ ),
+ FilledAutoLoadingButton(
+ onPressed: fakeWork,
+ switchDuration: const Duration(milliseconds: 600),
+ loadingLabel: const Text('Slowly…'),
+ child: const Text('Slow size switch'),
+ ),
+ ElevatedAutoLoadingButton(
+ onPressed: fakeWork,
+ onLongPress: () => fakeWork(const Duration(seconds: 2)),
+ statesController: _statesController,
+ onHover: (bool hovering) {},
+ onFocusChange: (bool focused) {},
+ child: const Text('Long-press me too'),
+ ),
+ ],
+ ),
+ );
+
+ // ----------------------------------------------------------------- icons
+
+ Widget _iconSection() => GallerySection(
+ title: 'IconAutoLoadingButton',
+ subtitle: 'All four Material 3 icon-button variants, plus the '
+ 'selected/unselected pair.',
+ code: 'IconAutoLoadingButton.filledTonal(onPressed: refresh, '
+ 'icon: Icon(Icons.refresh))',
+ child: DemoWrap(
+ children: [
+ LabelledDemo(
+ label: 'standard',
+ width: 110,
+ child: IconAutoLoadingButton(
+ onPressed: fakeWork,
+ tooltip: 'Refresh',
+ icon: const Icon(Icons.refresh),
+ ),
+ ),
+ LabelledDemo(
+ label: 'filled',
+ width: 110,
+ child: IconAutoLoadingButton.filled(
+ onPressed: fakeWork,
+ icon: const Icon(Icons.cloud_upload_outlined),
+ ),
+ ),
+ LabelledDemo(
+ label: 'filledTonal',
+ width: 110,
+ child: IconAutoLoadingButton.filledTonal(
+ onPressed: fakeWork,
+ indicator: const LoadingIndicator.orb(
+ state: OrbState.breathing,
+ size: 20,
+ ),
+ icon: const Icon(Icons.auto_awesome_outlined),
+ ),
+ ),
+ LabelledDemo(
+ label: 'outlined',
+ width: 110,
+ child: IconAutoLoadingButton.outlined(
+ onPressed: fakeWork,
+ icon: const Icon(Icons.delete_outline),
+ ),
+ ),
+ LabelledDemo(
+ label: 'selectable',
+ width: 130,
+ child: IconAutoLoadingButton.filledTonal(
+ isSelected: _bookmarked,
+ selectedIcon: const Icon(Icons.bookmark),
+ selectedLoadingIcon: const Icon(Icons.bookmark_border),
+ icon: const Icon(Icons.bookmark_outline),
+ onPressed: () async {
+ await fakeWork(const Duration(milliseconds: 600));
+ if (mounted) setState(() => _bookmarked = !_bookmarked);
+ },
+ ),
+ ),
+ const LabelledDemo(
+ label: 'disabled',
+ width: 110,
+ child: IconAutoLoadingButton(
+ onPressed: null,
+ icon: Icon(Icons.block),
+ ),
+ ),
+ ],
+ ),
+ );
+
+ // ---------------------------------------------------------- programmatic
+
+ Widget _programmaticSection() => GallerySection(
+ title: 'Pressing one from code',
+ subtitle: 'AutoLoadingButtonState is public. A GlobalKey onto it gives '
+ 'you doPress() and doLongPress(), each returning the future of the '
+ 'run — so a failure comes back to you instead of going unhandled.',
+ code: 'GlobalKey>()'
+ '.currentState?.doPress()',
+ child: DemoWrap(
+ children: [
+ ElevatedAutoLoadingButton(
+ key: _programmaticKey,
+ onPressed: fakeWork,
+ onLongPress: () => fakeWork(const Duration(seconds: 2)),
+ child: const Text('Target'),
+ ),
+ OutlinedButton(
+ onPressed: () => unawaited(_press(longPress: false)),
+ child: const Text('doPress()'),
+ ),
+ OutlinedButton(
+ onPressed: () => unawaited(_press(longPress: true)),
+ child: const Text('doLongPress()'),
+ ),
+ ],
+ ),
+ );
+
+ Future _press({required bool longPress}) async {
+ final AutoLoadingButtonState? state =
+ _programmaticKey.currentState;
+ if (state == null) return;
+ try {
+ await (longPress ? state.doLongPress() : state.doPress());
+ if (mounted) showNote(context, 'Run finished');
+ } catch (error) {
+ if (mounted) showNote(context, 'Run failed: $error');
+ }
+ }
+
+ // ------------------------------------------------------------------ flag
+
+ Widget _flagSection() => GallerySection(
+ title: 'Driven by a flag',
+ subtitle: 'The same widgets with an isLoading bool instead of a '
+ 'callback, for when the busy state lives in your own state '
+ 'management. loadingClickable keeps the button tappable while busy.',
+ code: 'FilledLoadingButton(isLoading: state.saving, '
+ 'onPressed: save, child: Text(\'Save\'))',
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Switch(
+ key: const Key('is-loading-switch'),
+ value: _isLoading,
+ onChanged: (bool value) => setState(() => _isLoading = value),
+ ),
+ const SizedBox(width: 12),
+ Expanded(child: Text('isLoading: $_isLoading')),
+ ],
+ ),
+ const SizedBox(height: 16),
+ DemoWrap(
+ key: const Key('flag-driven-demos'),
+ children: [
+ ElevatedLoadingButton(
+ isLoading: _isLoading,
+ onPressed: () {},
+ child: const Text('Elevated'),
+ ),
+ ElevatedLoadingButton.icon(
+ isLoading: _isLoading,
+ onPressed: () {},
+ icon: const Icon(Icons.save_outlined),
+ label: const Text('Elevated icon'),
+ ),
+ FilledLoadingButton(
+ isLoading: _isLoading,
+ onPressed: () {},
+ loadingLabel: const Text('Saving…'),
+ child: const Text('Filled'),
+ ),
+ FilledLoadingButton.icon(
+ isLoading: _isLoading,
+ onPressed: () {},
+ icon: const Icon(Icons.send_outlined),
+ label: const Text('Filled icon'),
+ ),
+ FilledLoadingButton.tonal(
+ isLoading: _isLoading,
+ onPressed: () {},
+ indicator: const LoadingIndicator.orb(
+ state: OrbState.working,
+ size: 20,
+ ),
+ child: const Text('Tonal'),
+ ),
+ FilledLoadingButton.tonalIcon(
+ isLoading: _isLoading,
+ onPressed: () {},
+ icon: const Icon(Icons.download_outlined),
+ label: const Text('Tonal icon'),
+ ),
+ OutlinedLoadingButton(
+ isLoading: _isLoading,
+ onPressed: () {},
+ child: const Text('Outlined'),
+ ),
+ OutlinedLoadingButton.icon(
+ isLoading: _isLoading,
+ onPressed: () {},
+ icon: const Icon(Icons.tune),
+ label: const Text('Outlined icon'),
+ ),
+ TextLoadingButton(
+ isLoading: _isLoading,
+ onPressed: () {},
+ child: const Text('Text'),
+ ),
+ TextLoadingButton.icon(
+ isLoading: _isLoading,
+ onPressed: () {},
+ icon: const Icon(Icons.link),
+ label: const Text('Text icon'),
+ ),
+ FilledLoadingButton(
+ isLoading: _isLoading,
+ loadingClickable: true,
+ onPressed: () => showNote(context, 'Tapped while busy'),
+ loadingLabel: const Text('Still tappable'),
+ child: const Text('loadingClickable'),
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+}
diff --git a/example/lib/pages/loading_button_page.dart b/example/lib/pages/loading_button_page.dart
new file mode 100644
index 0000000..cbcccbe
--- /dev/null
+++ b/example/lib/pages/loading_button_page.dart
@@ -0,0 +1,880 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:loading_icon_button/loading_icon_button.dart';
+
+import '../gallery.dart';
+
+/// Everything [LoadingButton] can do: its five shapes, its state machine, the
+/// controller, determinate progress, sizing, colours, indicators and the
+/// interaction guards.
+class LoadingButtonPage extends StatefulWidget {
+ const LoadingButtonPage({super.key});
+
+ @override
+ State createState() => _LoadingButtonPageState();
+}
+
+class _LoadingButtonPageState extends State {
+ /// Drives two buttons at once, from outside the widget tree.
+ final LoadingButtonController _controller = LoadingButtonController();
+
+ /// Held in the loading state so determinate progress stays on screen.
+ final LoadingButtonController _scrub = LoadingButtonController(
+ value: const LoadingButtonValue(
+ state: ActionState.loading,
+ progress: 0.35,
+ ),
+ );
+
+ /// Driven by a real (well, fake) upload loop.
+ final LoadingButtonController _upload = LoadingButtonController();
+
+ /// Pinned to one state each, so all three colours are visible at once.
+ final LoadingButtonController _pinLoading = LoadingButtonController(
+ value: const LoadingButtonValue(state: ActionState.loading),
+ );
+ final LoadingButtonController _pinSuccess = LoadingButtonController(
+ value: const LoadingButtonValue(state: ActionState.success),
+ );
+ final LoadingButtonController _pinError = LoadingButtonController(
+ value: const LoadingButtonValue(state: ActionState.error),
+ );
+
+ /// Pinned loading, one per indicator flavour.
+ final List _pinIndicators =
+ List.generate(
+ 3,
+ (int _) => LoadingButtonController(
+ value: const LoadingButtonValue(state: ActionState.loading),
+ ),
+ );
+
+ /// Pinned loading with progress, to show a styled progress fill at rest.
+ final LoadingButtonController _pinStyled = LoadingButtonController(
+ value: const LoadingButtonValue(
+ state: ActionState.loading,
+ progress: 0.45,
+ ),
+ );
+
+ /// Pinned loading *with* progress, so the builder indicator receives a value.
+ final LoadingButtonController _pinBuilder = LoadingButtonController(
+ value: const LoadingButtonValue(
+ state: ActionState.loading,
+ progress: 0.62,
+ ),
+ );
+
+ /// Reaches a button's [LoadingButtonState] without a controller.
+ final GlobalKey _stateKey =
+ GlobalKey();
+
+ final FocusNode _focusNode = FocusNode(debugLabel: 'loading-button-demo');
+
+ double _progress = 0.35;
+ LoadingProgressStyle _progressStyle = LoadingProgressStyle.both;
+ int _sizingIndex = 1;
+ int _colorIndex = 1;
+ bool _enabled = true;
+ int _debouncedTaps = 0;
+ ActionState _lastObservedState = ActionState.idle;
+
+ @override
+ void dispose() {
+ for (final LoadingButtonController c in [
+ _controller,
+ _scrub,
+ _upload,
+ _pinLoading,
+ _pinSuccess,
+ _pinError,
+ _pinStyled,
+ _pinBuilder,
+ ..._pinIndicators,
+ ]) {
+ c.dispose();
+ }
+ _focusNode.dispose();
+ super.dispose();
+ }
+
+ Future _runUpload() async {
+ for (int step = 1; step <= 20; step++) {
+ await Future.delayed(const Duration(milliseconds: 80));
+ if (!mounted) return;
+ _upload.setProgress(step / 20);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return GalleryPage(
+ title: 'LoadingButton',
+ blurb: 'One widget, one state machine: idle → loading → success or '
+ 'error → idle. Press it, drive it from a controller, or both.',
+ children: [
+ _shapes(),
+ _outcomes(),
+ _controllerSection(),
+ _progressSection(),
+ _sizingSection(),
+ _coloursSection(),
+ _styleSection(),
+ _indicatorSection(),
+ _guardsSection(),
+ _stateKeySection(),
+ _builderSection(),
+ _subtreeThemeSection(),
+ ],
+ );
+ }
+
+ // ---------------------------------------------------------------- shapes
+
+ Widget _shapes() => GallerySection(
+ title: 'Five shapes',
+ subtitle: 'ButtonType picks which Material button is rendered. The '
+ 'state machine is identical in all five.',
+ code: 'LoadingButton(type: ButtonType.filled, onPressed: submit, '
+ "child: Text('Submit'))",
+ child: DemoWrap(
+ children: [
+ for (final ButtonType type in ButtonType.values)
+ LoadingButton(
+ key: Key('shape-${type.name}'),
+ type: type,
+ onPressed: fakeWork,
+ tooltip: 'ButtonType.${type.name}',
+ child: type == ButtonType.icon
+ ? const Icon(Icons.favorite_border)
+ : Text(_titleCase(type.name)),
+ ),
+ ],
+ ),
+ );
+
+ // -------------------------------------------------------------- outcomes
+
+ Widget _outcomes() => GallerySection(
+ title: 'Success, failure and custom content',
+ subtitle: 'A throwing callback lands in the error state and calls '
+ 'onFailure with the error and its stack trace. Each phase can '
+ 'show a label or a widget of your own.',
+ code:
+ 'onFailure: (Object error, StackTrace stack) => report(error, stack)',
+ child: DemoWrap(
+ children: [
+ LoadingButton(
+ key: const Key('outcome-success'),
+ type: ButtonType.filled,
+ onPressed: fakeWork,
+ loadingText: 'Sending…',
+ successText: 'Sent',
+ onSuccess: () => showNote(context, 'onSuccess fired'),
+ child: const Text('Succeeds'),
+ ),
+ LoadingButton(
+ key: const Key('outcome-failure'),
+ type: ButtonType.filled,
+ onPressed: fakeFailure,
+ errorText: 'Rejected',
+ onFailure: (Object error, StackTrace stackTrace) =>
+ showNote(context, 'onFailure: $error'),
+ child: const Text('Fails'),
+ ),
+ LoadingButton(
+ type: ButtonType.outlined,
+ onPressed: fakeWork,
+ loadingWidget: const SizedBox.square(
+ dimension: 22,
+ child: ThinkingOrb(state: OrbState.listening, size: 22),
+ ),
+ successWidget: const Icon(Icons.thumb_up_outlined, size: 20),
+ errorWidget: const Icon(Icons.thumb_down_outlined, size: 20),
+ child: const Text('Custom widgets'),
+ ),
+ LoadingButton(
+ type: ButtonType.text,
+ onPressed: fakeWork,
+ resetAfterDuration: false,
+ successText: 'Stays done',
+ child: const Text('No auto-reset'),
+ ),
+ ],
+ ),
+ );
+
+ // ------------------------------------------------------------ controller
+
+ Widget _controllerSection() => GallerySection(
+ title: 'LoadingButtonController',
+ subtitle: 'A ValueNotifier. Attach it to as many '
+ 'buttons as you like — they all mirror the same value, and '
+ 'press() runs every attached callback.',
+ code: 'LoadingButton(controller: controller, onPressed: upload, '
+ "child: Text('Upload'))",
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ ValueListenableBuilder(
+ valueListenable: _controller,
+ builder: (BuildContext context, LoadingButtonValue value, _) {
+ return Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ Chip(
+ key: const Key('controller-state'),
+ label: Text('state: ${value.state.name}'),
+ ),
+ Chip(
+ label: Text(
+ value.isDeterminate
+ ? 'progress: ${(value.progress! * 100).round()}%'
+ : 'progress: indeterminate',
+ ),
+ ),
+ Chip(
+ label: Text('attached to ${_controller.attachmentCount}'),
+ ),
+ // ActionStateExtension: isIdle, isLoading, isSuccess,
+ // isError, isDisabled and isInteractive.
+ Chip(
+ label: Text(
+ value.state.isInteractive
+ ? 'a press would be accepted'
+ : 'presses are ignored',
+ ),
+ ),
+ if (value.error != null)
+ Chip(label: Text('error: ${value.error}')),
+ ],
+ );
+ },
+ ),
+ const SizedBox(height: 16),
+ DemoWrap(
+ children: [
+ LoadingButton(
+ key: const Key('controller-primary'),
+ type: ButtonType.filled,
+ controller: _controller,
+ onPressed: fakeWork,
+ child: const Text('Primary'),
+ ),
+ LoadingButton(
+ type: ButtonType.outlined,
+ controller: _controller,
+ onPressed: fakeWork,
+ child: const Text('Mirror'),
+ ),
+ ],
+ ),
+ const Divider(height: 32),
+ Text(
+ 'Drive it from here',
+ style: Theme.of(context).textTheme.labelLarge,
+ ),
+ const SizedBox(height: 10),
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ _cmd('start()', () => _controller.start()),
+ _cmd('setProgress(0.5)', () => _controller.setProgress(0.5)),
+ _cmd('setProgress(null)', () => _controller.setProgress(null)),
+ _cmd('success()', _controller.success),
+ _cmd('error(…)', () => _controller.error('Disk full')),
+ _cmd('reset()', _controller.reset),
+ _cmd(
+ 'setEnabled(false)',
+ () => _controller.setEnabled(false),
+ ),
+ _cmd('setEnabled(true)', () => _controller.setEnabled(true)),
+ _cmd(
+ 'setActionState(disabled)',
+ () => _controller.setActionState(ActionState.disabled),
+ ),
+ _cmd(
+ 'startCooldown(3s)',
+ () => _controller.startCooldown(const Duration(seconds: 3)),
+ ),
+ _cmd('press()', () => unawaited(_controller.press())),
+ ],
+ ),
+ ],
+ ),
+ );
+
+ Widget _cmd(String label, VoidCallback onPressed) =>
+ OutlinedButton(onPressed: onPressed, child: Text(label));
+
+ // -------------------------------------------------------------- progress
+
+ Widget _progressSection() => GallerySection(
+ title: 'Determinate progress',
+ subtitle: 'Report 0.0–1.0 and choose how it is drawn: on the '
+ 'indicator, as a fill clipped to the button shape, or both. The '
+ 'button below is pinned to the loading state so you can scrub it.',
+ code: 'LoadingButton(progressStyle: LoadingProgressStyle.both, '
+ 'controller: controller)',
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: SegmentedButton(
+ segments: const >[
+ ButtonSegment(
+ value: LoadingProgressStyle.indicator,
+ label: Text('indicator'),
+ ),
+ ButtonSegment(
+ value: LoadingProgressStyle.fill,
+ label: Text('fill'),
+ ),
+ ButtonSegment(
+ value: LoadingProgressStyle.both,
+ label: Text('both'),
+ ),
+ ],
+ selected: {_progressStyle},
+ onSelectionChanged: (Set selection) =>
+ setState(() => _progressStyle = selection.first),
+ ),
+ ),
+ Slider(
+ value: _progress,
+ label: '${(_progress * 100).round()}%',
+ divisions: 20,
+ onChanged: (double value) {
+ setState(() => _progress = value);
+ _scrub.setProgress(value);
+ },
+ ),
+ LoadingButton(
+ type: ButtonType.filled,
+ controller: _scrub,
+ resetAfterDuration: false,
+ progressStyle: _progressStyle,
+ sizing: const LoadingButtonSizing.expand(height: 52),
+ child: const Text('Pinned to loading'),
+ ),
+ const SizedBox(height: 20),
+ Text(
+ 'And the same thing driven by a real callback:',
+ style: Theme.of(context).textTheme.labelLarge,
+ ),
+ const SizedBox(height: 10),
+ LoadingButton(
+ type: ButtonType.elevated,
+ controller: _upload,
+ onPressed: _runUpload,
+ progressStyle: _progressStyle,
+ successText: 'Uploaded',
+ sizing: const LoadingButtonSizing.expand(height: 52),
+ child: const Text('Upload 20 chunks'),
+ ),
+ ],
+ ),
+ );
+
+ // ---------------------------------------------------------------- sizing
+
+ Widget _sizingSection() {
+ final LoadingButtonSizing sizing = switch (_sizingIndex) {
+ 1 => const LoadingButtonSizing.intrinsic(),
+ 2 => const LoadingButtonSizing.expand(height: 52),
+ 3 => const LoadingButtonSizing.fixed(width: 260, height: 64),
+ _ => LoadingButtonSizing.legacy,
+ };
+
+ return GallerySection(
+ title: 'Sizing',
+ subtitle: 'legacy is a fixed 200x50 box (240 wide above a 600px window) '
+ 'and is still the package default. The other three are new in '
+ '1.1.0. The dashed frame below is 340px wide.',
+ code: 'LoadingButton(sizing: LoadingButtonSizing.intrinsic())',
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: SegmentedButton(
+ segments: const >[
+ ButtonSegment(value: 0, label: Text('legacy')),
+ ButtonSegment(value: 1, label: Text('intrinsic')),
+ ButtonSegment(value: 2, label: Text('expand')),
+ ButtonSegment(value: 3, label: Text('fixed')),
+ ],
+ selected: {_sizingIndex},
+ onSelectionChanged: (Set selection) =>
+ setState(() => _sizingIndex = selection.first),
+ ),
+ ),
+ const SizedBox(height: 16),
+ Container(
+ width: 340,
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ border: Border.all(color: Theme.of(context).colorScheme.outline),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: LoadingButton(
+ type: ButtonType.filled,
+ sizing: sizing,
+ onPressed: fakeWork,
+ successText: 'Done',
+ child: const Text('Resize me'),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ // --------------------------------------------------------------- colours
+
+ Widget _coloursSection() {
+ final ThemeData theme = Theme.of(context);
+ final ColorScheme scheme = theme.colorScheme;
+
+ final LoadingButtonColorStrategy strategy = _colorIndex == 0
+ ? LoadingButtonColorStrategy.legacy
+ : LoadingButtonColorStrategy.material3;
+
+ final LoadingButtonColors? colors = switch (_colorIndex) {
+ 2 => LoadingButtonColors.traffic(theme.brightness),
+ // fromScheme gives you contrast-safe role pairs; copyWith adjusts the
+ // ones you care about without restating the rest.
+ 3 => LoadingButtonColors.fromScheme(scheme).copyWith(
+ loading: scheme.secondaryContainer,
+ onLoading: scheme.onSecondaryContainer,
+ success: const Color(0xFF0F766E),
+ onSuccess: Colors.white,
+ ),
+ _ => null,
+ };
+
+ Widget pinned(String label, LoadingButtonController controller) =>
+ LabelledDemo(
+ label: label,
+ width: 170,
+ child: LoadingButton(
+ type: ButtonType.filled,
+ controller: controller,
+ resetAfterDuration: false,
+ colorStrategy: strategy,
+ colors: colors,
+ sizing: const LoadingButtonSizing.expand(height: 48),
+ successText: 'Done',
+ errorText: 'Failed',
+ child: const Text('Submit'),
+ ),
+ );
+
+ return GallerySection(
+ title: 'Colours',
+ subtitle: 'legacy is the pre-1.1.0 behaviour: primaryColor plus an '
+ 'unconditional white foreground, which is not dark-mode safe. '
+ 'material3 derives everything from the ColorScheme. Toggle the app '
+ 'theme in the title bar to see the difference.',
+ code:
+ 'LoadingButton(colorStrategy: LoadingButtonColorStrategy.material3)',
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: SegmentedButton(
+ segments: const >[
+ ButtonSegment(value: 0, label: Text('legacy')),
+ ButtonSegment(value: 1, label: Text('material3')),
+ ButtonSegment(value: 2, label: Text('traffic')),
+ ButtonSegment(value: 3, label: Text('custom')),
+ ],
+ selected: {_colorIndex},
+ onSelectionChanged: (Set selection) =>
+ setState(() => _colorIndex = selection.first),
+ ),
+ ),
+ const SizedBox(height: 16),
+ DemoWrap(
+ children: [
+ pinned('loading', _pinLoading),
+ pinned('success', _pinSuccess),
+ pinned('error', _pinError),
+ LabelledDemo(
+ label: 'live',
+ width: 170,
+ child: LoadingButton(
+ type: ButtonType.filled,
+ colorStrategy: strategy,
+ colors: colors,
+ onPressed: fakeWork,
+ successText: 'Done',
+ sizing: const LoadingButtonSizing.expand(height: 48),
+ child: const Text('Press me'),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+
+ // ----------------------------------------------------------------- style
+
+ Widget _styleSection() {
+ final ColorScheme scheme = Theme.of(context).colorScheme;
+ return GallerySection(
+ title: 'Styling',
+ subtitle: 'buttonStyle is a plain Material ButtonStyle and is applied '
+ 'first; LoadingButtonStyle layers the package-specific bits (the '
+ 'per-state backgrounds, the border radius the progress fill is '
+ 'clipped to) on top. Reach for buttonStyle whenever Material '
+ 'already models what you want.',
+ code: 'LoadingButton(buttonStyle: ButtonStyle(...), '
+ 'style: LoadingButtonStyle(borderRadius: 28))',
+ child: DemoWrap(
+ children: [
+ LabelledDemo(
+ label: 'buttonStyle',
+ width: 220,
+ child: LoadingButton(
+ type: ButtonType.elevated,
+ onPressed: fakeWork,
+ successText: 'Done',
+ buttonStyle: ButtonStyle(
+ backgroundColor:
+ WidgetStatePropertyAll(scheme.tertiaryContainer),
+ foregroundColor:
+ WidgetStatePropertyAll(scheme.onTertiaryContainer),
+ elevation: const WidgetStatePropertyAll(6),
+ shape: WidgetStatePropertyAll(
+ RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(28),
+ ),
+ ),
+ ),
+ sizing: const LoadingButtonSizing.expand(height: 52),
+ child: const Text('Material style'),
+ ),
+ ),
+ LabelledDemo(
+ label: 'LoadingButtonStyle',
+ width: 220,
+ child: LoadingButton(
+ type: ButtonType.outlined,
+ onPressed: fakeWork,
+ successText: 'Done',
+ style: LoadingButtonStyle(
+ backgroundColor: scheme.surfaceContainerHighest,
+ foregroundColor: scheme.onSurface,
+ loadingBackgroundColor: scheme.secondaryContainer,
+ successBackgroundColor: scheme.tertiaryContainer,
+ errorBackgroundColor: scheme.errorContainer,
+ borderColor: scheme.outline,
+ borderWidth: 1.5,
+ borderRadius: 28,
+ padding: const EdgeInsets.symmetric(horizontal: 24),
+ textStyle: const TextStyle(fontWeight: FontWeight.w700),
+ ),
+ sizing: const LoadingButtonSizing.expand(height: 52),
+ child: const Text('Package style'),
+ ),
+ ),
+ LabelledDemo(
+ label: 'both, with a progress fill',
+ width: 220,
+ child: LoadingButton(
+ type: ButtonType.filled,
+ controller: _pinStyled,
+ resetAfterDuration: false,
+ progressStyle: LoadingProgressStyle.both,
+ buttonStyle: ButtonStyle(
+ backgroundColor: WidgetStatePropertyAll(scheme.primary),
+ foregroundColor:
+ WidgetStatePropertyAll(scheme.onPrimary),
+ ),
+ // The fill is clipped to this radius, so keep the two in step.
+ style: const LoadingButtonStyle(borderRadius: 28),
+ sizing: const LoadingButtonSizing.expand(height: 52),
+ child: const Text('Styled fill'),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ // ------------------------------------------------------------- indicator
+
+ Widget _indicatorSection() => GallerySection(
+ title: 'Indicators',
+ subtitle: 'LoadingIndicator has four constructors and every button '
+ 'family in the package understands all of them. These four are '
+ 'pinned to the loading state.',
+ code: 'LoadingButton(indicator: LoadingIndicator.orb('
+ 'state: OrbState.composing))',
+ child: DemoWrap(
+ children: [
+ LabelledDemo(
+ label: '.circular()',
+ width: 160,
+ child: LoadingButton(
+ type: ButtonType.filled,
+ controller: _pinIndicators[0],
+ resetAfterDuration: false,
+ indicator: const LoadingIndicator.circular(
+ size: 22,
+ strokeWidth: 2.5,
+ ),
+ sizing: const LoadingButtonSizing.expand(height: 48),
+ child: const Text('Busy'),
+ ),
+ ),
+ LabelledDemo(
+ label: '.orb()',
+ width: 160,
+ child: LoadingButton(
+ type: ButtonType.filled,
+ controller: _pinIndicators[1],
+ resetAfterDuration: false,
+ indicator: const LoadingIndicator.orb(
+ state: OrbState.composing,
+ size: 24,
+ speed: 1.2,
+ ),
+ sizing: const LoadingButtonSizing.expand(height: 48),
+ child: const Text('Busy'),
+ ),
+ ),
+ LabelledDemo(
+ label: '.widget()',
+ width: 160,
+ child: LoadingButton(
+ type: ButtonType.filled,
+ controller: _pinIndicators[2],
+ resetAfterDuration: false,
+ indicator: const LoadingIndicator.widget(
+ Icon(Icons.hourglass_bottom, size: 22),
+ ),
+ sizing: const LoadingButtonSizing.expand(height: 48),
+ child: const Text('Busy'),
+ ),
+ ),
+ LabelledDemo(
+ label: '.builder()',
+ width: 160,
+ child: LoadingButton(
+ type: ButtonType.filled,
+ controller: _pinBuilder,
+ resetAfterDuration: false,
+ indicator: LoadingIndicator.builder(
+ (BuildContext context, double? progress) => Text(
+ progress == null ? '…' : '${(progress * 100).round()}%',
+ ),
+ ),
+ sizing: const LoadingButtonSizing.expand(height: 48),
+ child: const Text('Busy'),
+ ),
+ ),
+ ],
+ ),
+ );
+
+ // ---------------------------------------------------------------- guards
+
+ Widget _guardsSection() => GallerySection(
+ title: 'Guards and niceties',
+ subtitle: 'debounce ignores taps that arrive too soon after the last '
+ 'accepted one; cooldown holds the button disabled after a run; '
+ 'enabled maps straight onto ActionState.disabled.',
+ code: 'LoadingButton(debounce: Duration(seconds: 2), '
+ 'cooldown: Duration(seconds: 3), enabled: false)',
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ DemoWrap(
+ children: [
+ LabelledDemo(
+ label: 'debounce 2s — accepted: $_debouncedTaps',
+ width: 240,
+ child: LoadingButton(
+ type: ButtonType.filled,
+ debounce: const Duration(seconds: 2),
+ successDuration: const Duration(milliseconds: 400),
+ onPressed: () async {
+ setState(() => _debouncedTaps++);
+ await fakeWork(const Duration(milliseconds: 300));
+ },
+ sizing: const LoadingButtonSizing.expand(height: 48),
+ child: const Text('Tap me fast'),
+ ),
+ ),
+ LabelledDemo(
+ label: 'cooldown 3s',
+ width: 200,
+ child: LoadingButton(
+ type: ButtonType.outlined,
+ cooldown: const Duration(seconds: 3),
+ successDuration: const Duration(milliseconds: 400),
+ onPressed: () =>
+ fakeWork(const Duration(milliseconds: 400)),
+ successText: 'Sent',
+ sizing: const LoadingButtonSizing.expand(height: 48),
+ child: const Text('Resend code'),
+ ),
+ ),
+ LabelledDemo(
+ label: 'transitionBuilder',
+ width: 200,
+ child: LoadingButton(
+ type: ButtonType.filled,
+ onPressed: fakeWork,
+ successText: 'Zoomed',
+ animationDuration: const Duration(milliseconds: 400),
+ transitionBuilder:
+ (Widget child, Animation animation) =>
+ ScaleTransition(scale: animation, child: child),
+ sizing: const LoadingButtonSizing.expand(height: 48),
+ child: const Text('Scale, not fade'),
+ ),
+ ),
+ ],
+ ),
+ const Divider(height: 32),
+ Row(
+ children: [
+ Switch(
+ key: const Key('enabled-switch'),
+ value: _enabled,
+ onChanged: (bool value) => setState(() => _enabled = value),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ _enabled
+ ? 'enabled: true'
+ : 'enabled: false — the button reports '
+ 'ActionState.disabled',
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ LoadingButton(
+ key: const Key('enabled-button'),
+ type: ButtonType.filled,
+ enabled: _enabled,
+ autofocus: false,
+ focusNode: _focusNode,
+ tooltip: 'Focusable, and disabled by the switch above',
+ onPressed: fakeWork,
+ onStateChanged: (ActionState state) =>
+ setState(() => _lastObservedState = state),
+ sizing: const LoadingButtonSizing.expand(height: 48),
+ child: const Text('Focusable button'),
+ ),
+ const SizedBox(height: 10),
+ Text('onStateChanged saw: ${_lastObservedState.name}'),
+ ],
+ ),
+ );
+
+ // ------------------------------------------------------------- state key
+
+ Widget _stateKeySection() => GallerySection(
+ title: 'Reaching the State directly',
+ subtitle: 'LoadingButtonState is public, so a GlobalKey gets you '
+ 'press() and currentState without a controller. A controller is '
+ 'usually the better tool, but this works.',
+ code: 'GlobalKey().currentState?.press()',
+ child: DemoWrap(
+ children: [
+ LoadingButton(
+ key: _stateKey,
+ type: ButtonType.filled,
+ onPressed: fakeWork,
+ successText: 'Ran',
+ child: const Text('Target'),
+ ),
+ OutlinedButton(
+ onPressed: () => unawaited(
+ _stateKey.currentState?.press() ?? Future.value()),
+ child: const Text('press() it from here'),
+ ),
+ OutlinedButton(
+ onPressed: () => showNote(
+ context,
+ 'currentState: '
+ '${_stateKey.currentState?.currentState.name ?? 'unmounted'}',
+ ),
+ child: const Text('read currentState'),
+ ),
+ ],
+ ),
+ );
+
+ // --------------------------------------------------------------- builder
+
+ Widget _builderSection() => GallerySection(
+ title: 'Fluent builder',
+ subtitle: 'LoadingButtonBuilder is a mutable, single-use builder: '
+ 'configure it, build it once.',
+ code: 'LoadingButtonBuilder.filled().onPressed(submit)'
+ ".child(Text('Go')).build()",
+ child: LoadingButtonBuilder.filled()
+ .onPressed(fakeWork)
+ .child(const Text('Built fluently'))
+ .indicator(
+ const LoadingIndicator.orb(state: OrbState.solving, size: 22))
+ .successText('Built and done')
+ .sizing(const LoadingButtonSizing.intrinsic())
+ .tooltip('Every LoadingButton parameter has a setter')
+ .onFailure((Object error, StackTrace stack) => debugPrint('$error'))
+ .build(),
+ );
+
+ // ---------------------------------------------------------- subtree theme
+
+ Widget _subtreeThemeSection() => GallerySection(
+ title: 'Defaults for a subtree',
+ subtitle: 'A LoadingButtonTheme widget beats the ThemeExtension, which '
+ 'beats the package defaults. Both buttons below inherit an orb '
+ 'indicator, a custom success widget and expanded sizing without '
+ 'saying so themselves.',
+ code: 'LoadingButtonTheme(data: LoadingButtonThemeData(...), '
+ 'child: subtree)',
+ child: LoadingButtonTheme(
+ data: const LoadingButtonThemeData(
+ indicator: LoadingIndicator.orb(state: OrbState.weaving, size: 24),
+ successWidget: Icon(Icons.done_all, size: 20),
+ sizing: LoadingButtonSizing.expand(height: 52),
+ colorStrategy: LoadingButtonColorStrategy.material3,
+ successDuration: Duration(milliseconds: 1200),
+ ),
+ child: Column(
+ children: [
+ LoadingButton(
+ type: ButtonType.filled,
+ onPressed: fakeWork,
+ child: const Text('Inherits everything'),
+ ),
+ const SizedBox(height: 12),
+ LoadingButton(
+ type: ButtonType.outlined,
+ onPressed: fakeWork,
+ child: const Text('So does this one'),
+ ),
+ ],
+ ),
+ ),
+ );
+
+ static String _titleCase(String value) =>
+ value[0].toUpperCase() + value.substring(1);
+}
diff --git a/example/lib/pages/orbs_page.dart b/example/lib/pages/orbs_page.dart
new file mode 100644
index 0000000..404fe59
--- /dev/null
+++ b/example/lib/pages/orbs_page.dart
@@ -0,0 +1,350 @@
+import 'package:flutter/material.dart';
+import 'package:loading_icon_button/loading_icon_button.dart';
+
+import '../gallery.dart';
+
+/// Which ink the orbs on this page use.
+enum _Tint { none, primary, tertiary }
+
+/// All nine [OrbState]s at both tuned size tiers, with live controls for
+/// theme, speed, pause and tint.
+class OrbsPage extends StatefulWidget {
+ const OrbsPage({super.key});
+
+ @override
+ State createState() => _OrbsPageState();
+}
+
+class _OrbsPageState extends State {
+ OrbTheme _theme = OrbTheme.auto;
+ _Tint _tint = _Tint.none;
+ double _speed = 1;
+ bool _paused = false;
+
+ Color? get _color {
+ final ColorScheme scheme = Theme.of(context).colorScheme;
+ switch (_tint) {
+ case _Tint.primary:
+ return scheme.primary;
+ case _Tint.tertiary:
+ return scheme.tertiary;
+ case _Tint.none:
+ return null;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return GalleryPage(
+ title: 'Thinking orbs',
+ blurb: 'Nine animations over a rotated, z-sorted 3D point field. No '
+ 'shaders, no blurs, no assets — one CustomPainter, identical on '
+ 'every platform Flutter supports.',
+ children: [
+ _controls(),
+ _grid(),
+ _inline(),
+ _notes(),
+ ],
+ );
+ }
+
+ // -------------------------------------------------------------- controls
+
+ Widget _controls() => GallerySection(
+ title: 'Controls',
+ subtitle: 'Every mounted orb reads the same static clock, so the whole '
+ 'page stays in step no matter when each orb was built.',
+ code: 'ThinkingOrb(state: OrbState.searching, size: 64, speed: 1.0, '
+ 'paused: false, theme: OrbTheme.auto)',
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _controlRow(
+ 'theme',
+ SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: SegmentedButton(
+ segments: const >[
+ ButtonSegment(
+ value: OrbTheme.auto,
+ label: Text('auto'),
+ ),
+ ButtonSegment(
+ value: OrbTheme.light,
+ label: Text('light'),
+ ),
+ ButtonSegment(
+ value: OrbTheme.dark,
+ label: Text('dark'),
+ ),
+ ],
+ selected: {_theme},
+ onSelectionChanged: (Set selection) =>
+ setState(() => _theme = selection.first),
+ ),
+ ),
+ ),
+ const SizedBox(height: 12),
+ _controlRow(
+ 'tint',
+ SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: SegmentedButton<_Tint>(
+ segments: const >[
+ ButtonSegment<_Tint>(
+ value: _Tint.none,
+ label: Text('monochrome'),
+ ),
+ ButtonSegment<_Tint>(
+ value: _Tint.primary,
+ label: Text('primary'),
+ ),
+ ButtonSegment<_Tint>(
+ value: _Tint.tertiary,
+ label: Text('tertiary'),
+ ),
+ ],
+ selected: <_Tint>{_tint},
+ onSelectionChanged: (Set<_Tint> selection) =>
+ setState(() => _tint = selection.first),
+ ),
+ ),
+ ),
+ const SizedBox(height: 4),
+ _controlRow(
+ 'speed',
+ Row(
+ children: [
+ Expanded(
+ child: Slider(
+ value: _speed,
+ min: 0.25,
+ max: 3,
+ divisions: 11,
+ label: '${_speed.toStringAsFixed(2)}x',
+ onChanged: (double value) =>
+ setState(() => _speed = value),
+ ),
+ ),
+ SizedBox(
+ width: 56,
+ child: Text('${_speed.toStringAsFixed(2)}x'),
+ ),
+ ],
+ ),
+ ),
+ _controlRow(
+ 'paused',
+ Row(
+ children: [
+ Switch(
+ key: const Key('orb-pause-switch'),
+ value: _paused,
+ onChanged: (bool value) => setState(() => _paused = value),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ _paused
+ ? 'frozen on the current frame'
+ : 'animating (also stops automatically when the '
+ 'route is inactive or the platform asks for '
+ 'reduced motion)',
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+
+ Widget _controlRow(String label, Widget control) => Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ SizedBox(
+ width: 68,
+ child: Text(
+ label,
+ style: Theme.of(context).textTheme.labelMedium,
+ ),
+ ),
+ Expanded(child: control),
+ ],
+ );
+
+ // ------------------------------------------------------------------ grid
+
+ Widget _grid() => GallerySection(
+ title: 'Nine states, two tiers',
+ subtitle:
+ 'Sizes below kOrbTierBreakpoint (${kOrbTierBreakpoint.toInt()}'
+ ') use the inline tuning; sizes at or above it use the avatar '
+ 'tuning. They are separate designs with their own dot counts, dot '
+ 'sizes and speeds, not one design scaled — which is what keeps a '
+ '20px orb legible.',
+ code: 'const ThinkingOrb(state: OrbState.solving, size: 64)',
+ child: Wrap(
+ spacing: 16,
+ runSpacing: 16,
+ children: [
+ for (final OrbState state in OrbState.values)
+ _OrbCell(
+ state: state,
+ theme: _theme,
+ speed: _speed,
+ paused: _paused,
+ color: _color,
+ ),
+ ],
+ ),
+ );
+
+ // ---------------------------------------------------------------- inline
+
+ Widget _inline() => GallerySection(
+ title: 'At text size',
+ subtitle: 'The inline tier is tuned at 20 logical pixels, for sitting '
+ 'beside a line of copy.',
+ code: 'ThinkingOrb(state: OrbState.breathing, size: 20)',
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ for (final OrbState state in [
+ OrbState.breathing,
+ OrbState.searching,
+ OrbState.connecting,
+ ])
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 6),
+ child: Row(
+ children: [
+ ThinkingOrb(
+ state: state,
+ size: 20,
+ theme: _theme,
+ speed: _speed,
+ paused: _paused,
+ color: _color,
+ semanticLabel: '',
+ ),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Text(
+ '${kOrbSemanticLabels[state]}… this is what an orb '
+ 'looks like next to body copy.',
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+
+ // ----------------------------------------------------------------- notes
+
+ Widget _notes() => GallerySection(
+ title: 'Accessibility and testing',
+ subtitle: 'Each state ships a default screen-reader label from '
+ 'kOrbSemanticLabels. Pass semanticLabel: \'\' to hide an orb from '
+ 'assistive tech when a nearby label already describes it — which '
+ 'is exactly what LoadingIndicator.orb() does inside a button.',
+ code: 'const ThinkingOrb(state: OrbState.working, paused: true) '
+ '// for pumpAndSettle',
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ for (final MapEntry entry
+ in kOrbSemanticLabels.entries)
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 2),
+ child: Text('OrbState.${entry.key.name} → "${entry.value}"'),
+ ),
+ const SizedBox(height: 12),
+ Text(
+ 'An orb animates continuously, so tester.pumpAndSettle() never '
+ 'settles while one is mounted. Drive widget tests with '
+ 'tester.pump(duration), or mount the orb with paused: true.',
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ ),
+ );
+}
+
+/// One state, shown at both tuned tiers with its label.
+class _OrbCell extends StatelessWidget {
+ const _OrbCell({
+ required this.state,
+ required this.theme,
+ required this.speed,
+ required this.paused,
+ required this.color,
+ });
+
+ final OrbState state;
+ final OrbTheme theme;
+ final double speed;
+ final bool paused;
+ final Color? color;
+
+ @override
+ Widget build(BuildContext context) {
+ final ColorScheme scheme = Theme.of(context).colorScheme;
+ return Container(
+ width: 168,
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ decoration: BoxDecoration(
+ color: scheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(16),
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ // Avatar tier.
+ ThinkingOrb(
+ key: Key('orb-avatar-${state.name}'),
+ state: state,
+ size: 64,
+ theme: theme,
+ speed: speed,
+ paused: paused,
+ color: color,
+ ),
+ const SizedBox(width: 20),
+ // Inline tier.
+ ThinkingOrb(
+ key: Key('orb-inline-${state.name}'),
+ state: state,
+ size: 20,
+ theme: theme,
+ speed: speed,
+ paused: paused,
+ color: color,
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Text(
+ state.name,
+ style: Theme.of(context).textTheme.labelLarge,
+ ),
+ Text(
+ '64 · 20',
+ style: Theme.of(context).textTheme.labelSmall?.copyWith(
+ color: scheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/example/lib/pages/overview_page.dart b/example/lib/pages/overview_page.dart
new file mode 100644
index 0000000..ae00c0c
--- /dev/null
+++ b/example/lib/pages/overview_page.dart
@@ -0,0 +1,219 @@
+import 'package:flutter/material.dart';
+import 'package:loading_icon_button/loading_icon_button.dart';
+
+import '../gallery.dart';
+
+/// The landing page: what the package ships, with one live sample of each
+/// family and a jump to its full page.
+class OverviewPage extends StatelessWidget {
+ const OverviewPage({super.key, required this.onNavigate});
+
+ /// Switches the shell to the destination at this index.
+ final ValueChanged onNavigate;
+
+ @override
+ Widget build(BuildContext context) {
+ final ColorScheme scheme = Theme.of(context).colorScheme;
+
+ return GalleryPage(
+ title: 'loading_icon_button',
+ blurb: 'Four button families with a built-in busy state, plus nine '
+ 'animated thinking orbs. Everything below is live — press it.',
+ children: [
+ Card(
+ margin: const EdgeInsets.only(bottom: 16),
+ color: scheme.primaryContainer,
+ child: Padding(
+ padding: const EdgeInsets.all(20),
+ child: Row(
+ children: [
+ const ThinkingOrb(state: OrbState.breathing, size: 56),
+ const SizedBox(width: 20),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'One busy state, four ways to drive it',
+ style: Theme.of(context)
+ .textTheme
+ .titleMedium
+ ?.copyWith(color: scheme.onPrimaryContainer),
+ ),
+ const SizedBox(height: 6),
+ Text(
+ 'Drive it from an async callback, from a plain bool, '
+ 'or from a controller you hold yourself.',
+ style: Theme.of(context)
+ .textTheme
+ .bodyMedium
+ ?.copyWith(color: scheme.onPrimaryContainer),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ _FamilyCard(
+ title: 'LoadingButton',
+ description: 'A full idle → loading → success/error state machine in '
+ 'one widget, in any of the five Material button shapes.',
+ code: "LoadingButton(onPressed: submit, child: Text('Submit'))",
+ onOpen: () => onNavigate(1),
+ demo: LoadingButton(
+ type: ButtonType.filled,
+ onPressed: fakeWork,
+ successText: 'Saved',
+ child: const Text('Save changes'),
+ ),
+ ),
+ _FamilyCard(
+ title: 'Auto-loading buttons',
+ description: 'Thin wrappers over the Material buttons that stay busy '
+ 'for exactly as long as their async callback runs. Their '
+ 'flag-driven twins take an isLoading bool instead.',
+ code: 'ElevatedAutoLoadingButton(onPressed: () async { ... })',
+ onOpen: () => onNavigate(2),
+ demo: DemoWrap(
+ children: [
+ ElevatedAutoLoadingButton(
+ onPressed: fakeWork,
+ child: const Text('Elevated'),
+ ),
+ FilledAutoLoadingButton.tonalIcon(
+ onPressed: fakeWork,
+ icon: const Icon(Icons.cloud_upload_outlined),
+ label: const Text('Tonal'),
+ ),
+ IconAutoLoadingButton.filled(
+ onPressed: fakeWork,
+ icon: const Icon(Icons.refresh),
+ ),
+ ],
+ ),
+ ),
+ _FamilyCard(
+ title: 'Argon buttons',
+ description: 'A button that collapses into its own loader, and a '
+ 'countdown variant for "resend in 30s" affordances.',
+ code: 'ArgonButton(height: 48, width: 200, onTap: ...)',
+ onOpen: () => onNavigate(3),
+ demo: ArgonButton(
+ height: 48,
+ width: 200,
+ minWidth: 48,
+ borderRadius: 24,
+ color: scheme.primary,
+ loader: const Padding(
+ padding: EdgeInsets.all(10),
+ child: ThinkingOrb(state: OrbState.working, size: 28),
+ ),
+ onTap: (Function startLoading, Function stopLoading,
+ ArgonButtonState state) async {
+ if (state != ArgonButtonState.idle) return;
+ startLoading();
+ await fakeWork();
+ stopLoading();
+ },
+ child: Text(
+ 'Collapse me',
+ style: TextStyle(color: scheme.onPrimary),
+ ),
+ ),
+ ),
+ _FamilyCard(
+ title: 'Thinking orbs',
+ description: 'Nine hand-tuned dotted-sphere animations for AI and '
+ 'agent interfaces, each with a separate tuning for inline and '
+ 'avatar sizes. No shaders, no assets, one CustomPainter.',
+ code: 'ThinkingOrb(state: OrbState.searching, size: 64)',
+ onOpen: () => onNavigate(4),
+ demo: const DemoWrap(
+ children: [
+ ThinkingOrb(state: OrbState.working, size: 48),
+ ThinkingOrb(state: OrbState.searching, size: 48),
+ ThinkingOrb(state: OrbState.connecting, size: 48),
+ ThinkingOrb(state: OrbState.shaping, size: 48),
+ ThinkingOrb(state: OrbState.listening, size: 48),
+ ],
+ ),
+ ),
+ GallerySection(
+ title: 'Indicators are interchangeable',
+ subtitle: 'Every family accepts the same LoadingIndicator, so '
+ 'swapping an app from spinners to orbs is one line in a theme.',
+ code: 'LoadingButtonTheme(data: LoadingButtonThemeData('
+ 'indicator: LoadingIndicator.orb()), child: app)',
+ child: DemoWrap(
+ children: [
+ LoadingButton(
+ onPressed: fakeWork,
+ indicator: const LoadingIndicator.circular(),
+ child: const Text('Spinner'),
+ ),
+ LoadingButton(
+ type: ButtonType.filled,
+ onPressed: fakeWork,
+ indicator: const LoadingIndicator.orb(
+ state: OrbState.weaving,
+ size: 22,
+ ),
+ child: const Text('Orb'),
+ ),
+ ElevatedAutoLoadingButton(
+ onPressed: fakeWork,
+ indicator: const LoadingIndicator.orb(
+ state: OrbState.solving,
+ size: 22,
+ ),
+ child: const Text('Orb, auto'),
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+}
+
+class _FamilyCard extends StatelessWidget {
+ const _FamilyCard({
+ required this.title,
+ required this.description,
+ required this.code,
+ required this.demo,
+ required this.onOpen,
+ });
+
+ final String title;
+ final String description;
+ final String code;
+ final Widget demo;
+ final VoidCallback onOpen;
+
+ @override
+ Widget build(BuildContext context) {
+ return GallerySection(
+ title: title,
+ subtitle: description,
+ code: code,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ demo,
+ const SizedBox(height: 12),
+ Align(
+ alignment: Alignment.centerRight,
+ child: TextButton.icon(
+ onPressed: onOpen,
+ icon: const Icon(Icons.arrow_forward, size: 18),
+ label: const Text('See every option'),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 9257382..7cb2d90 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -1,93 +1,36 @@
name: example
-description: A new Flutter project.
+description: >-
+ Gallery app for the loading_icon_button package: every public widget, in
+ light and dark Material 3.
-# The following line prevents the package from being accidentally published to
-# pub.dev using `flutter pub publish`. This is preferred for private packages.
-publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+# Never publish the gallery itself.
+publish_to: "none"
-# The following defines the version and build number for your application.
-# A version number is three numbers separated by dots, like 1.2.43
-# followed by an optional build number separated by a +.
-# Both the version and the builder number may be overridden in flutter
-# build by specifying --build-name and --build-number, respectively.
-# In Android, build-name is used as versionName while build-number used as versionCode.
-# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
-# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
-# Read more about iOS versioning at
-# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
-version: 1.0.0+1
+version: 1.1.0+1
environment:
- sdk: ">=2.15.1 <3.0.0"
+ # Deliberately the same floor as the package, so the gallery proves
+ # loading_icon_button really does build on the oldest SDK it claims.
+ sdk: ">=3.4.0 <4.0.0"
+ flutter: ">=3.22.0"
-# Dependencies specify other packages that your package needs in order to work.
-# To automatically upgrade your package dependencies to the latest versions
-# consider running `flutter pub upgrade --major-versions`. Alternatively,
-# dependencies can be manually updated by changing the version numbers below to
-# the latest version available on pub.dev. To see which dependencies have newer
-# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
-
- # The following adds the Cupertino Icons font to your application.
- # Use with the CupertinoIcons class for iOS style icons.
- cupertino_icons: ^1.0.4
+ # The package under demonstration. A real dependency, not a dev_dependency:
+ # lib/main.dart imports it.
+ loading_icon_button:
+ path: ../
dev_dependencies:
flutter_test:
sdk: flutter
- loading_icon_button:
- path: ../
- phosphor_flutter: ^1.4.0
- google_fonts: ^2.2.0
-
- # The "flutter_lints" package below contains a set of recommended lints to
- # encourage good coding practices. The lint set provided by the package is
- # activated in the `analysis_options.yaml` file located at the root of your
- # package. See that file for information about deactivating specific lint
- # rules and activating additional ones.
- flutter_lints: ^1.0.4
+ flutter_lints: ^6.0.0
-# For information on the generic Dart part of this file, see the
-# following page: https://dart.dev/tools/pub/pubspec
+# No third-party packages on purpose. The gallery uses Material icons and the
+# default type ramp, so `flutter pub get` here resolves nothing but the
+# package itself.
-# The following section is specific to Flutter.
flutter:
-
- # The following line ensures that the Material Icons font is
- # included with your application, so that you can use the icons in
- # the material Icons class.
uses-material-design: true
-
- # To add assets to your application, add an assets section, like this:
- # assets:
- # - images/a_dot_burr.jpeg
- # - images/a_dot_ham.jpeg
-
- # An image asset can refer to one or more resolution-specific "variants", see
- # https://flutter.dev/assets-and-images/#resolution-aware.
-
- # For details regarding adding assets from package dependencies, see
- # https://flutter.dev/assets-and-images/#from-packages
-
- # To add custom fonts to your application, add a fonts section here,
- # in this "flutter" section. Each entry in this list should have a
- # "family" key with the font family name, and a "fonts" key with a
- # list giving the asset and other descriptors for the font. For
- # example:
- # fonts:
- # - family: Schyler
- # fonts:
- # - asset: fonts/Schyler-Regular.ttf
- # - asset: fonts/Schyler-Italic.ttf
- # style: italic
- # - family: Trajan Pro
- # fonts:
- # - asset: fonts/TrajanPro.ttf
- # - asset: fonts/TrajanPro_Bold.ttf
- # weight: 700
- #
- # For details regarding fonts from package dependencies,
- # see https://flutter.dev/custom-fonts/#from-packages
diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart
index 747db1d..5d76a17 100644
--- a/example/test/widget_test.dart
+++ b/example/test/widget_test.dart
@@ -1,30 +1,334 @@
-// This is a basic Flutter widget test.
+// Smoke tests for the gallery app.
//
-// To perform an interaction with a widget in your test, use the WidgetTester
-// utility that Flutter provides. For example, you can send tap and scroll
-// gestures. You can also use WidgetTester to find child widgets in the widget
-// tree, read text, and verify that the values of widget properties are correct.
+// A note on pumping: a ThinkingOrb animates continuously, and the gallery
+// mounts orbs on four of its five pages, so `pumpAndSettle()` would never
+// return. Everything below drives time with explicit `pump(duration)` calls
+// instead — which is the same advice the package gives its own users.
+import 'package:example/main.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
-
-import 'package:example/main.dart';
+import 'package:loading_icon_button/loading_icon_button.dart';
void main() {
- testWidgets('Counter increments smoke test', (WidgetTester tester) async {
- // Build our app and trigger a frame.
- await tester.pumpWidget(MyApp());
+ /// A window wide enough for the navigation rail and tall enough that most
+ /// sections are laid out without scrolling.
+ void useLargeWindow(WidgetTester tester) {
+ tester.view.physicalSize = const Size(1200, 2400);
+ tester.view.devicePixelRatio = 1;
+ addTearDown(tester.view.resetPhysicalSize);
+ addTearDown(tester.view.resetDevicePixelRatio);
+ }
+
+ /// Advances time in one-second steps, so periodic timers fire the way they
+ /// would in a running app.
+ Future pumpSeconds(WidgetTester tester, int seconds) async {
+ for (int i = 0; i < seconds; i++) {
+ await tester.pump(const Duration(seconds: 1));
+ }
+ }
+
+ /// Scrolls [finder] into view, then taps it.
+ Future scrollAndTap(WidgetTester tester, Finder finder) async {
+ await tester.ensureVisible(finder);
+ await tester.pump();
+ await tester.tap(finder);
+ await tester.pump();
+ }
+
+ /// Switches the shell to the destination whose unselected icon is [icon].
+ Future goTo(WidgetTester tester, IconData icon) async {
+ await tester.tap(find.byIcon(icon).first);
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 400));
+ }
+
+ testWidgets('opens on the overview page with every destination listed',
+ (WidgetTester tester) async {
+ useLargeWindow(tester);
+ await tester.pumpWidget(const GalleryApp());
+ await tester.pump();
+
+ expect(find.widgetWithText(AppBar, 'Overview'), findsOneWidget);
+ expect(find.byType(NavigationRail), findsOneWidget);
+
+ for (final String label in [
+ 'Overview',
+ 'LoadingButton',
+ 'Auto-loading',
+ 'Argon',
+ 'Thinking orbs',
+ ]) {
+ expect(
+ find.descendant(
+ of: find.byType(NavigationRail),
+ matching: find.text(label),
+ ),
+ findsOneWidget,
+ reason: 'the $label destination should be in the rail',
+ );
+ }
+
+ // The overview samples every family.
+ expect(find.byType(LoadingButton), findsWidgets);
+ expect(find.byType(ElevatedAutoLoadingButton), findsWidgets);
+ expect(find.byType(ArgonButton), findsWidgets);
+ expect(find.byType(ThinkingOrb), findsWidgets);
+ });
+
+ testWidgets('the app bar toggle flips the theme brightness',
+ (WidgetTester tester) async {
+ useLargeWindow(tester);
+ await tester.pumpWidget(const GalleryApp());
+ await tester.pump();
+
+ Brightness currentBrightness() =>
+ Theme.of(tester.element(find.byType(NavigationRail))).brightness;
+
+ expect(currentBrightness(), Brightness.light);
+ expect(find.byIcon(Icons.dark_mode), findsOneWidget);
+
+ await tester.tap(find.byKey(const Key('theme-toggle')));
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 400));
- // Verify that our counter starts at 0.
- expect(find.text('0'), findsOneWidget);
- expect(find.text('1'), findsNothing);
+ expect(currentBrightness(), Brightness.dark);
+ expect(find.byIcon(Icons.light_mode), findsOneWidget);
- // Tap the '+' icon and trigger a frame.
- await tester.tap(find.byIcon(Icons.add));
+ await tester.tap(find.byKey(const Key('theme-toggle')));
await tester.pump();
+ await tester.pump(const Duration(milliseconds: 400));
+
+ expect(currentBrightness(), Brightness.light);
+ });
+
+ testWidgets('an overview card links through to its page',
+ (WidgetTester tester) async {
+ useLargeWindow(tester);
+ await tester.pumpWidget(const GalleryApp());
+ await tester.pump();
+
+ await scrollAndTap(
+ tester,
+ find.widgetWithText(TextButton, 'See every option').first,
+ );
+ await tester.pump(const Duration(milliseconds: 400));
+
+ expect(find.widgetWithText(AppBar, 'LoadingButton'), findsOneWidget);
+ });
+
+ testWidgets('a LoadingButton runs its callback and reports success',
+ (WidgetTester tester) async {
+ useLargeWindow(tester);
+ await tester.pumpWidget(const GalleryApp());
+ await tester.pump();
+
+ await goTo(tester, Icons.smart_button_outlined);
+
+ final Finder button = find.byKey(const Key('outcome-success'));
+ expect(button, findsOneWidget);
+ expect(find.text('Succeeds'), findsOneWidget);
+
+ await scrollAndTap(tester, button);
+
+ // Loading: the callback is in flight.
+ expect(find.text('Sending…'), findsOneWidget);
+
+ // The fake work takes 1200ms.
+ await tester.pump(const Duration(milliseconds: 1500));
+ expect(find.text('Sent'), findsOneWidget);
+
+ // Success is held for two seconds, then the button resets itself.
+ await pumpSeconds(tester, 3);
+ expect(find.text('Succeeds'), findsOneWidget);
+
+ // Drain the snack bar onSuccess posted.
+ await pumpSeconds(tester, 4);
+ });
+
+ testWidgets('the controller drives every attached button',
+ (WidgetTester tester) async {
+ useLargeWindow(tester);
+ await tester.pumpWidget(const GalleryApp());
+ await tester.pump();
+
+ await goTo(tester, Icons.smart_button_outlined);
+
+ expect(find.byKey(const Key('controller-state')), findsOneWidget);
+ expect(
+ find.descendant(
+ of: find.byKey(const Key('controller-state')),
+ matching: find.text('state: idle'),
+ ),
+ findsOneWidget,
+ );
+
+ await scrollAndTap(tester, find.widgetWithText(OutlinedButton, 'start()'));
+ await tester.pump(const Duration(milliseconds: 400));
+
+ expect(
+ find.descendant(
+ of: find.byKey(const Key('controller-state')),
+ matching: find.text('state: loading'),
+ ),
+ findsOneWidget,
+ );
+
+ // Two buttons share the controller, so both moved.
+ expect(find.text('attached to 2'), findsOneWidget);
+
+ await scrollAndTap(tester, find.widgetWithText(OutlinedButton, 'reset()'));
+ await pumpSeconds(tester, 3);
+ });
+
+ testWidgets('an auto-loading button recovers when its callback throws',
+ (WidgetTester tester) async {
+ useLargeWindow(tester);
+ await tester.pumpWidget(const GalleryApp());
+ await tester.pump();
+
+ await goTo(tester, Icons.autorenew_outlined);
+
+ final Finder button = find.byKey(const Key('auto-throws'));
+ expect(button, findsOneWidget);
+ expect(find.text('Throws (recovers)'), findsOneWidget);
+
+ await scrollAndTap(tester, button);
+
+ // Busy: the label has been replaced by an indicator.
+ expect(find.text('Throws (recovers)'), findsNothing);
+
+ // fakeFailure throws after 900ms. With no caller awaiting doPress(), the
+ // package routes the failure to FlutterError rather than dropping it.
+ await tester.pump(const Duration(milliseconds: 1200));
+ expect(tester.takeException(), isNotNull);
+
+ // ...and the button is usable again rather than stuck spinning.
+ await tester.pump(const Duration(milliseconds: 400));
+ expect(find.text('Throws (recovers)'), findsOneWidget);
+ });
+
+ testWidgets('the isLoading flag swaps every flag-driven button',
+ (WidgetTester tester) async {
+ useLargeWindow(tester);
+ await tester.pumpWidget(const GalleryApp());
+ await tester.pump();
+
+ await goTo(tester, Icons.autorenew_outlined);
+
+ // 'Elevated' also labels a button in the auto section above, so scope the
+ // search to the flag-driven row.
+ Finder inFlagRow(String label) => find.descendant(
+ of: find.byKey(const Key('flag-driven-demos')),
+ matching: find.text(label),
+ );
+
+ expect(inFlagRow('Elevated'), findsOneWidget);
+
+ await scrollAndTap(tester, find.byKey(const Key('is-loading-switch')));
+ await tester.pump(const Duration(milliseconds: 400));
+
+ expect(find.text('isLoading: true'), findsOneWidget);
+ // The plain labels are gone; the busy ones are in their place.
+ expect(inFlagRow('Elevated'), findsNothing);
+ expect(inFlagRow('Saving…'), findsOneWidget);
+ expect(inFlagRow('Still tappable'), findsOneWidget);
+
+ await scrollAndTap(tester, find.byKey(const Key('is-loading-switch')));
+ await tester.pump(const Duration(milliseconds: 400));
+ expect(inFlagRow('Elevated'), findsOneWidget);
+ });
+
+ testWidgets('the orbs page shows all nine states at both tiers',
+ (WidgetTester tester) async {
+ useLargeWindow(tester);
+ await tester.pumpWidget(const GalleryApp());
+ await tester.pump();
+
+ await goTo(tester, Icons.blur_on_outlined);
+
+ for (final OrbState state in OrbState.values) {
+ expect(
+ find.byKey(Key('orb-avatar-${state.name}')),
+ findsOneWidget,
+ reason: '${state.name} should be shown at the avatar tier',
+ );
+ expect(
+ find.byKey(Key('orb-inline-${state.name}')),
+ findsOneWidget,
+ reason: '${state.name} should be shown at the inline tier',
+ );
+ expect(find.text(state.name), findsOneWidget);
+ }
+
+ // Nine states x two tiers, plus the three inline-with-copy samples.
+ expect(find.byType(ThinkingOrb),
+ findsNWidgets(OrbState.values.length * 2 + 3));
+
+ // Orbs keep animating; pausing them must not throw or unmount anything.
+ await scrollAndTap(tester, find.byKey(const Key('orb-pause-switch')));
+ await tester.pump(const Duration(milliseconds: 400));
+ expect(find.byType(ThinkingOrb),
+ findsNWidgets(OrbState.values.length * 2 + 3));
+ });
+
+ testWidgets('every page lays out without overflowing a phone-sized window',
+ (WidgetTester tester) async {
+ tester.view.physicalSize = const Size(390, 844);
+ tester.view.devicePixelRatio = 1;
+ addTearDown(tester.view.resetPhysicalSize);
+ addTearDown(tester.view.resetDevicePixelRatio);
+
+ await tester.pumpWidget(const GalleryApp());
+ await tester.pump();
+
+ // Narrow windows get the bottom navigation bar instead of the rail.
+ expect(find.byType(NavigationBar), findsOneWidget);
+ expect(find.byType(NavigationRail), findsNothing);
+
+ for (final IconData icon in [
+ Icons.smart_button_outlined,
+ Icons.autorenew_outlined,
+ Icons.swipe_left_outlined,
+ Icons.blur_on_outlined,
+ Icons.dashboard_outlined,
+ ]) {
+ await goTo(tester, icon);
+ expect(
+ tester.takeException(),
+ isNull,
+ reason: 'page behind $icon should lay out cleanly at 390px wide',
+ );
+ // Scroll through the whole page so sections below the fold are laid out.
+ for (int i = 0; i < 12; i++) {
+ await tester.drag(find.byType(ListView), const Offset(0, -400));
+ await tester.pump();
+ expect(
+ tester.takeException(),
+ isNull,
+ reason: 'scrolling the page behind $icon should not overflow',
+ );
+ }
+ }
+
+ // Let the countdown button on the argon page finish its timer.
+ await pumpSeconds(tester, 10);
+ });
+
+ testWidgets('the argon page builds and its countdown runs down',
+ (WidgetTester tester) async {
+ useLargeWindow(tester);
+ await tester.pumpWidget(const GalleryApp());
+ await tester.pump();
+
+ await goTo(tester, Icons.swipe_left_outlined);
+
+ expect(find.byKey(const Key('argon-basic')), findsOneWidget);
+ expect(find.byKey(const Key('argon-timer')), findsOneWidget);
- // Verify that our counter has incremented.
- expect(find.text('0'), findsNothing);
- expect(find.text('1'), findsOneWidget);
+ // One of the timer buttons starts counting down on mount; let it finish so
+ // no periodic timer outlives the test.
+ await pumpSeconds(tester, 10);
+ expect(find.text('initialTimer: 5'), findsOneWidget);
});
}
diff --git a/example/web/favicon.png b/example/web/favicon.png
deleted file mode 100644
index 8aaa46a..0000000
Binary files a/example/web/favicon.png and /dev/null differ
diff --git a/example/web/icons/Icon-192.png b/example/web/icons/Icon-192.png
deleted file mode 100644
index b749bfe..0000000
Binary files a/example/web/icons/Icon-192.png and /dev/null differ
diff --git a/example/web/icons/Icon-512.png b/example/web/icons/Icon-512.png
deleted file mode 100644
index 88cfd48..0000000
Binary files a/example/web/icons/Icon-512.png and /dev/null differ
diff --git a/example/web/icons/Icon-maskable-192.png b/example/web/icons/Icon-maskable-192.png
deleted file mode 100644
index eb9b4d7..0000000
Binary files a/example/web/icons/Icon-maskable-192.png and /dev/null differ
diff --git a/example/web/icons/Icon-maskable-512.png b/example/web/icons/Icon-maskable-512.png
deleted file mode 100644
index d69c566..0000000
Binary files a/example/web/icons/Icon-maskable-512.png and /dev/null differ
diff --git a/example/web/index.html b/example/web/index.html
deleted file mode 100644
index b6b9dd2..0000000
--- a/example/web/index.html
+++ /dev/null
@@ -1,104 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- example
-
-
-
-
-
-
-
diff --git a/example/web/manifest.json b/example/web/manifest.json
deleted file mode 100644
index 096edf8..0000000
--- a/example/web/manifest.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
- "name": "example",
- "short_name": "example",
- "start_url": ".",
- "display": "standalone",
- "background_color": "#0175C2",
- "theme_color": "#0175C2",
- "description": "A new Flutter project.",
- "orientation": "portrait-primary",
- "prefer_related_applications": false,
- "icons": [
- {
- "src": "icons/Icon-192.png",
- "sizes": "192x192",
- "type": "image/png"
- },
- {
- "src": "icons/Icon-512.png",
- "sizes": "512x512",
- "type": "image/png"
- },
- {
- "src": "icons/Icon-maskable-192.png",
- "sizes": "192x192",
- "type": "image/png",
- "purpose": "maskable"
- },
- {
- "src": "icons/Icon-maskable-512.png",
- "sizes": "512x512",
- "type": "image/png",
- "purpose": "maskable"
- }
- ]
-}
diff --git a/example/windows/.gitignore b/example/windows/.gitignore
deleted file mode 100644
index d492d0d..0000000
--- a/example/windows/.gitignore
+++ /dev/null
@@ -1,17 +0,0 @@
-flutter/ephemeral/
-
-# Visual Studio user-specific files.
-*.suo
-*.user
-*.userosscache
-*.sln.docstates
-
-# Visual Studio build-related files.
-x64/
-x86/
-
-# Visual Studio cache files
-# files ending in .cache can be ignored
-*.[Cc]ache
-# but keep track of directories ending in .cache
-!*.[Cc]ache/
diff --git a/example/windows/CMakeLists.txt b/example/windows/CMakeLists.txt
deleted file mode 100644
index 1633297..0000000
--- a/example/windows/CMakeLists.txt
+++ /dev/null
@@ -1,95 +0,0 @@
-cmake_minimum_required(VERSION 3.14)
-project(example LANGUAGES CXX)
-
-set(BINARY_NAME "example")
-
-cmake_policy(SET CMP0063 NEW)
-
-set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
-
-# Configure build options.
-get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
-if(IS_MULTICONFIG)
- set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
- CACHE STRING "" FORCE)
-else()
- if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
- set(CMAKE_BUILD_TYPE "Debug" CACHE
- STRING "Flutter build mode" FORCE)
- set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
- "Debug" "Profile" "Release")
- endif()
-endif()
-
-set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
-set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
-set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
-set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
-
-# Use Unicode for all projects.
-add_definitions(-DUNICODE -D_UNICODE)
-
-# Compilation settings that should be applied to most targets.
-function(APPLY_STANDARD_SETTINGS TARGET)
- target_compile_features(${TARGET} PUBLIC cxx_std_17)
- target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
- target_compile_options(${TARGET} PRIVATE /EHsc)
- target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
- target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>")
-endfunction()
-
-set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
-
-# Flutter library and tool build rules.
-add_subdirectory(${FLUTTER_MANAGED_DIR})
-
-# Application build
-add_subdirectory("runner")
-
-# Generated plugin build rules, which manage building the plugins and adding
-# them to the application.
-include(flutter/generated_plugins.cmake)
-
-
-# === Installation ===
-# Support files are copied into place next to the executable, so that it can
-# run in place. This is done instead of making a separate bundle (as on Linux)
-# so that building and running from within Visual Studio will work.
-set(BUILD_BUNDLE_DIR "$")
-# Make the "install" step default, as it's required to run.
-set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
-if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
- set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
-endif()
-
-set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
-set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
-
-install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
- COMPONENT Runtime)
-
-install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
- COMPONENT Runtime)
-
-install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
- COMPONENT Runtime)
-
-if(PLUGIN_BUNDLED_LIBRARIES)
- install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
- DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
- COMPONENT Runtime)
-endif()
-
-# Fully re-copy the assets directory on each build to avoid having stale files
-# from a previous install.
-set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
-install(CODE "
- file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
- " COMPONENT Runtime)
-install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
- DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
-
-# Install the AOT library on non-Debug builds only.
-install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
- CONFIGURATIONS Profile;Release
- COMPONENT Runtime)
diff --git a/example/windows/flutter/CMakeLists.txt b/example/windows/flutter/CMakeLists.txt
deleted file mode 100644
index b2e4bd8..0000000
--- a/example/windows/flutter/CMakeLists.txt
+++ /dev/null
@@ -1,103 +0,0 @@
-cmake_minimum_required(VERSION 3.14)
-
-set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
-
-# Configuration provided via flutter tool.
-include(${EPHEMERAL_DIR}/generated_config.cmake)
-
-# TODO: Move the rest of this into files in ephemeral. See
-# https://github.com/flutter/flutter/issues/57146.
-set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
-
-# === Flutter Library ===
-set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
-
-# Published to parent scope for install step.
-set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
-set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
-set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
-set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
-
-list(APPEND FLUTTER_LIBRARY_HEADERS
- "flutter_export.h"
- "flutter_windows.h"
- "flutter_messenger.h"
- "flutter_plugin_registrar.h"
- "flutter_texture_registrar.h"
-)
-list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
-add_library(flutter INTERFACE)
-target_include_directories(flutter INTERFACE
- "${EPHEMERAL_DIR}"
-)
-target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
-add_dependencies(flutter flutter_assemble)
-
-# === Wrapper ===
-list(APPEND CPP_WRAPPER_SOURCES_CORE
- "core_implementations.cc"
- "standard_codec.cc"
-)
-list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
-list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
- "plugin_registrar.cc"
-)
-list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
-list(APPEND CPP_WRAPPER_SOURCES_APP
- "flutter_engine.cc"
- "flutter_view_controller.cc"
-)
-list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
-
-# Wrapper sources needed for a plugin.
-add_library(flutter_wrapper_plugin STATIC
- ${CPP_WRAPPER_SOURCES_CORE}
- ${CPP_WRAPPER_SOURCES_PLUGIN}
-)
-apply_standard_settings(flutter_wrapper_plugin)
-set_target_properties(flutter_wrapper_plugin PROPERTIES
- POSITION_INDEPENDENT_CODE ON)
-set_target_properties(flutter_wrapper_plugin PROPERTIES
- CXX_VISIBILITY_PRESET hidden)
-target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
-target_include_directories(flutter_wrapper_plugin PUBLIC
- "${WRAPPER_ROOT}/include"
-)
-add_dependencies(flutter_wrapper_plugin flutter_assemble)
-
-# Wrapper sources needed for the runner.
-add_library(flutter_wrapper_app STATIC
- ${CPP_WRAPPER_SOURCES_CORE}
- ${CPP_WRAPPER_SOURCES_APP}
-)
-apply_standard_settings(flutter_wrapper_app)
-target_link_libraries(flutter_wrapper_app PUBLIC flutter)
-target_include_directories(flutter_wrapper_app PUBLIC
- "${WRAPPER_ROOT}/include"
-)
-add_dependencies(flutter_wrapper_app flutter_assemble)
-
-# === Flutter tool backend ===
-# _phony_ is a non-existent file to force this command to run every time,
-# since currently there's no way to get a full input/output list from the
-# flutter tool.
-set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
-set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
-add_custom_command(
- OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
- ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
- ${CPP_WRAPPER_SOURCES_APP}
- ${PHONY_OUTPUT}
- COMMAND ${CMAKE_COMMAND} -E env
- ${FLUTTER_TOOL_ENVIRONMENT}
- "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
- windows-x64 $
- VERBATIM
-)
-add_custom_target(flutter_assemble DEPENDS
- "${FLUTTER_LIBRARY}"
- ${FLUTTER_LIBRARY_HEADERS}
- ${CPP_WRAPPER_SOURCES_CORE}
- ${CPP_WRAPPER_SOURCES_PLUGIN}
- ${CPP_WRAPPER_SOURCES_APP}
-)
diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc
deleted file mode 100644
index 8b6d468..0000000
--- a/example/windows/flutter/generated_plugin_registrant.cc
+++ /dev/null
@@ -1,11 +0,0 @@
-//
-// Generated file. Do not edit.
-//
-
-// clang-format off
-
-#include "generated_plugin_registrant.h"
-
-
-void RegisterPlugins(flutter::PluginRegistry* registry) {
-}
diff --git a/example/windows/flutter/generated_plugin_registrant.h b/example/windows/flutter/generated_plugin_registrant.h
deleted file mode 100644
index dc139d8..0000000
--- a/example/windows/flutter/generated_plugin_registrant.h
+++ /dev/null
@@ -1,15 +0,0 @@
-//
-// Generated file. Do not edit.
-//
-
-// clang-format off
-
-#ifndef GENERATED_PLUGIN_REGISTRANT_
-#define GENERATED_PLUGIN_REGISTRANT_
-
-#include
-
-// Registers Flutter plugins.
-void RegisterPlugins(flutter::PluginRegistry* registry);
-
-#endif // GENERATED_PLUGIN_REGISTRANT_
diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake
deleted file mode 100644
index b93c4c3..0000000
--- a/example/windows/flutter/generated_plugins.cmake
+++ /dev/null
@@ -1,23 +0,0 @@
-#
-# Generated file, do not edit.
-#
-
-list(APPEND FLUTTER_PLUGIN_LIST
-)
-
-list(APPEND FLUTTER_FFI_PLUGIN_LIST
-)
-
-set(PLUGIN_BUNDLED_LIBRARIES)
-
-foreach(plugin ${FLUTTER_PLUGIN_LIST})
- add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
- target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
- list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
- list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
-endforeach(plugin)
-
-foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
- add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
- list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
-endforeach(ffi_plugin)
diff --git a/example/windows/runner/CMakeLists.txt b/example/windows/runner/CMakeLists.txt
deleted file mode 100644
index de2d891..0000000
--- a/example/windows/runner/CMakeLists.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-cmake_minimum_required(VERSION 3.14)
-project(runner LANGUAGES CXX)
-
-add_executable(${BINARY_NAME} WIN32
- "flutter_window.cpp"
- "main.cpp"
- "utils.cpp"
- "win32_window.cpp"
- "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
- "Runner.rc"
- "runner.exe.manifest"
-)
-apply_standard_settings(${BINARY_NAME})
-target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
-target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
-target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
-add_dependencies(${BINARY_NAME} flutter_assemble)
diff --git a/example/windows/runner/Runner.rc b/example/windows/runner/Runner.rc
deleted file mode 100644
index 15791b9..0000000
--- a/example/windows/runner/Runner.rc
+++ /dev/null
@@ -1,121 +0,0 @@
-// Microsoft Visual C++ generated resource script.
-//
-#pragma code_page(65001)
-#include "resource.h"
-
-#define APSTUDIO_READONLY_SYMBOLS
-/////////////////////////////////////////////////////////////////////////////
-//
-// Generated from the TEXTINCLUDE 2 resource.
-//
-#include "winres.h"
-
-/////////////////////////////////////////////////////////////////////////////
-#undef APSTUDIO_READONLY_SYMBOLS
-
-/////////////////////////////////////////////////////////////////////////////
-// English (United States) resources
-
-#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
-LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
-
-#ifdef APSTUDIO_INVOKED
-/////////////////////////////////////////////////////////////////////////////
-//
-// TEXTINCLUDE
-//
-
-1 TEXTINCLUDE
-BEGIN
- "resource.h\0"
-END
-
-2 TEXTINCLUDE
-BEGIN
- "#include ""winres.h""\r\n"
- "\0"
-END
-
-3 TEXTINCLUDE
-BEGIN
- "\r\n"
- "\0"
-END
-
-#endif // APSTUDIO_INVOKED
-
-
-/////////////////////////////////////////////////////////////////////////////
-//
-// Icon
-//
-
-// Icon with lowest ID value placed first to ensure application icon
-// remains consistent on all systems.
-IDI_APP_ICON ICON "resources\\app_icon.ico"
-
-
-/////////////////////////////////////////////////////////////////////////////
-//
-// Version
-//
-
-#ifdef FLUTTER_BUILD_NUMBER
-#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER
-#else
-#define VERSION_AS_NUMBER 1,0,0
-#endif
-
-#ifdef FLUTTER_BUILD_NAME
-#define VERSION_AS_STRING #FLUTTER_BUILD_NAME
-#else
-#define VERSION_AS_STRING "1.0.0"
-#endif
-
-VS_VERSION_INFO VERSIONINFO
- FILEVERSION VERSION_AS_NUMBER
- PRODUCTVERSION VERSION_AS_NUMBER
- FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
-#ifdef _DEBUG
- FILEFLAGS VS_FF_DEBUG
-#else
- FILEFLAGS 0x0L
-#endif
- FILEOS VOS__WINDOWS32
- FILETYPE VFT_APP
- FILESUBTYPE 0x0L
-BEGIN
- BLOCK "StringFileInfo"
- BEGIN
- BLOCK "040904e4"
- BEGIN
- VALUE "CompanyName", "com.example" "\0"
- VALUE "FileDescription", "A new Flutter project." "\0"
- VALUE "FileVersion", VERSION_AS_STRING "\0"
- VALUE "InternalName", "example" "\0"
- VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0"
- VALUE "OriginalFilename", "example.exe" "\0"
- VALUE "ProductName", "example" "\0"
- VALUE "ProductVersion", VERSION_AS_STRING "\0"
- END
- END
- BLOCK "VarFileInfo"
- BEGIN
- VALUE "Translation", 0x409, 1252
- END
-END
-
-#endif // English (United States) resources
-/////////////////////////////////////////////////////////////////////////////
-
-
-
-#ifndef APSTUDIO_INVOKED
-/////////////////////////////////////////////////////////////////////////////
-//
-// Generated from the TEXTINCLUDE 3 resource.
-//
-
-
-/////////////////////////////////////////////////////////////////////////////
-#endif // not APSTUDIO_INVOKED
diff --git a/example/windows/runner/flutter_window.cpp b/example/windows/runner/flutter_window.cpp
deleted file mode 100644
index b43b909..0000000
--- a/example/windows/runner/flutter_window.cpp
+++ /dev/null
@@ -1,61 +0,0 @@
-#include "flutter_window.h"
-
-#include
-
-#include "flutter/generated_plugin_registrant.h"
-
-FlutterWindow::FlutterWindow(const flutter::DartProject& project)
- : project_(project) {}
-
-FlutterWindow::~FlutterWindow() {}
-
-bool FlutterWindow::OnCreate() {
- if (!Win32Window::OnCreate()) {
- return false;
- }
-
- RECT frame = GetClientArea();
-
- // The size here must match the window dimensions to avoid unnecessary surface
- // creation / destruction in the startup path.
- flutter_controller_ = std::make_unique(
- frame.right - frame.left, frame.bottom - frame.top, project_);
- // Ensure that basic setup of the controller was successful.
- if (!flutter_controller_->engine() || !flutter_controller_->view()) {
- return false;
- }
- RegisterPlugins(flutter_controller_->engine());
- SetChildContent(flutter_controller_->view()->GetNativeWindow());
- return true;
-}
-
-void FlutterWindow::OnDestroy() {
- if (flutter_controller_) {
- flutter_controller_ = nullptr;
- }
-
- Win32Window::OnDestroy();
-}
-
-LRESULT
-FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
- WPARAM const wparam,
- LPARAM const lparam) noexcept {
- // Give Flutter, including plugins, an opportunity to handle window messages.
- if (flutter_controller_) {
- std::optional result =
- flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
- lparam);
- if (result) {
- return *result;
- }
- }
-
- switch (message) {
- case WM_FONTCHANGE:
- flutter_controller_->engine()->ReloadSystemFonts();
- break;
- }
-
- return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
-}
diff --git a/example/windows/runner/flutter_window.h b/example/windows/runner/flutter_window.h
deleted file mode 100644
index 6da0652..0000000
--- a/example/windows/runner/flutter_window.h
+++ /dev/null
@@ -1,33 +0,0 @@
-#ifndef RUNNER_FLUTTER_WINDOW_H_
-#define RUNNER_FLUTTER_WINDOW_H_
-
-#include
-#include
-
-#include
-
-#include "win32_window.h"
-
-// A window that does nothing but host a Flutter view.
-class FlutterWindow : public Win32Window {
- public:
- // Creates a new FlutterWindow hosting a Flutter view running |project|.
- explicit FlutterWindow(const flutter::DartProject& project);
- virtual ~FlutterWindow();
-
- protected:
- // Win32Window:
- bool OnCreate() override;
- void OnDestroy() override;
- LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
- LPARAM const lparam) noexcept override;
-
- private:
- // The project to run.
- flutter::DartProject project_;
-
- // The Flutter instance hosted by this window.
- std::unique_ptr flutter_controller_;
-};
-
-#endif // RUNNER_FLUTTER_WINDOW_H_
diff --git a/example/windows/runner/main.cpp b/example/windows/runner/main.cpp
deleted file mode 100644
index bcb57b0..0000000
--- a/example/windows/runner/main.cpp
+++ /dev/null
@@ -1,43 +0,0 @@
-#include
-#include
-#include
-
-#include "flutter_window.h"
-#include "utils.h"
-
-int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
- _In_ wchar_t *command_line, _In_ int show_command) {
- // Attach to console when present (e.g., 'flutter run') or create a
- // new console when running with a debugger.
- if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
- CreateAndAttachConsole();
- }
-
- // Initialize COM, so that it is available for use in the library and/or
- // plugins.
- ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
-
- flutter::DartProject project(L"data");
-
- std::vector command_line_arguments =
- GetCommandLineArguments();
-
- project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
-
- FlutterWindow window(project);
- Win32Window::Point origin(10, 10);
- Win32Window::Size size(1280, 720);
- if (!window.CreateAndShow(L"example", origin, size)) {
- return EXIT_FAILURE;
- }
- window.SetQuitOnClose(true);
-
- ::MSG msg;
- while (::GetMessage(&msg, nullptr, 0, 0)) {
- ::TranslateMessage(&msg);
- ::DispatchMessage(&msg);
- }
-
- ::CoUninitialize();
- return EXIT_SUCCESS;
-}
diff --git a/example/windows/runner/resource.h b/example/windows/runner/resource.h
deleted file mode 100644
index 66a65d1..0000000
--- a/example/windows/runner/resource.h
+++ /dev/null
@@ -1,16 +0,0 @@
-//{{NO_DEPENDENCIES}}
-// Microsoft Visual C++ generated include file.
-// Used by Runner.rc
-//
-#define IDI_APP_ICON 101
-
-// Next default values for new objects
-//
-#ifdef APSTUDIO_INVOKED
-#ifndef APSTUDIO_READONLY_SYMBOLS
-#define _APS_NEXT_RESOURCE_VALUE 102
-#define _APS_NEXT_COMMAND_VALUE 40001
-#define _APS_NEXT_CONTROL_VALUE 1001
-#define _APS_NEXT_SYMED_VALUE 101
-#endif
-#endif
diff --git a/example/windows/runner/resources/app_icon.ico b/example/windows/runner/resources/app_icon.ico
deleted file mode 100644
index c04e20c..0000000
Binary files a/example/windows/runner/resources/app_icon.ico and /dev/null differ
diff --git a/example/windows/runner/runner.exe.manifest b/example/windows/runner/runner.exe.manifest
deleted file mode 100644
index c977c4a..0000000
--- a/example/windows/runner/runner.exe.manifest
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
- PerMonitorV2
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/example/windows/runner/utils.cpp b/example/windows/runner/utils.cpp
deleted file mode 100644
index d19bdbb..0000000
--- a/example/windows/runner/utils.cpp
+++ /dev/null
@@ -1,64 +0,0 @@
-#include "utils.h"
-
-#include
-#include
-#include
-#include
-
-#include
-
-void CreateAndAttachConsole() {
- if (::AllocConsole()) {
- FILE *unused;
- if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
- _dup2(_fileno(stdout), 1);
- }
- if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
- _dup2(_fileno(stdout), 2);
- }
- std::ios::sync_with_stdio();
- FlutterDesktopResyncOutputStreams();
- }
-}
-
-std::vector GetCommandLineArguments() {
- // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
- int argc;
- wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
- if (argv == nullptr) {
- return std::vector();
- }
-
- std::vector command_line_arguments;
-
- // Skip the first argument as it's the binary name.
- for (int i = 1; i < argc; i++) {
- command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
- }
-
- ::LocalFree(argv);
-
- return command_line_arguments;
-}
-
-std::string Utf8FromUtf16(const wchar_t* utf16_string) {
- if (utf16_string == nullptr) {
- return std::string();
- }
- int target_length = ::WideCharToMultiByte(
- CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
- -1, nullptr, 0, nullptr, nullptr);
- if (target_length == 0) {
- return std::string();
- }
- std::string utf8_string;
- utf8_string.resize(target_length);
- int converted_length = ::WideCharToMultiByte(
- CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
- -1, utf8_string.data(),
- target_length, nullptr, nullptr);
- if (converted_length == 0) {
- return std::string();
- }
- return utf8_string;
-}
diff --git a/example/windows/runner/utils.h b/example/windows/runner/utils.h
deleted file mode 100644
index 3879d54..0000000
--- a/example/windows/runner/utils.h
+++ /dev/null
@@ -1,19 +0,0 @@
-#ifndef RUNNER_UTILS_H_
-#define RUNNER_UTILS_H_
-
-#include
-#include
-
-// Creates a console for the process, and redirects stdout and stderr to
-// it for both the runner and the Flutter library.
-void CreateAndAttachConsole();
-
-// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
-// encoded in UTF-8. Returns an empty std::string on failure.
-std::string Utf8FromUtf16(const wchar_t* utf16_string);
-
-// Gets the command line arguments passed in as a std::vector,
-// encoded in UTF-8. Returns an empty std::vector on failure.
-std::vector GetCommandLineArguments();
-
-#endif // RUNNER_UTILS_H_
diff --git a/example/windows/runner/win32_window.cpp b/example/windows/runner/win32_window.cpp
deleted file mode 100644
index c10f08d..0000000
--- a/example/windows/runner/win32_window.cpp
+++ /dev/null
@@ -1,245 +0,0 @@
-#include "win32_window.h"
-
-#include
-
-#include "resource.h"
-
-namespace {
-
-constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
-
-// The number of Win32Window objects that currently exist.
-static int g_active_window_count = 0;
-
-using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
-
-// Scale helper to convert logical scaler values to physical using passed in
-// scale factor
-int Scale(int source, double scale_factor) {
- return static_cast(source * scale_factor);
-}
-
-// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
-// This API is only needed for PerMonitor V1 awareness mode.
-void EnableFullDpiSupportIfAvailable(HWND hwnd) {
- HMODULE user32_module = LoadLibraryA("User32.dll");
- if (!user32_module) {
- return;
- }
- auto enable_non_client_dpi_scaling =
- reinterpret_cast(
- GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
- if (enable_non_client_dpi_scaling != nullptr) {
- enable_non_client_dpi_scaling(hwnd);
- FreeLibrary(user32_module);
- }
-}
-
-} // namespace
-
-// Manages the Win32Window's window class registration.
-class WindowClassRegistrar {
- public:
- ~WindowClassRegistrar() = default;
-
- // Returns the singleton registar instance.
- static WindowClassRegistrar* GetInstance() {
- if (!instance_) {
- instance_ = new WindowClassRegistrar();
- }
- return instance_;
- }
-
- // Returns the name of the window class, registering the class if it hasn't
- // previously been registered.
- const wchar_t* GetWindowClass();
-
- // Unregisters the window class. Should only be called if there are no
- // instances of the window.
- void UnregisterWindowClass();
-
- private:
- WindowClassRegistrar() = default;
-
- static WindowClassRegistrar* instance_;
-
- bool class_registered_ = false;
-};
-
-WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
-
-const wchar_t* WindowClassRegistrar::GetWindowClass() {
- if (!class_registered_) {
- WNDCLASS window_class{};
- window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
- window_class.lpszClassName = kWindowClassName;
- window_class.style = CS_HREDRAW | CS_VREDRAW;
- window_class.cbClsExtra = 0;
- window_class.cbWndExtra = 0;
- window_class.hInstance = GetModuleHandle(nullptr);
- window_class.hIcon =
- LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
- window_class.hbrBackground = 0;
- window_class.lpszMenuName = nullptr;
- window_class.lpfnWndProc = Win32Window::WndProc;
- RegisterClass(&window_class);
- class_registered_ = true;
- }
- return kWindowClassName;
-}
-
-void WindowClassRegistrar::UnregisterWindowClass() {
- UnregisterClass(kWindowClassName, nullptr);
- class_registered_ = false;
-}
-
-Win32Window::Win32Window() {
- ++g_active_window_count;
-}
-
-Win32Window::~Win32Window() {
- --g_active_window_count;
- Destroy();
-}
-
-bool Win32Window::CreateAndShow(const std::wstring& title,
- const Point& origin,
- const Size& size) {
- Destroy();
-
- const wchar_t* window_class =
- WindowClassRegistrar::GetInstance()->GetWindowClass();
-
- const POINT target_point = {static_cast(origin.x),
- static_cast(origin.y)};
- HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
- UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
- double scale_factor = dpi / 96.0;
-
- HWND window = CreateWindow(
- window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE,
- Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
- Scale(size.width, scale_factor), Scale(size.height, scale_factor),
- nullptr, nullptr, GetModuleHandle(nullptr), this);
-
- if (!window) {
- return false;
- }
-
- return OnCreate();
-}
-
-// static
-LRESULT CALLBACK Win32Window::WndProc(HWND const window,
- UINT const message,
- WPARAM const wparam,
- LPARAM const lparam) noexcept {
- if (message == WM_NCCREATE) {
- auto window_struct = reinterpret_cast(lparam);
- SetWindowLongPtr(window, GWLP_USERDATA,
- reinterpret_cast(window_struct->lpCreateParams));
-
- auto that = static_cast(window_struct->lpCreateParams);
- EnableFullDpiSupportIfAvailable(window);
- that->window_handle_ = window;
- } else if (Win32Window* that = GetThisFromHandle(window)) {
- return that->MessageHandler(window, message, wparam, lparam);
- }
-
- return DefWindowProc(window, message, wparam, lparam);
-}
-
-LRESULT
-Win32Window::MessageHandler(HWND hwnd,
- UINT const message,
- WPARAM const wparam,
- LPARAM const lparam) noexcept {
- switch (message) {
- case WM_DESTROY:
- window_handle_ = nullptr;
- Destroy();
- if (quit_on_close_) {
- PostQuitMessage(0);
- }
- return 0;
-
- case WM_DPICHANGED: {
- auto newRectSize = reinterpret_cast(lparam);
- LONG newWidth = newRectSize->right - newRectSize->left;
- LONG newHeight = newRectSize->bottom - newRectSize->top;
-
- SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
- newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
-
- return 0;
- }
- case WM_SIZE: {
- RECT rect = GetClientArea();
- if (child_content_ != nullptr) {
- // Size and position the child window.
- MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
- rect.bottom - rect.top, TRUE);
- }
- return 0;
- }
-
- case WM_ACTIVATE:
- if (child_content_ != nullptr) {
- SetFocus(child_content_);
- }
- return 0;
- }
-
- return DefWindowProc(window_handle_, message, wparam, lparam);
-}
-
-void Win32Window::Destroy() {
- OnDestroy();
-
- if (window_handle_) {
- DestroyWindow(window_handle_);
- window_handle_ = nullptr;
- }
- if (g_active_window_count == 0) {
- WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
- }
-}
-
-Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
- return reinterpret_cast(
- GetWindowLongPtr(window, GWLP_USERDATA));
-}
-
-void Win32Window::SetChildContent(HWND content) {
- child_content_ = content;
- SetParent(content, window_handle_);
- RECT frame = GetClientArea();
-
- MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
- frame.bottom - frame.top, true);
-
- SetFocus(child_content_);
-}
-
-RECT Win32Window::GetClientArea() {
- RECT frame;
- GetClientRect(window_handle_, &frame);
- return frame;
-}
-
-HWND Win32Window::GetHandle() {
- return window_handle_;
-}
-
-void Win32Window::SetQuitOnClose(bool quit_on_close) {
- quit_on_close_ = quit_on_close;
-}
-
-bool Win32Window::OnCreate() {
- // No-op; provided for subclasses.
- return true;
-}
-
-void Win32Window::OnDestroy() {
- // No-op; provided for subclasses.
-}
diff --git a/example/windows/runner/win32_window.h b/example/windows/runner/win32_window.h
deleted file mode 100644
index 17ba431..0000000
--- a/example/windows/runner/win32_window.h
+++ /dev/null
@@ -1,98 +0,0 @@
-#ifndef RUNNER_WIN32_WINDOW_H_
-#define RUNNER_WIN32_WINDOW_H_
-
-#include
-
-#include
-#include
-#include
-
-// A class abstraction for a high DPI-aware Win32 Window. Intended to be
-// inherited from by classes that wish to specialize with custom
-// rendering and input handling
-class Win32Window {
- public:
- struct Point {
- unsigned int x;
- unsigned int y;
- Point(unsigned int x, unsigned int y) : x(x), y(y) {}
- };
-
- struct Size {
- unsigned int width;
- unsigned int height;
- Size(unsigned int width, unsigned int height)
- : width(width), height(height) {}
- };
-
- Win32Window();
- virtual ~Win32Window();
-
- // Creates and shows a win32 window with |title| and position and size using
- // |origin| and |size|. New windows are created on the default monitor. Window
- // sizes are specified to the OS in physical pixels, hence to ensure a
- // consistent size to will treat the width height passed in to this function
- // as logical pixels and scale to appropriate for the default monitor. Returns
- // true if the window was created successfully.
- bool CreateAndShow(const std::wstring& title,
- const Point& origin,
- const Size& size);
-
- // Release OS resources associated with window.
- void Destroy();
-
- // Inserts |content| into the window tree.
- void SetChildContent(HWND content);
-
- // Returns the backing Window handle to enable clients to set icon and other
- // window properties. Returns nullptr if the window has been destroyed.
- HWND GetHandle();
-
- // If true, closing this window will quit the application.
- void SetQuitOnClose(bool quit_on_close);
-
- // Return a RECT representing the bounds of the current client area.
- RECT GetClientArea();
-
- protected:
- // Processes and route salient window messages for mouse handling,
- // size change and DPI. Delegates handling of these to member overloads that
- // inheriting classes can handle.
- virtual LRESULT MessageHandler(HWND window,
- UINT const message,
- WPARAM const wparam,
- LPARAM const lparam) noexcept;
-
- // Called when CreateAndShow is called, allowing subclass window-related
- // setup. Subclasses should return false if setup fails.
- virtual bool OnCreate();
-
- // Called when Destroy is called.
- virtual void OnDestroy();
-
- private:
- friend class WindowClassRegistrar;
-
- // OS callback called by message pump. Handles the WM_NCCREATE message which
- // is passed when the non-client area is being created and enables automatic
- // non-client DPI scaling so that the non-client area automatically
- // responsponds to changes in DPI. All other messages are handled by
- // MessageHandler.
- static LRESULT CALLBACK WndProc(HWND const window,
- UINT const message,
- WPARAM const wparam,
- LPARAM const lparam) noexcept;
-
- // Retrieves a class instance pointer for |window|
- static Win32Window* GetThisFromHandle(HWND const window) noexcept;
-
- bool quit_on_close_ = false;
-
- // window handle for top level window.
- HWND window_handle_ = nullptr;
-
- // window handle for hosted content.
- HWND child_content_ = nullptr;
-};
-
-#endif // RUNNER_WIN32_WINDOW_H_
diff --git a/generated-image.png b/generated-image.png
deleted file mode 100644
index f8156ec..0000000
Binary files a/generated-image.png and /dev/null differ
diff --git a/icon_loading_button.iml b/icon_loading_button.iml
deleted file mode 100644
index 8a97792..0000000
--- a/icon_loading_button.iml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/lib/loading_icon_button.dart b/lib/loading_icon_button.dart
index 54dda7f..74b07d0 100644
--- a/lib/loading_icon_button.dart
+++ b/lib/loading_icon_button.dart
@@ -1,13 +1,37 @@
-library loading_icon_button;
+/// Loading buttons for Flutter, plus a family of animated "thinking orb"
+/// indicators for AI and agent interfaces.
+///
+/// The package ships four button families:
+///
+/// * [LoadingButton] — a single widget with a built-in idle/loading/success/
+/// error state machine, drivable by hand or through a
+/// [LoadingButtonController].
+/// * `XxxAutoLoadingButton` ([ElevatedAutoLoadingButton] and friends) — thin
+/// wrappers over the Material buttons that enter the loading state for as
+/// long as an async callback runs.
+/// * `XxxLoadingButton` ([ElevatedLoadingButton] and friends) — the same
+/// widgets with the loading state driven by a plain `isLoading` flag.
+/// * [ArgonButton] and [ArgonTimerButton] — a button that collapses into its
+/// loader, and a countdown variant.
+///
+/// Any of them can show a [ThinkingOrb] instead of a spinner via
+/// [LoadingIndicator.orb].
+library;
-import 'dart:async' show Completer, Timer;
+import 'dart:async' show Completer, Timer, unawaited;
import 'dart:math' show min;
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart' show AsyncCallback;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show HapticFeedback;
-import 'package:rxdart/rxdart.dart' show BehaviorSubject;
+
+import 'src/orbs/orb_presets.dart';
+import 'src/orbs/thinking_orb.dart';
+
+export 'src/orbs/orb_presets.dart' show OrbState, kOrbTierBreakpoint;
+export 'src/orbs/thinking_orb.dart'
+ show ThinkingOrb, OrbTheme, kOrbSemanticLabels;
part 'src/_common_components.dart';
part 'src/argon_button.dart';
@@ -20,5 +44,10 @@ part 'src/material/filled_loading_button.dart';
part 'src/material/icon_loading_button.dart';
part 'src/material/outlined_loading_button.dart';
part 'src/material/text_loading_button.dart';
+part 'src/models/loading_button_colors.dart';
part 'src/models/loading_button_config.dart';
+part 'src/models/loading_button_controller.dart';
+part 'src/models/loading_button_indicator.dart';
+part 'src/models/loading_button_sizing.dart';
part 'src/models/loading_button_style.dart';
+part 'src/models/loading_button_theme.dart';
diff --git a/lib/src/_common_components.dart b/lib/src/_common_components.dart
index 190b767..c2def80 100644
--- a/lib/src/_common_components.dart
+++ b/lib/src/_common_components.dart
@@ -12,34 +12,10 @@ abstract class AutoLoadingButtonState
/// Return the future of the original onPressed event completion.
/// If the widget does not pass the original onPressed event, nothing will happen,
/// If the widget is in the loading state, the future of the current loading event will be returned, and the event will not be triggered repeatedly
- Future doPress() {
- if (_onPressedFuture != null) {
- return _onPressedFuture!;
- }
-
- final onPressed = _onPressed;
-
- if (onPressed == null) {
- return Future.value();
- }
-
- final completer = Completer();
-
- setState(() {
- _onPressedFuture = completer.future;
- });
-
- onPressed().then((value) {
- _onPressedFuture = null;
- completer.complete();
-
- if (mounted) {
- setState(() {});
- }
- });
-
- return completer.future;
- }
+ Future doPress() =>
+ _run(() => _onPressed, () => _onPressedFuture, (Future? f) {
+ _onPressedFuture = f;
+ });
/// Trigger the execution of the widget's onLongPress event
///
@@ -48,33 +24,64 @@ abstract class AutoLoadingButtonState
/// Return the future of the original onLongPress event completion.
/// If the widget does not pass the original onLongPress event, nothing will happen,
/// If the widget is in the loading state, the future of the current loading event will be returned, and the event will not be triggered repeatedly
- Future doLongPress() {
- if (_onLongPressFuture != null) {
- return _onLongPressFuture!;
- }
-
- final onPressed = _onLongPress;
+ Future doLongPress() =>
+ _run(() => _onLongPress, () => _onLongPressFuture, (Future