diff --git a/.cursorindexingignore b/.cursorindexingignore
deleted file mode 100644
index 953908e..0000000
--- a/.cursorindexingignore
+++ /dev/null
@@ -1,3 +0,0 @@
-
-# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
-.specstory/**
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index ffb400a..38f87b6 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -9,4 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
-custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
\ No newline at end of file
+custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/.github/cliff.toml b/.github/cliff.toml
new file mode 100644
index 0000000..a2ed6c4
--- /dev/null
+++ b/.github/cliff.toml
@@ -0,0 +1,74 @@
+# git-cliff configuration file for flutter_use
+# https://git-cliff.org/docs/configuration
+
+[changelog]
+# changelog header
+header = """
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+"""
+# template for the changelog body
+# https://keats.github.io/tera/docs/
+body = """
+{% if version %}\
+ ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
+{% else %}\
+ ## [unreleased]
+{% endif %}\
+{% for group, commits in commits | group_by(attribute="group") %}
+ ### {{ group | upper_first }}
+ {% for commit in commits %}
+ - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/wasabeef/flutter_use/commit/{{ commit.id }}))
+ {%- endfor %}
+{% endfor %}\n
+"""
+# remove the leading and trailing whitespace from the template
+trim = true
+# changelog footer
+footer = """
+
+"""
+
+[git]
+# parse the commits based on https://www.conventionalcommits.org
+conventional_commits = true
+# filter out the commits that are not conventional
+filter_unconventional = true
+# process each line of a commit as an individual commit
+split_commits = false
+# regex for preprocessing the commit messages
+commit_preprocessors = [
+ { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/wasabeef/flutter_use/issues/${2}))"},
+]
+# regex for parsing and grouping commits
+commit_parsers = [
+ { message = "^feat", group = "Features"},
+ { message = "^fix", group = "Bug Fixes"},
+ { message = "^docs", group = "Documentation"},
+ { message = "^perf", group = "Performance"},
+ { message = "^refactor", group = "Refactor"},
+ { message = "^style", group = "Styling"},
+ { message = "^test", group = "Testing"},
+ { message = "^chore\\(release\\): prepare for", skip = true},
+ { message = "^chore", group = "Miscellaneous Tasks"},
+ { body = ".*security", group = "Security"},
+ { message = "^upgrade", group = "Dependencies"},
+ { message = "^add", group = "Features"},
+ { message = "^update", group = "Improvements"},
+]
+# protect breaking changes from being skipped due to matching a skipping commit_parser
+protect_breaking_commits = false
+# filter out the commits that are not matched by commit parsers
+filter_commits = false
+# glob pattern for matching git tags
+tag_pattern = "v[0-9]*"
+# regex for skipping tags
+skip_tags = ""
+# regex for ignoring tags
+ignore_tags = ""
+# sort the tags topologically
+topo_order = false
+# sort the commits inside sections by oldest/newest order
+sort_commits = "oldest"
\ No newline at end of file
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 5428807..e5f9f1b 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -12,7 +12,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: [ubuntu-latest]
+ os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
@@ -30,11 +30,11 @@ jobs:
dart pub global activate melos
melos run get
- - name: Run tests for our dart project.
+ - name: Run tests with coverage
run: |
- melos run test
+ melos run test-coverage
- name: Check for any formatting and statically analyze the code.
run: |
- melos run format
+ melos run check
melos run analyze
diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml
new file mode 100644
index 0000000..47fa65d
--- /dev/null
+++ b/.github/workflows/deploy-demo.yml
@@ -0,0 +1,70 @@
+name: Deploy Flutter Demo to GitHub Pages
+
+on:
+ push:
+ branches: [ main ]
+ paths:
+ - 'demo/**'
+ - 'packages/**'
+ - '.github/workflows/deploy-demo.yml'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: "pages"
+ cancel-in-progress: false
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Import tool versions
+ uses: wasabeef/import-asdf-tool-versions-action@v1.1.0
+ id: asdf
+
+ - name: Setup Flutter
+ uses: subosito/flutter-action@v2
+ with:
+ flutter-version: ${{ steps.asdf.outputs.flutter }}
+ cache: true
+
+ - name: Setup Dart
+ uses: dart-lang/setup-dart@v1
+ with:
+ sdk: ${{ steps.asdf.outputs.dart }}
+
+ - name: Install Melos
+ run: dart pub global activate melos
+
+ - name: Bootstrap packages
+ run: melos bootstrap
+
+ - name: Build demo app
+ run: melos run demo-build
+
+ - name: Setup Pages
+ uses: actions/configure-pages@v4
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: 'demo/build/web'
+
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
new file mode 100644
index 0000000..747c6bf
--- /dev/null
+++ b/.github/workflows/release.yaml
@@ -0,0 +1,72 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - "v[0-9]+.[0-9]+.[0-9]+*"
+
+permissions:
+ contents: write
+ id-token: write # Required for OIDC authentication
+
+jobs:
+ release:
+ name: Release and Publish
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup Flutter and Dart
+ uses: subosito/flutter-action@v2
+ with:
+ channel: stable
+
+ - name: Setup Dart
+ uses: dart-lang/setup-dart@v1
+ with:
+ sdk: stable
+
+ - name: Set environment
+ run: echo "$HOME/.pub-cache/bin" >> "$GITHUB_PATH"
+
+ - name: Install Melos
+ run: dart pub global activate melos
+
+ - name: Bootstrap packages
+ run: melos bootstrap
+
+ - name: Verify all tests pass
+ run: melos run test
+
+ - name: Verify code formatting
+ run: melos run check
+
+ - name: Verify static analysis
+ run: melos run analyze
+
+ - name: Check packages can be published (dry-run)
+ run: melos publish --dry-run
+
+ - name: Generate release notes
+ id: release_notes
+ uses: orhun/git-cliff-action@v3
+ with:
+ config: .github/cliff.toml
+ args: --latest --strip header
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: ${{ github.ref_name }}
+ name: ${{ github.ref_name }}
+ body: ${{ steps.release_notes.outputs.content }}
+ draft: false
+ prerelease: false
+
+ - name: Publish to pub.dev
+ run: melos publish --no-dry-run --yes
diff --git a/.gitignore b/.gitignore
index fb1f215..4d450e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -90,3 +90,11 @@ node_modules/
# SpecStory
.specstory/
+# Coverage reports
+coverage/
+*.lcov
+
+# Lefthook
+.lefthook/
+.lefthook-local/
+
diff --git a/.husky/pre-commit b/.husky/pre-commit
deleted file mode 100755
index 9fc1a6a..0000000
--- a/.husky/pre-commit
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/sh
-. "$(dirname "$0")/_/husky.sh"
-
-bun lint-staged --allow-empty --max-arg-length 1
\ No newline at end of file
diff --git a/.prettierignore b/.prettierignore
index 25368dd..dae61bb 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,5 +1,28 @@
-*.md
+# Flutter/Dart
.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub/
build/
-ios/
-android/
+coverage/
+
+# IDE
+.idea/
+.vscode/
+
+# OS
+.DS_Store
+
+# Dependencies
+node_modules/
+
+# Lefthook
+.lefthook/
+.lefthook-local/
+
+# Generated files
+**/*.g.dart
+**/*.freezed.dart
+**/*.reflectable.dart
+**/generated_plugin_registrant.dart
\ No newline at end of file
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..df354b5
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,9 @@
+{
+ "semi": true,
+ "singleQuote": true,
+ "tabWidth": 2,
+ "trailingComma": "es5",
+ "printWidth": 100,
+ "arrowParens": "always",
+ "endOfLine": "lf"
+}
\ No newline at end of file
diff --git a/.specstory/.gitignore b/.specstory/.gitignore
deleted file mode 100644
index 53b537f..0000000
--- a/.specstory/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# SpecStory explanation file
-/.what-is-this.md
diff --git a/.tool-versions b/.tool-versions
index 8c2122b..d95ac71 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,2 +1,4 @@
-nodejs 23.11.0
-bun 1.2.10
+nodejs 24.2.0
+bun 1.2.16
+flutter 3.32.1
+dart 3.8.1
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 9e657e4..0b806a9 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -1,50 +1,40 @@
{
- "version": "0.2.0",
- "configurations": [
- {
- "name": "Run Basic example",
- "program": "${workspaceFolder}/packages/basic/example/lib/main.dart",
- "request": "launch",
- "type": "dart",
- "args": [
- "--debug"
- ]
- },
- {
- "name": "Run Geolocation example",
- "program": "${workspaceFolder}/packages/geolocation/example/lib/main.dart",
- "request": "launch",
- "type": "dart",
- "args": [
- "--debug"
- ]
- },
- {
- "name": "Run Network example",
- "program": "${workspaceFolder}/packages/network/example/lib/main.dart",
- "request": "launch",
- "type": "dart",
- "args": [
- "--debug"
- ]
- },
- {
- "name": "Run Battery example",
- "program": "${workspaceFolder}/packages/battery/example/lib/main.dart",
- "request": "launch",
- "type": "dart",
- "args": [
- "--debug"
- ]
- },
- {
- "name": "Run Sensors example",
- "program": "${workspaceFolder}/packages/sensors/example/lib/main.dart",
- "request": "launch",
- "type": "dart",
- "args": [
- "--debug"
- ]
- },
- ]
-}
\ No newline at end of file
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Run Basic example",
+ "program": "${workspaceFolder}/packages/basic/example/lib/main.dart",
+ "request": "launch",
+ "type": "dart",
+ "args": ["--debug"]
+ },
+ {
+ "name": "Run Geolocation example",
+ "program": "${workspaceFolder}/packages/geolocation/example/lib/main.dart",
+ "request": "launch",
+ "type": "dart",
+ "args": ["--debug"]
+ },
+ {
+ "name": "Run Network example",
+ "program": "${workspaceFolder}/packages/network/example/lib/main.dart",
+ "request": "launch",
+ "type": "dart",
+ "args": ["--debug"]
+ },
+ {
+ "name": "Run Battery example",
+ "program": "${workspaceFolder}/packages/battery/example/lib/main.dart",
+ "request": "launch",
+ "type": "dart",
+ "args": ["--debug"]
+ },
+ {
+ "name": "Run Sensors example",
+ "program": "${workspaceFolder}/packages/sensors/example/lib/main.dart",
+ "request": "launch",
+ "type": "dart",
+ "args": ["--debug"]
+ }
+ ]
+}
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..9d507a8
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,91 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Repository Overview
+
+This is a monorepo for `flutter_use` - a collection of Flutter Hooks inspired by React's `react-use` library. The project uses Melos for managing multiple packages within a single repository.
+
+## Package Structure
+
+- `/packages/basic/` - Core hooks library (`flutter_use`)
+- `/packages/audio/` - Audio hooks (`flutter_use_audio`)
+- `/packages/battery/` - Battery state hooks (`flutter_use_battery`)
+- `/packages/geolocation/` - Geolocation hooks (`flutter_use_geolocation`)
+- `/packages/network/` - Network state hooks (`flutter_use_network_state`)
+- `/packages/sensors/` - Device sensors hooks (`flutter_use_sensors`)
+- `/packages/video/` - Video player hooks (`flutter_use_video`)
+
+## Essential Commands
+
+### Monorepo Management (Melos)
+```bash
+# Install dependencies for all packages
+melos get
+
+# Upgrade dependencies across all packages
+melos upgrade
+
+# Run static analysis on all packages
+melos analyze
+
+# Format all Dart code
+melos format
+
+# Run tests (only available for flutter_use package)
+melos test
+```
+
+### Per-Package Development
+```bash
+# Navigate to specific package first
+cd packages/basic # or any other package
+
+# Standard Flutter commands
+flutter pub get
+flutter test
+flutter analyze
+dart format .
+```
+
+### Running Example Apps
+```bash
+# Each package has an example directory
+cd packages/basic/example
+flutter run
+```
+
+## Architecture Patterns
+
+### Hook Development Pattern
+All hooks follow a consistent pattern:
+1. Located in `packages/[package_name]/lib/src/` as individual files
+2. Return specialized action classes that encapsulate:
+ - Getters for current state
+ - Methods for state manipulation
+ - Additional utility methods
+3. Main export file aggregates all hooks
+
+### Testing Pattern
+- Uses custom `buildHook` and `act` utilities (see `packages/basic/test/testing/hook_testing.dart`)
+- Test files mirror source structure
+- Each hook has comprehensive test coverage
+
+### Package Dependencies
+- Core package (`flutter_use`) has minimal dependencies
+- Other packages require specific plugins (e.g., `battery_plus`, `geolocator`)
+- Dependencies are clearly marked in README with badge indicators
+
+## Development Workflow
+
+1. Pre-commit hooks automatically format Dart code via Husky and lint-staged
+2. All packages support Dart SDK `>=2.17.0 <4.0.0` and Flutter `>=3.0.0`
+3. Each package is independently versioned and published to pub.dev
+
+## Creating New Hooks
+
+1. Add hook file to `packages/[package_name]/lib/src/`
+2. Follow existing naming convention: `use_[hook_name].dart`
+3. Export from main library file
+4. Add comprehensive tests using the hook testing utilities
+5. Add documentation in `/docs/` with DartPad example link if applicable
\ No newline at end of file
diff --git a/README.md b/README.md
index bf0e3b7..e4d1fc0 100644
--- a/README.md
+++ b/README.md
@@ -16,82 +16,150 @@
-
+A collection of Flutter Hooks inspired by React's `react-use` library. This monorepo contains multiple packages providing different categories of hooks for Flutter development.
+
+## 📦 Packages
+
+| Package | Description | Version |
+| ------------------------------------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
+| **[`flutter_use`](./packages/basic)** | Core hooks library with essential utilities | [](https://pub.dev/packages/flutter_use) |
+| **[`flutter_use_audio`](./packages/audio)** | Audio playback and control hooks | [](https://pub.dev/packages/flutter_use_audio) |
+| **[`flutter_use_battery`](./packages/battery)** | Battery state monitoring hooks | [](https://pub.dev/packages/flutter_use_battery) |
+| **[`flutter_use_geolocation`](./packages/geolocation)** | Location and permission hooks | [](https://pub.dev/packages/flutter_use_geolocation) |
+| **[`flutter_use_network_state`](./packages/network)** | Network connectivity hooks | [](https://pub.dev/packages/flutter_use_network_state) |
+| **[`flutter_use_sensors`](./packages/sensors)** | Device sensors hooks | [](https://pub.dev/packages/flutter_use_sensors) |
+| **[`flutter_use_video`](./packages/video)** | Video playbook hooks | [](https://pub.dev/packages/flutter_use_video) |
+
+## 🚀 Installation
+
+For the core package:
+
+```bash
+flutter pub add flutter_use
+```
+
+For specialized packages:
+
+```bash
+flutter pub add flutter_use_audio # Audio hooks
+flutter pub add flutter_use_battery # Battery hooks
+# ... and so on
+```
+
+## 🌐 Interactive Demo Site
+
+Try out all hooks with live examples at: **[https://wasabeef.github.io/flutter_use/](https://wasabeef.github.io/flutter_use/)**
+
+## 📚 Hooks by Category
+
+### 🎭 State Management
+
+_Core package: `flutter_use`_
+
+- [`useToggle` and `useBoolean`](./docs/useToggle.md) — tracks state of a boolean.
+- [`useCounter` and `useNumber`](./docs/useCounter.md) — tracks state of a number.
+- [`useList`](./docs/useList.md) — tracks state of an array.
+- [`useMap`](./docs/useMap.md) — tracks state of a map.
+- [`useSet`](./docs/useSet.md) — tracks state of a Set.
+- [`useStateList`](./docs/useStateList.md) — circularly iterates over an array.
+- [`useDefault`](./docs/useDefault.md) — returns the default value when state is `null`.
+- [`useLatest`](./docs/useLatest.md) — returns the latest state or props.
+- [`usePreviousDistinct`](./docs/usePreviousDistinct.md) — like [`usePrevious`](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/usePrevious.html) but with a predicate to determine if `previous` should update.
+- [`useTextFormValidator`](./docs/useTextFormValidator.md) — reactive form validation with real-time feedback.
+
+### ⏱️ Timing & Animation
+
+_Core package: `flutter_use`_
+
+- [`useInterval`](./docs/useInterval.md) — re-builds component on a set interval using [`Timer.periodic`](https://api.dart.dev/stable/2.14.4/dart-async/Timer/Timer.periodic.html).
+- [`useTimeout`](./docs/useTimeout.md) — re-builds component after a timeout.
+- [`useTimeoutFn`](./docs/useTimeoutFn.md) — calls given function after a timeout.
+- [`useUpdate`](./docs/useUpdate.md) — returns a callback, which re-builds component when called.
+
+### 🔄 Side Effects & Performance
+
+_Core package: `flutter_use`_
+
+- [`useFutureRetry`](./docs/useFutureRetry.md) — [`useFuture`](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html) with an additional retry method.
+- [`useDebounce`](./docs/useDebounce.md) — debounces a function.
+- [`useThrottle`](./docs/useThrottle.md) — throttles a value to update at most once per duration.
+- [`useThrottleFn`](./docs/useThrottleFn.md) — throttles a function to execute at most once per duration.
+- [`useError`](./docs/useError.md) — error dispatcher.
+- [`useException`](./docs/useException.md) — exception dispatcher.
+
+### 🎯 UI Interactions
+
+_Core package: `flutter_use`_
+
+- [`useScroll`](./docs/useScroll.md) — tracks a widget's scroll position.
+- [`useScrolling`](./docs/useScrolling.md) — tracks whether widget is scrolling.
+- [`useClickAway`](./docs/useClickAway.md) — triggers callback when user clicks outside target area.
+- [`useCopyToClipboard`](./docs/useCopyToClipboard.md) — copies text to clipboard.
+
+### ♻️ Lifecycle Management
+
+_Core package: `flutter_use`_
+
+- [`useEffectOnce`](./docs/useEffectOnce.md) — a modified [`useEffect`](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) hook that only runs once.
+- [`useLifecycles`](./docs/useLifecycles.md) — calls `mount` and `unmount` callbacks.
+- [`useMount`](./docs/useMount.md) — calls `mount` callbacks.
+- [`useUnmount`](./docs/useUnmount.md) — calls `unmount` callbacks.
+- [`useUpdateEffect`](./docs/useUpdateEffect.md) — run an `effect` only on updates.
+- [`useCustomCompareEffect`](./docs/useCustomCompareEffect.md) — run an `effect` depending on deep comparison of its dependencies.
+- [`useFirstMountState`](./docs/useFirstMountState.md) — check if current build is first.
+- [`useBuildsCount`](./docs/useBuildsCount.md) — count component builds.
+
+### 🎨 Development & Debugging
+
+_Core package: `flutter_use`_
+
+- [`useLogger`](./docs/useLogger.md) — logs in console as component goes through life-cycles.
+
+### 📱 Device Sensors
+
+_Package: `flutter_use_sensors`_
+
+- [`useAccelerometer`](./docs/useAccelerometer.md), [`useUserAccelerometer`](./docs/useUserAccelerometer.md), [`useGyroscope`](./docs/useGyroscope.md), and [`useMagnetometer`](./docs/useMagnetometer.md) — tracks accelerometer, gyroscope, and magnetometer sensors. [](https://pub.dev/packages/sensors_plus)
+
+_Core package: `flutter_use`_
+
+- [`useOrientation`](./docs/useOrientation.md) — tracks state of device's screen orientation.
+- [`useOrientationFn`](./docs/useOrientationFn.md) — calls given function when screen orientation changes.
+
+### 🔋 Device Information
+
+_Package: `flutter_use_battery`_
+
+- [`useBattery`](./docs/useBattery.md) — tracks device battery state. [](https://pub.dev/packages/battery_plus)
+
+_Package: `flutter_use_geolocation`_
+
+- [`useGeolocation`](./docs/useGeolocation.md) — tracks geo location and permission state of user's device. [](https://pub.dev/packages/geolocator)
+
+_Package: `flutter_use_network_state`_
+
+- [`useNetworkState`](./docs/useNetworkState.md) — tracks the state of apps network connection. [](https://pub.dev/packages/connectivity_plus)
+
+### 🎵 Media
+
+_Package: `flutter_use_audio`_
+
+- [`useAudio`](./docs/useAudio.md) — plays audio and exposes its controls. [](https://pub.dev/packages/just_audio)
+
+_Package: `flutter_use_video`_
+
+- [`useAssetVideo`](./docs/useAssetVideo.md) and [`useNetworkVideo`](./docs/useNetworkVideo.md) — plays video, tracks its state, and exposes playback controls. [](https://pub.dev/packages/video_player)
+
+## 🚧 Coming Soon
-- **Sensors**
- - [`useBattery`](./docs/useBattery.md) — tracks device battery state. [](https://pub.dev/packages/battery_plus)
- - [`useGeolocation`](./docs/useGeolocation.md) — tracks geo location and permission state of user's device. [](https://pub.dev/packages/geolocator)
- - [`useNetworkState`](./docs/useNetworkState.md) — tracks the state of apps network connection. [](https://pub.dev/packages/connectivity_plus)
- - [`useAccelerometer`](./docs/useAccelerometer.md), [`useUserAccelerometer`](./docs/useUserAccelerometer.md), [`useGyroscope`](./docs/useGyroscope.md), and [`useMagnetometer`](./docs/useMagnetometer.md) — tracks accelerometer, gyroscope, and magnetometer sensors state of user's device. [](https://pub.dev/packages/sensors_plus)
- - [`useOrientation`](./docs/useOrientation.md) — tracks state of device's screen orientation.
- - [`useOrientationFn`](./docs/useOrientationFn.md) — calls given function changed screen orientation of user's device.
-
-
-- **UI**
- - [`useAudio`](./docs/useAudio.md) — plays audio and exposes its controls. [](https://pub.dev/packages/just_audio)
- - [`useAssetVideo`](./docs/useAssetVideo.md) and [`useNetworkVideo`](./docs/useNetworkVideo.md) — plays video, tracks its state, and exposes playback controls. [](https://pub.dev/packages/video_player)
-
-
-- **Animations**
- - [`useInterval`](./docs/useInterval.md) — re-builds component on a set interval using [`Timer.periodic`](https://api.dart.dev/stable/2.14.4/dart-async/Timer/Timer.periodic.html). [![][img-demo]](https://dartpad.dev/?id=d4ce8c315a0157ad18257886d661c8b9&null_safety=true)
- - [`useTimeout`](./docs/useTimeout.md) — re-builds component after a timeout. [![][img-demo]](https://dartpad.dev/?id=e1cb8d7045982ec96b0b314e9fb58202&null_safety=true)
- - [`useTimeoutFn`](./docs/useTimeoutFn.md) — calls given function after a timeout. [![][img-demo]](https://dartpad.dev/?id=12449436914e1dec13c8f9c5cf63935b&null_safety=true)
- - [`useUpdate`](./docs/useUpdate.md) — returns a callback, which re-builds component when called. [![][img-demo]](https://dartpad.dev/?id=27a74d481219749f532776a8e73f3464&null_safety=true)
-
-
-- **Side-effects**
- - [`useFutureRetry`](./docs/useFutureRetry.md) — [`useFuture`](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html) with an additional retry method. [![][img-demo]](https://dartpad.dev/?id=ab910cc4170f5e8746229cc958ba845c&null_safety=true)
- - [`useDebounce`](./docs/useDebounce.md) — debounces a function. [![][img-demo]](https://dartpad.dev/?id=977ee00fc30da8f0dd1888f6808114eb&null_safety=true)
- - [`useError`](./docs/useError.md) — error dispatcher. [![][img-demo]](https://dartpad.dev/?id=8e8e4876d546dd38517cb833ee694359&null_safety=true)
- - [`useException`](./docs/useException.md) — exception dispatcher. [![][img-demo]](https://dartpad.dev/?id=98580d1987dcae38ea0f27ee67a0d089&null_safety=true)
-
-
-- **Lifecycles**
- - [`useEffectOnce`](./docs/useEffectOnce.md) — a modified [`useEffect`](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html) hook that only runs once. [![][img-demo]](https://dartpad.dev/?id=adec4d3a92f52bc8a40dc55ff330d2ab&null_safety=true)
- - [`useLifecycles`](./docs/useLifecycles.md) — calls `mount` and `unmount` callbacks.
- - [`useLogger`](./docs/useLogger.md) — logs in console as component goes through life-cycles. [![][img-demo]](https://dartpad.dev/?id=c72c9ab0fa46f93dd266f6557a29a3ed&null_safety=true)
- - [`useMount`](./docs/useMount.md) — calls `mount` callbacks. [![][img-demo]](https://dartpad.dev/?id=aa25e9bc3913779fcc795bef2bdc8d39&null_safety=true)
- - [`useUnmount`](./docs/useUnmount.md) — calls `unmount` callbacks. [![][img-demo]](https://dartpad.dev/?id=aa25e9bc3913779fcc795bef2bdc8d39&null_safety=true)
- - [`useUpdateEffect`](./docs/useUpdateEffect.md) — run an `effect` only on updates. [![][img-demo]](https://dartpad.dev/?id=724fee007fe78419fde61f185b83095b&null_safety=true)
- - [`useCustomCompareEffect`](./docs/useCustomCompareEffect.md) — run an `effect` depending on deep comparison of its dependencies. [![][img-demo]](https://dartpad.dev/?id=27146b5ca9189664e39ad4dfe9b08abe&null_safety=true)
-
-
-- **State**
- - [`useDefault`](./docs/useDefault.md) — returns the default value when state is `null`. [![][img-demo]](https://dartpad.dev/?id=6511219165b2e5c64ec8890b69633da6&null_safety=true)
- - [`useLatest`](./docs/useLatest.md) — returns the latest state or props. [![][img-demo]](https://dartpad.dev/?id=2a76f5b16c2f27d11c023a140f38ce33&null_safety=true)
- - [`usePreviousDistinct`](./docs/usePreviousDistinct.md) — like [`usePrevious`](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/usePrevious.html) but with a predicate to determine if `previous` should update. [![][img-demo]](https://dartpad.dev/?id=86e0e29f8198095dbd0d68a736c671bb&null_safety=true)
- - [`useStateList`](./docs/useStateList.md) — circularly iterates over an array. [![][img-demo]](https://dartpad.dev/?id=5761442418062838b04cbe21a36be586&null_safety=true)
- - [`useToggle` and `useBoolean`](./docs/useToggle.md) — tracks state of a boolean. [![][img-demo]](https://dartpad.dev/?id=7e070264db2566b3c990c403dd61c3ff&null_safety=true)
- - [`useCounter` and `useNumber`](./docs/useCounter.md) — tracks state of a number. [![][img-demo]](https://dartpad.dev/?id=5ee82acd2f1947b2d0ca02da4ab327b8&null_safety=true)
- - [`useList`](./docs/useList.md) — tracks state of an array. [![][img-demo]](https://dartpad.dev/?id=e04b584b8ab67492a1024ea7dd9adcbb&null_safety=true)
- - [`useMap`](./docs/useMap.md) — tracks state of a map. [![][img-demo]](https://dartpad.dev/?id=325b4737e78d40463fc0f3d3cc317b35&null_safety=true)
- - [`useSet`](./docs/useSet.md) — tracks state of a Set. [![][img-demo]](https://dartpad.dev/?id=3d1199828a54b19c526a26a6c0021293&null_safety=true)
- - [`useTextFormValidator`](./docs/useTextFormValidator.md) — tracks state of an object. [![][img-demo]](https://dartpad.dev/?id=23dee1c153a8a9e455d463584537256e&null_safety=true)
- - [`useFirstMountState`](./docs/useFirstMountState.md) — check if current build is first. [![][img-demo]](https://dartpad.dev/?id=c9b6853d726ae29dcf902efcf7e85dc6&null_safety=true)
- - [`useBuildsCount`](./docs/useBuildsCount.md) — count component builds. [![][img-demo]](https://dartpad.dev/?id=d54979d95910abd48054547202e20c12&null_safety=true)
-
-
-- TBD
-
- - `useCopyToClipboard` — copies text to clipboard.
- - `useEvent` — subscribe to events.
- - `useScroll` — tracks a widget's scroll position.
- - `useScrolling` — tracks whether widget is scrolling.
- - `useFullscreen` — display an element or video full-screen.
- - `useClickAway`— triggers callback when user clicks outside target area.
- - `usePageLeave` — triggers when mouse leaves page boundaries.
- - `usePermission` — query permission status for apps APIs.
- - `useMethods` — neat alternative to `useReducer`.
- - `useSetState` — creates `setState` method which works like `this.setState`.
- - `usePromise` — resolves promise only while component is mounted.
- - `useObservable` — tracks latest value of an `Observable`.
- - `useThrottle` and `useThrottleFn` — throttles a function.
-
-
+- `useEvent` — subscribe to events.
+- `useFullscreen` — display an element or video full-screen.
+- `usePageLeave` — triggers when mouse leaves page boundaries.
+- `usePermission` — query permission status for apps APIs.
+- `useMethods` — neat alternative to `useReducer`.
+- `useSetState` — creates `setState` method which works like `this.setState`.
+- `usePromise` — resolves promise only while component is mounted.
+- `useObservable` — tracks latest value of an `Observable`.
@@ -104,4 +172,3 @@
-[img-demo]: https://img.shields.io/badge/demo-%20%20%20%F0%9F%9A%80-green.svg
diff --git a/analysis_options.yaml b/analysis_options.yaml
index a5744c1..6a7c976 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -1,4 +1,92 @@
+# Analysis options for flutter_use package (basic hooks)
+# This file provides strict, comprehensive linting rules for high-quality Flutter packages
+
include: package:flutter_lints/flutter.yaml
-# Additional information about this file can be found at
-# https://dart.dev/guides/language/analysis-options
+analyzer:
+ # Enable stricter type checking for better code quality
+ language:
+ strict-casts: true
+ strict-inference: true
+ strict-raw-types: true
+
+ # Treat specific warnings as errors for critical issues
+ errors:
+ # Treat missing return statements as errors
+ missing_return: error
+ # Treat unused imports as errors
+ unused_import: error
+ # Treat unused local variables as errors
+ unused_local_variable: error
+ # Treat dead code as errors
+ dead_code: error
+ # Treat invalid assignments as errors
+ invalid_assignment: error
+
+linter:
+ rules:
+ # === ERROR PREVENTION ===
+ avoid_dynamic_calls: true
+ only_throw_errors: true
+ unrelated_type_equality_checks: true
+ cancel_subscriptions: true
+ close_sinks: true
+ test_types_in_equals: true
+ valid_regexps: true
+
+ # === NULL SAFETY ===
+ prefer_null_aware_operators: true
+ unnecessary_nullable_for_final_variable_declarations: true
+ avoid_null_checks_in_equality_operators: true
+
+ # === TYPE SAFETY ===
+ always_specify_types: false # Allow type inference for cleaner code
+ avoid_types_on_closure_parameters: true
+ omit_local_variable_types: true
+ prefer_typing_uninitialized_variables: true
+ type_annotate_public_apis: true
+
+ # === PERFORMANCE ===
+ avoid_print: true # Use logging framework instead
+ prefer_const_constructors: true
+ prefer_const_constructors_in_immutables: true
+ prefer_const_declarations: true
+ prefer_const_literals_to_create_immutables: true
+ prefer_final_fields: true
+ prefer_final_locals: true
+ unnecessary_lambdas: true
+
+ # === CODE STYLE ===
+ always_put_control_body_on_new_line: true
+ avoid_catches_without_on_clauses: true
+ avoid_catching_errors: true
+ avoid_empty_else: true
+ avoid_redundant_argument_values: true
+ camel_case_extensions: true
+ camel_case_types: true
+ curly_braces_in_flow_control_structures: true
+ prefer_single_quotes: true
+ require_trailing_commas: true
+
+ # === DOCUMENTATION ===
+ public_member_api_docs: true # Require docs for all public APIs
+
+ # === MAINTENANCE ===
+ avoid_positional_boolean_parameters: false # Allow positional bool for simple hooks like useBoolean/useToggle
+ prefer_expression_function_bodies: true
+ prefer_if_null_operators: true
+ prefer_is_empty: true
+ prefer_is_not_empty: true
+ prefer_iterable_whereType: true
+ unnecessary_const: true
+ unnecessary_new: true
+ unnecessary_this: true
+
+ # === DISABLED RULES ===
+ avoid_classes_with_only_static_members: false # Allow utility classes
+ avoid_function_literals_in_foreach_calls: false # forEach is readable
+ prefer_relative_imports: false # Package imports are clearer
+
+ # === CORE HOOKS SPECIFIC ===
+ prefer_final_in_for_each: true
+ prefer_foreach: false # Use for loops for better readability in hooks
diff --git a/bun.lock b/bun.lock
index f7ffd99..ac64bb7 100644
--- a/bun.lock
+++ b/bun.lock
@@ -4,193 +4,14 @@
"": {
"name": "flutter_use",
"devDependencies": {
- "husky": "8.0.3",
- "lint-staged": "13.2.3",
- "prettier": "^3.0.0",
+ "@evilmartians/lefthook": "^1.5.5",
+ "prettier": "^3.5.3",
},
},
},
"packages": {
- "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="],
-
- "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
-
- "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
-
- "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
-
- "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="],
-
- "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
-
- "chalk": ["chalk@5.2.0", "", {}, "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA=="],
-
- "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="],
-
- "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
-
- "cli-truncate": ["cli-truncate@3.1.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^5.0.0" } }, "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA=="],
-
- "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
-
- "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
-
- "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
-
- "commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
-
- "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
-
- "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
-
- "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
-
- "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
-
- "execa": ["execa@7.2.0", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.1", "human-signals": "^4.3.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^3.0.7", "strip-final-newline": "^3.0.0" } }, "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA=="],
-
- "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
-
- "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
-
- "human-signals": ["human-signals@4.3.1", "", {}, "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ=="],
-
- "husky": ["husky@8.0.3", "", { "bin": { "husky": "lib/bin.js" } }, "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg=="],
-
- "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
-
- "is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
-
- "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
-
- "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="],
-
- "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
-
- "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
-
- "lint-staged": ["lint-staged@13.2.3", "", { "dependencies": { "chalk": "5.2.0", "cli-truncate": "^3.1.0", "commander": "^10.0.0", "debug": "^4.3.4", "execa": "^7.0.0", "lilconfig": "2.1.0", "listr2": "^5.0.7", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-inspect": "^1.12.3", "pidtree": "^0.6.0", "string-argv": "^0.3.1", "yaml": "^2.2.2" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-zVVEXLuQIhr1Y7R7YAWx4TZLdvuzk7DnmrsTNL0fax6Z3jrpFcas+vKbzxhhvp6TA55m1SQuWkpzI1qbfDZbAg=="],
-
- "listr2": ["listr2@5.0.8", "", { "dependencies": { "cli-truncate": "^2.1.0", "colorette": "^2.0.19", "log-update": "^4.0.0", "p-map": "^4.0.0", "rfdc": "^1.3.0", "rxjs": "^7.8.0", "through": "^2.3.8", "wrap-ansi": "^7.0.0" }, "peerDependencies": { "enquirer": ">= 2.3.0 < 3" }, "optionalPeers": ["enquirer"] }, "sha512-mC73LitKHj9w6v30nLNGPetZIlfpUniNSsxxrbaPcWOjDb92SHPzJPi/t+v1YC/lxKz/AJ9egOjww0qUuFxBpA=="],
-
- "log-update": ["log-update@4.0.0", "", { "dependencies": { "ansi-escapes": "^4.3.0", "cli-cursor": "^3.1.0", "slice-ansi": "^4.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg=="],
-
- "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
-
- "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
-
- "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="],
-
- "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
-
- "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
-
- "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="],
-
- "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
-
- "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="],
-
- "p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="],
-
- "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
-
- "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
-
- "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="],
+ "@evilmartians/lefthook": ["@evilmartians/lefthook@1.11.14", "", { "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "ia32", "arm64", ], "bin": { "lefthook": "bin/index.js" } }, "sha512-vh6lqXVwh7uhI5C/gKB7InW8RyMFYXN27U4Hlum8ZBcDrOviF9fmcBJf4C6ZboWkD3j8v1qp4psc3bwynhPlTQ=="],
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
-
- "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
-
- "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
-
- "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
-
- "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
-
- "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
-
- "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
-
- "slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="],
-
- "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="],
-
- "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
-
- "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
-
- "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="],
-
- "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="],
-
- "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
-
- "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
-
- "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
-
- "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
-
- "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
-
- "yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="],
-
- "listr2/cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="],
-
- "log-update/slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="],
-
- "log-update/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
-
- "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
-
- "restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
-
- "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
-
- "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
-
- "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
-
- "listr2/cli-truncate/slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="],
-
- "listr2/cli-truncate/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
-
- "log-update/slice-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
-
- "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
-
- "log-update/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
-
- "log-update/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
-
- "log-update/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
-
- "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
-
- "wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
-
- "wrap-ansi/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
-
- "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
-
- "listr2/cli-truncate/slice-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
-
- "listr2/cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
-
- "listr2/cli-truncate/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
-
- "listr2/cli-truncate/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
-
- "listr2/cli-truncate/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
-
- "log-update/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
-
- "log-update/wrap-ansi/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
-
- "log-update/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
-
- "listr2/cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
}
}
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..24e5824
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,29 @@
+coverage:
+ status:
+ project:
+ default:
+ target: 80%
+ threshold: 5%
+ if_not_found: success
+ patch:
+ default:
+ target: 80%
+ threshold: 5%
+ if_not_found: success
+
+comment:
+ layout: "diff, flags, files"
+ behavior: default
+ require_changes: false
+ require_base: no
+ require_head: yes
+
+github_checks:
+ annotations: true
+
+ignore:
+ - "packages/**/example/**"
+ - "packages/**/test/**"
+ - "**/*.g.dart"
+ - "**/*.freezed.dart"
+ - "**/*.mocks.dart"
diff --git a/demo/.gitignore b/demo/.gitignore
new file mode 100644
index 0000000..79c113f
--- /dev/null
+++ b/demo/.gitignore
@@ -0,0 +1,45 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+
+# 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/demo/.metadata b/demo/.metadata
new file mode 100644
index 0000000..5d9f7ee
--- /dev/null
+++ b/demo/.metadata
@@ -0,0 +1,30 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "b25305a8832cfc6ba632a7f87ad455e319dccce8"
+ channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
+ base_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
+ - platform: web
+ create_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
+ base_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/demo/FLUTTER_USE_HOOKS_IMPLEMENTATION_PLAN.md b/demo/FLUTTER_USE_HOOKS_IMPLEMENTATION_PLAN.md
new file mode 100644
index 0000000..a340dd8
--- /dev/null
+++ b/demo/FLUTTER_USE_HOOKS_IMPLEMENTATION_PLAN.md
@@ -0,0 +1,213 @@
+# Flutter Use Hooks - Comprehensive Demo Implementation Plan
+
+## Current Status
+**Total hooks in flutter_use (basic package): 37**
+**Hooks with demos implemented: 6** ✅
+**Hooks remaining: 31** 🔲
+
+## Implemented Hooks ✅
+1. `useThrottle` - Performance & Optimization
+2. `useThrottleFn` - Performance & Optimization
+3. `useScroll` - Scroll & Navigation
+4. `useScrolling` - Scroll & Navigation
+5. `useCopyToClipboard` - Utility & Integration
+6. `useClickAway` - Utility & Integration
+
+## Categorized Hooks to Implement
+
+### 🎯 State Management (9 hooks)
+Priority: HIGH - These are fundamental hooks that demonstrate state management patterns
+
+1. **`useBoolean`** 🔲
+ - Simple boolean state with toggle/set methods
+ - Demo: Toggle switches, checkboxes, feature flags
+
+2. **`useCounter`** 🔲
+ - Numeric counter with increment/decrement/reset
+ - Demo: Shopping cart quantity, pagination counter
+
+3. **`useToggle`** 🔲
+ - Toggle between two values (not just boolean)
+ - Demo: Theme switcher, language selector
+
+4. **`useList`** 🔲
+ - List state management with add/remove/update
+ - Demo: Todo list, dynamic form fields
+
+5. **`useMap`** 🔲
+ - Map/dictionary state management
+ - Demo: Form data, configuration settings
+
+6. **`useSet`** 🔲
+ - Set state management with add/remove/has
+ - Demo: Selected items, tags, filters
+
+7. **`useStateList`** 🔲
+ - Cycle through a list of states
+ - Demo: Image carousel, stepper
+
+8. **`useDefault`** 🔲
+ - State with default/fallback value
+ - Demo: User preferences with defaults
+
+9. **`useNumber`** 🔲
+ - Numeric state with constraints
+ - Demo: Slider with min/max, numeric input
+
+### ⚡ Effects & Lifecycle (10 hooks)
+Priority: HIGH - Core lifecycle management patterns
+
+1. **`useEffectOnce`** 🔲
+ - Run effect only once on mount
+ - Demo: API call on load, analytics
+
+2. **`useUpdateEffect`** 🔲
+ - Skip effect on first render
+ - Demo: Save changes indicator
+
+3. **`useCustomCompareEffect`** 🔲
+ - Effect with custom equality check
+ - Demo: Deep object comparison
+
+4. **`useMount`** 🔲
+ - Callback on component mount
+ - Demo: Welcome message, initialization
+
+5. **`useUnmount`** 🔲
+ - Callback on component unmount
+ - Demo: Cleanup, save draft
+
+6. **`useLifecycles`** 🔲
+ - Combined mount/unmount callbacks
+ - Demo: Component lifecycle logger
+
+7. **`useFirstMountState`** 🔲
+ - Check if first render
+ - Demo: Skip animation on first load
+
+8. **`useUpdate`** 🔲
+ - Force component re-render
+ - Demo: Manual refresh button
+
+9. **`useInterval`** 🔲
+ - Safe interval management
+ - Demo: Clock, auto-save, polling
+
+10. **`useTimeout`** 🔲 / **`useTimeoutFn`** 🔲
+ - Delayed execution with cleanup
+ - Demo: Toast notifications, delayed actions
+
+### 🛡️ Performance & Optimization (3 hooks)
+Priority: MEDIUM - Already have 2 implemented
+
+1. **`useDebounce`** 🔲
+ - Debounce value updates
+ - Demo: Search input, form validation
+
+2. **`useLatest`** 🔲
+ - Always get latest value in callbacks
+ - Demo: Event handlers with current state
+
+3. **`usePreviousDistinct`** 🔲
+ - Track previous distinct values
+ - Demo: Undo functionality, change detection
+
+### 🔧 Utilities (8 hooks)
+Priority: MEDIUM - Useful utilities for common tasks
+
+1. **`useLogger`** 🔲
+ - Log component lifecycle/updates
+ - Demo: Debug panel showing renders
+
+2. **`useOrientation`** 🔲 / **`useOrientationFn`** 🔲
+ - Device orientation detection
+ - Demo: Responsive layout switcher
+
+3. **`useError`** 🔲 / **`useException`** 🔲
+ - Error state management
+ - Demo: Form validation errors
+
+4. **`useFutureRetry`** 🔲
+ - Future with retry capability
+ - Demo: API call with retry button
+
+5. **`useTextFormValidator`** 🔲
+ - Form field validation
+ - Demo: Registration form
+
+6. **`useBuildsCount`** 🔲
+ - Count widget rebuilds
+ - Demo: Performance monitoring
+
+## Implementation Priority Order
+
+### Phase 1: Core State Management (Week 1)
+1. `useCounter` - Most basic state hook
+2. `useBoolean` - Common toggle pattern
+3. `useToggle` - Extended toggle functionality
+4. `useList` - Dynamic lists
+5. `useMap` - Key-value state
+
+### Phase 2: Essential Effects (Week 2)
+6. `useEffectOnce` - Common pattern
+7. `useMount` / `useUnmount` - Lifecycle basics
+8. `useInterval` - Timer management
+9. `useTimeout` - Delayed actions
+10. `useDebounce` - Search optimization
+
+### Phase 3: Advanced State (Week 3)
+11. `useSet` - Unique collections
+12. `useStateList` - State cycling
+13. `useDefault` - Fallback values
+14. `useNumber` - Numeric constraints
+15. `usePreviousDistinct` - History tracking
+
+### Phase 4: Utilities & Polish (Week 4)
+16. `useLogger` - Debugging
+17. `useOrientation` - Device features
+18. `useError` - Error handling
+19. `useFutureRetry` - Async patterns
+20. `useTextFormValidator` - Forms
+21. Remaining hooks
+
+## Demo Structure Template
+
+Each demo should include:
+1. **Interactive Example** - Visual demonstration
+2. **Code Snippet** - Usage example with syntax highlighting
+3. **Use Cases** - Real-world applications
+4. **Parameters** - Hook configuration options
+5. **Related Hooks** - Cross-references
+
+## Technical Requirements
+
+1. **Consistent UI Pattern**
+ - Card-based layout matching existing demos
+ - Responsive design for mobile/desktop
+ - Material 3 design system
+
+2. **Code Examples**
+ - Syntax highlighting
+ - Copy-to-clipboard functionality
+ - Minimal but complete examples
+
+3. **Navigation Structure**
+ - Categorized sections on homepage
+ - Search/filter capability (future)
+ - Direct linking to specific demos
+
+## Next Steps
+
+1. Create demo template/base class for consistency
+2. Implement Phase 1 hooks (5 demos)
+3. Update homepage with new categories
+4. Add search functionality
+5. Deploy and gather feedback
+
+## Success Metrics
+
+- All 37 hooks have interactive demos
+- Each demo loads in < 1 second
+- Mobile-friendly responsive design
+- SEO-optimized for discoverability
+- Analytics to track most-used hooks
\ No newline at end of file
diff --git a/demo/HOOKS_DEMO_ROADMAP.md b/demo/HOOKS_DEMO_ROADMAP.md
new file mode 100644
index 0000000..d6dd9f3
--- /dev/null
+++ b/demo/HOOKS_DEMO_ROADMAP.md
@@ -0,0 +1,187 @@
+# Flutter Use Demo Site - Complete Implementation Roadmap
+
+## Executive Summary
+
+The flutter_use library contains **37 hooks** in the basic package. Currently, only **6 hooks (16%)** have interactive demos implemented. This roadmap outlines a systematic approach to achieve 100% demo coverage.
+
+## Current Architecture
+
+### Demo Structure
+- **Location**: `/demo/lib/demos/`
+- **Pattern**: Each hook has its own demo file (`use_[hook_name]_demo.dart`)
+- **Navigation**: Routes defined in `main.dart`
+- **UI**: Material 3 design with categorized sections
+
+### Implemented Demos (6/37)
+✅ Performance & Optimization
+- `useThrottle`
+- `useThrottleFn`
+
+✅ Scroll & Navigation
+- `useScroll`
+- `useScrolling`
+
+✅ Utility & Integration
+- `useCopyToClipboard`
+- `useClickAway`
+
+## Categorized Implementation Plan
+
+### 🎯 Category 1: State Management Hooks (9 hooks)
+These are fundamental hooks that demonstrate state patterns.
+
+| Hook | Description | Demo Ideas | Priority |
+|------|-------------|------------|----------|
+| `useCounter` | Numeric counter with min/max | Shopping cart, pagination | **HIGH** |
+| `useBoolean` | Boolean state toggle | Dark mode, feature flags | **HIGH** |
+| `useToggle` | Toggle between any values | Theme selector, language | **HIGH** |
+| `useList` | List state management | Todo list, cart items | **HIGH** |
+| `useMap` | Key-value state | Form data, settings | **MEDIUM** |
+| `useSet` | Unique collection state | Selected tags, filters | **MEDIUM** |
+| `useStateList` | Cycle through states | Image carousel, wizard | **MEDIUM** |
+| `useDefault` | State with fallback | User preferences | **LOW** |
+| `useNumber` | Alias for useCounter | Slider input | **LOW** |
+
+### ⚡ Category 2: Effects & Lifecycle Hooks (12 hooks)
+Control component lifecycle and side effects.
+
+| Hook | Description | Demo Ideas | Priority |
+|------|-------------|------------|----------|
+| `useEffectOnce` | Run effect once on mount | API fetch, analytics | **HIGH** |
+| `useMount` | Callback on mount | Welcome message | **HIGH** |
+| `useUnmount` | Callback on unmount | Save draft, cleanup | **HIGH** |
+| `useInterval` | Safe interval management | Clock, auto-save | **HIGH** |
+| `useTimeout` | Delayed execution | Toast, delayed action | **HIGH** |
+| `useTimeoutFn` | Timeout with function | Debounced save | **MEDIUM** |
+| `useUpdateEffect` | Skip first render | Change indicator | **MEDIUM** |
+| `useLifecycles` | Mount + unmount | Lifecycle logger | **MEDIUM** |
+| `useFirstMountState` | Is first render? | Skip animation | **LOW** |
+| `useUpdate` | Force re-render | Manual refresh | **LOW** |
+| `useCustomCompareEffect` | Custom equality check | Deep comparison | **LOW** |
+| `useLatest` | Latest value in callbacks | Event handlers | **LOW** |
+
+### 🛡️ Category 3: Performance Hooks (3 hooks)
+Optimize rendering and updates.
+
+| Hook | Description | Demo Ideas | Priority |
+|------|-------------|------------|----------|
+| `useDebounce` | Debounce value updates | Search input | **HIGH** |
+| `usePreviousDistinct` | Track previous values | Undo feature | **MEDIUM** |
+| `useBuildsCount` | Count rebuilds | Performance monitor | **LOW** |
+
+### 🔧 Category 4: Utility Hooks (7 hooks)
+Various utilities for common tasks.
+
+| Hook | Description | Demo Ideas | Priority |
+|------|-------------|------------|----------|
+| `useLogger` | Log lifecycle/updates | Debug panel | **MEDIUM** |
+| `useOrientation` | Device orientation | Layout switcher | **MEDIUM** |
+| `useOrientationFn` | Orientation callback | Responsive UI | **MEDIUM** |
+| `useError` | Error state management | Form errors | **MEDIUM** |
+| `useException` | Exception handling | API errors | **MEDIUM** |
+| `useFutureRetry` | Retry failed futures | API with retry | **MEDIUM** |
+| `useTextFormValidator` | Form validation | Registration form | **HIGH** |
+
+## Implementation Phases
+
+### Phase 1: Core State (Week 1) - 5 demos
+Start with the most fundamental state management hooks:
+1. `useCounter` - Basic numeric state
+2. `useBoolean` - Toggle pattern
+3. `useToggle` - Extended toggle
+4. `useList` - Dynamic lists
+5. `useDebounce` - Search optimization
+
+### Phase 2: Essential Effects (Week 2) - 5 demos
+Add lifecycle and timing hooks:
+6. `useEffectOnce` - One-time effects
+7. `useMount`/`useUnmount` - Lifecycle
+8. `useInterval` - Periodic updates
+9. `useTimeout` - Delayed actions
+10. `useTextFormValidator` - Forms
+
+### Phase 3: Advanced State (Week 3) - 6 demos
+Complete state management coverage:
+11. `useMap` - Key-value pairs
+12. `useSet` - Unique collections
+13. `useStateList` - State cycling
+14. `usePreviousDistinct` - History
+15. `useUpdateEffect` - Update detection
+16. `useFutureRetry` - Async patterns
+
+### Phase 4: Utilities & Polish (Week 4) - 15 demos
+Complete remaining hooks:
+17-31. All remaining utility and specialized hooks
+
+## Technical Implementation Guide
+
+### Demo Template Structure
+```dart
+class Use[HookName]Demo extends HookWidget {
+ const Use[HookName]Demo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ // 1. Hook usage
+ // 2. Interactive UI
+ // 3. Real-time display
+ // 4. Code example dialog
+ }
+}
+```
+
+### Required Features per Demo
+1. **Interactive Example** - Visual, interactive demonstration
+2. **Live State Display** - Show current values/state
+3. **Code Snippet** - Copyable usage example
+4. **Use Cases** - 2-3 practical applications
+5. **Reset/Clear** - Return to initial state
+
+### UI/UX Guidelines
+- Consistent card-based layout
+- Material 3 design system
+- Responsive for mobile/desktop
+- Smooth animations
+- Clear visual feedback
+
+## Homepage Reorganization
+
+Update the homepage to reflect all categories:
+
+```
+🎯 State Management (9 hooks)
+⚡ Effects & Lifecycle (12 hooks)
+🛡️ Performance (5 hooks)
+📜 Scroll & Navigation (2 hooks)
+🔧 Utilities (9 hooks)
+```
+
+## Success Criteria
+
+- [ ] 100% hook coverage (37/37 demos)
+- [ ] Consistent UI/UX across all demos
+- [ ] Mobile-responsive design
+- [ ] Code examples for each hook
+- [ ] Search/filter functionality
+- [ ] Performance: < 1s load time
+- [ ] SEO optimization
+- [ ] Analytics integration
+
+## Next Immediate Steps
+
+1. Create a base demo widget class for consistency
+2. Implement `useCounter` demo as template
+3. Set up automated route generation
+4. Add category filtering on homepage
+5. Deploy Phase 1 demos
+
+## Long-term Enhancements
+
+1. **Search & Filter** - Find hooks by name/category
+2. **Playground Mode** - Edit code live
+3. **Comparison Tool** - Compare similar hooks
+4. **Performance Metrics** - Show render counts
+5. **API Documentation** - Inline parameter docs
+6. **Export Examples** - Download demo code
+7. **Dark Mode** - Theme toggle using `useBoolean`
+8. **Favorites** - Save frequently used hooks
\ No newline at end of file
diff --git a/demo/README.md b/demo/README.md
new file mode 100644
index 0000000..dbd403a
--- /dev/null
+++ b/demo/README.md
@@ -0,0 +1,16 @@
+# demo
+
+A new Flutter project.
+
+## Getting Started
+
+This project is a starting point for a Flutter application.
+
+A few resources to get you started if this is your first Flutter project:
+
+- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
+- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
+
+For help getting started with Flutter development, view the
+[online documentation](https://docs.flutter.dev/), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
diff --git a/packages/basic/example/analysis_options.yaml b/demo/analysis_options.yaml
similarity index 93%
rename from packages/basic/example/analysis_options.yaml
rename to demo/analysis_options.yaml
index 61b6c4d..0d29021 100644
--- a/packages/basic/example/analysis_options.yaml
+++ b/demo/analysis_options.yaml
@@ -13,8 +13,7 @@ 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.
+ # and their documentation is published at https://dart.dev/lints.
#
# 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
diff --git a/demo/lib/hooks/use_boolean_demo.dart b/demo/lib/hooks/use_boolean_demo.dart
new file mode 100644
index 0000000..79bb492
--- /dev/null
+++ b/demo/lib/hooks/use_boolean_demo.dart
@@ -0,0 +1,384 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseBooleanDemo extends HookWidget {
+ const UseBooleanDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final boolean = useBoolean(false);
+ final darkMode = useBoolean(true);
+ final isExpanded = useBoolean(false);
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useBoolean Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Header
+ const Text(
+ '🔵 useBoolean Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Boolean state management with toggle, set true/false',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ // Basic Boolean Toggle
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎯 Basic Boolean Toggle',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Status Display
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ color: boolean.value
+ ? Colors.green.withValues(alpha: 0.1)
+ : Colors.red.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color: boolean.value ? Colors.green : Colors.red,
+ width: 2,
+ ),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ boolean.value ? Icons.check_circle : Icons.cancel,
+ color: boolean.value ? Colors.green : Colors.red,
+ size: 32,
+ ),
+ const SizedBox(width: 12),
+ Text(
+ boolean.value ? 'TRUE' : 'FALSE',
+ style: TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ color: boolean.value ? Colors.green : Colors.red,
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Control Buttons
+ Row(
+ children: [
+ Expanded(
+ child: ElevatedButton.icon(
+ onPressed: boolean.toggle,
+ icon: const Icon(Icons.swap_horiz),
+ label: const Text('Toggle'),
+ style: ElevatedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ ),
+ ),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 12),
+
+ Row(
+ children: [
+ Expanded(
+ child: OutlinedButton(
+ onPressed: () => boolean.toggle(true),
+ style: OutlinedButton.styleFrom(
+ foregroundColor: Colors.green,
+ ),
+ child: const Text('Set True'),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: OutlinedButton(
+ onPressed: () => boolean.toggle(false),
+ style: OutlinedButton.styleFrom(
+ foregroundColor: Colors.red,
+ ),
+ child: const Text('Set False'),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Dark Mode Example
+ Card(
+ elevation: 4,
+ color: darkMode.value ? Colors.grey[900] : null,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ '🌙 Dark Mode Toggle',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ color: darkMode.value ? Colors.white : null,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ Row(
+ children: [
+ Icon(
+ darkMode.value ? Icons.dark_mode : Icons.light_mode,
+ size: 48,
+ color: darkMode.value ? Colors.amber : Colors.orange,
+ ),
+ const SizedBox(width: 20),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ darkMode.value
+ ? 'Dark Mode Active'
+ : 'Light Mode Active',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ color: darkMode.value ? Colors.white : null,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ 'Toggle to switch theme',
+ style: TextStyle(
+ color: darkMode.value
+ ? Colors.grey[400]
+ : Colors.grey[600],
+ ),
+ ),
+ ],
+ ),
+ ),
+ Switch(
+ value: darkMode.value,
+ onChanged: (_) => darkMode.toggle(),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Expandable Section Example
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '📦 Expandable Content',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ InkWell(
+ onTap: isExpanded.toggle,
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Row(
+ children: [
+ const Text(
+ 'Click to expand/collapse',
+ style: TextStyle(fontSize: 16),
+ ),
+ const Spacer(),
+ AnimatedRotation(
+ turns: isExpanded.value ? 0.5 : 0,
+ duration: const Duration(milliseconds: 200),
+ child: const Icon(Icons.expand_more),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ AnimatedSize(
+ duration: const Duration(milliseconds: 300),
+ child: isExpanded.value
+ ? Container(
+ margin: const EdgeInsets.only(top: 16),
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.blue.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ 'This is the expanded content! 🎉\n\n'
+ 'The useBoolean hook makes it easy to manage '
+ 'toggle states like expand/collapse, show/hide, '
+ 'and any other binary state in your application.',
+ style: TextStyle(fontSize: 14),
+ ),
+ )
+ : const SizedBox.shrink(),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ // Usage Info
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Simple boolean state management\n'
+ '• toggle() method to flip the value\n'
+ '• toggle(true) and toggle(false) for explicit setting\n'
+ '• Perfect for switches, checkboxes, visibility toggles\n'
+ '• Returns ToggleState with value and toggle method',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useBoolean Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Initialize with default value
+final isVisible = useBoolean(true);
+final darkMode = useBoolean(false);
+
+// Access the value
+if (isVisible.value) {
+ // Show content
+}
+
+// Toggle the value
+ElevatedButton(
+ onPressed: isVisible.toggle,
+ child: Text('Toggle Visibility'),
+)
+
+// Set explicitly
+TextButton(
+ onPressed: () => darkMode.toggle(true),
+ child: Text('Enable Dark Mode'),
+)
+
+// Use with Switch widget
+Switch(
+ value: darkMode.value,
+ onChanged: (_) => darkMode.toggle(),
+)''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_builds_count_demo.dart b/demo/lib/hooks/use_builds_count_demo.dart
new file mode 100644
index 0000000..ffbc615
--- /dev/null
+++ b/demo/lib/hooks/use_builds_count_demo.dart
@@ -0,0 +1,417 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseBuildsCountDemo extends HookWidget {
+ const UseBuildsCountDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final buildsCount = useBuildsCount();
+ final counter = useState(0);
+ final text = useState('');
+ final isChecked = useState(false);
+ final sliderValue = useState(50.0);
+ final buildHistory = useState>([]);
+
+ // Track build reasons
+ useEffect(() {
+ final reason = _getBuildReason(
+ counter.value,
+ text.value,
+ isChecked.value,
+ sliderValue.value,
+ );
+ buildHistory.value = [
+ 'Build #$buildsCount: $reason at ${DateTime.now().toString().substring(11, 19)}',
+ ...buildHistory.value.take(9),
+ ];
+ return null;
+ });
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useBuildsCount Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🔢 useBuildsCount Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Track how many times your widget rebuilds',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '📊 Build Statistics',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Build count display
+ Center(
+ child: Container(
+ width: 150,
+ height: 150,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ gradient: LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [
+ Colors.blue.withValues(alpha: 0.2),
+ Colors.purple.withValues(alpha: 0.2),
+ ],
+ ),
+ border: Border.all(color: Colors.blue, width: 3),
+ ),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Text(
+ 'Builds',
+ style: TextStyle(
+ fontSize: 16,
+ color: Colors.grey,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ '$buildsCount',
+ style: const TextStyle(
+ fontSize: 48,
+ fontWeight: FontWeight.bold,
+ color: Colors.blue,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ const Text(
+ '🎮 Interactive Controls',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Each interaction causes a rebuild:',
+ style: TextStyle(color: Colors.grey, fontSize: 12),
+ ),
+ const SizedBox(height: 16),
+
+ // Counter control
+ Row(
+ children: [
+ const Text('Counter: '),
+ const SizedBox(width: 16),
+ IconButton(
+ onPressed: () => counter.value--,
+ icon: const Icon(Icons.remove_circle),
+ ),
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Text(
+ '${counter.value}',
+ style: const TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ IconButton(
+ onPressed: () => counter.value++,
+ icon: const Icon(Icons.add_circle),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 16),
+
+ // Text input
+ TextField(
+ decoration: const InputDecoration(
+ labelText: 'Type something',
+ hintText: 'Each character triggers a rebuild',
+ border: OutlineInputBorder(),
+ prefixIcon: Icon(Icons.keyboard),
+ ),
+ onChanged: (value) => text.value = value,
+ ),
+
+ const SizedBox(height: 16),
+
+ // Checkbox
+ CheckboxListTile(
+ title: const Text('Toggle me'),
+ subtitle: const Text('Causes rebuild on change'),
+ value: isChecked.value,
+ onChanged: (value) => isChecked.value = value!,
+ controlAffinity: ListTileControlAffinity.leading,
+ ),
+
+ const SizedBox(height: 16),
+
+ // Slider
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Slider: ${sliderValue.value.round()}'),
+ Slider(
+ value: sliderValue.value,
+ min: 0,
+ max: 100,
+ divisions: 100,
+ label: sliderValue.value.round().toString(),
+ onChanged: (value) => sliderValue.value = value,
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ // Build history
+ const Text(
+ '📜 Build History:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ Container(
+ height: 150,
+ width: double.infinity,
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: buildHistory.value.isEmpty
+ ? const Center(
+ child: Text(
+ 'Build history will appear here',
+ style: TextStyle(color: Colors.grey),
+ ),
+ )
+ : ListView.builder(
+ itemCount: buildHistory.value.length,
+ itemBuilder: (context, index) {
+ return Text(
+ buildHistory.value[index],
+ style: const TextStyle(
+ color: Colors.white,
+ fontFamily: 'monospace',
+ fontSize: 12,
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Performance tips
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.speed, color: Colors.green),
+ SizedBox(width: 8),
+ Text(
+ 'Performance Tips',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• High build counts may indicate optimization opportunities\n'
+ '• Use const widgets where possible\n'
+ '• Consider memoization for expensive computations\n'
+ '• Split large widgets into smaller ones\n'
+ '• Use keys to preserve widget state',
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Increments counter on each build\n'
+ '• Persists count across rebuilds\n'
+ '• Starts from 1 (first build)\n'
+ '• Simple performance monitoring\n'
+ '• Helps identify excessive rebuilds',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ String _getBuildReason(
+ int counter,
+ String text,
+ bool checked,
+ double slider,
+ ) {
+ // In a real scenario, you'd track what actually changed
+ return 'State changed';
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useBuildsCount Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Track build count
+final buildsCount = useBuildsCount();
+
+// Display in UI
+Text('This widget has rebuilt \$buildsCount times')
+
+// Debug performance
+if (buildsCount > 100) {
+ print('Warning: Excessive rebuilds detected!');
+}
+
+// Monitor specific widget
+class ExpensiveWidget extends HookWidget {
+ @override
+ Widget build(BuildContext context) {
+ final builds = useBuildsCount();
+
+ if (kDebugMode) {
+ print('ExpensiveWidget build #\$builds');
+ }
+
+ return Container();
+ }
+}
+
+// Track rebuild reasons
+useEffect(() {
+ print('Build #\$buildsCount triggered');
+ return null;
+});
+
+// Performance monitoring
+final builds = useBuildsCount();
+final lastBuildTime = useRef(DateTime.now());
+
+useEffect(() {
+ final duration = DateTime.now()
+ .difference(lastBuildTime.value);
+ print('Build #\$builds took \${duration.inMilliseconds}ms');
+ lastBuildTime.value = DateTime.now();
+ return null;
+});''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_click_away_demo.dart b/demo/lib/hooks/use_click_away_demo.dart
new file mode 100644
index 0000000..25ae0b0
--- /dev/null
+++ b/demo/lib/hooks/use_click_away_demo.dart
@@ -0,0 +1,449 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseClickAwayDemo extends HookWidget {
+ const UseClickAwayDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final isDropdownOpen = useState(false);
+ final isModalOpen = useState(false);
+ final clickCount = useState(0);
+
+ // Click away for dropdown
+ final dropdownClickAway = useClickAway(() {
+ if (isDropdownOpen.value) {
+ isDropdownOpen.value = false;
+ }
+ });
+
+ // Click away for modal
+ final modalClickAway = useClickAway(() {
+ if (isModalOpen.value) {
+ isModalOpen.value = false;
+ }
+ });
+
+ // Click away for counter (just for demo)
+ final counterClickAway = useClickAway(() {
+ clickCount.value++;
+ });
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useClickAway Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: Stack(
+ children: [
+ SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Header
+ const Text(
+ '👆 useClickAway Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Detect clicks outside of specific elements',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ // Dropdown Demo
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '📋 Dropdown Example',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 16),
+
+ Text(
+ 'Click the button to open the dropdown, then click anywhere outside to close it.',
+ style: TextStyle(color: Colors.grey[700]),
+ ),
+ const SizedBox(height: 20),
+
+ // Dropdown Container
+ Container(
+ key: dropdownClickAway.ref,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ ElevatedButton.icon(
+ onPressed: () {
+ isDropdownOpen.value = !isDropdownOpen.value;
+ },
+ icon: Icon(
+ isDropdownOpen.value
+ ? Icons.keyboard_arrow_up
+ : Icons.keyboard_arrow_down,
+ ),
+ label: Text(
+ isDropdownOpen.value
+ ? 'Close Dropdown'
+ : 'Open Dropdown',
+ ),
+ ),
+
+ if (isDropdownOpen.value) ...[
+ const SizedBox(height: 8),
+ Container(
+ width: 200,
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(
+ color: Theme.of(context)
+ .colorScheme
+ .outline
+ .withValues(alpha: 0.2),
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withValues(
+ alpha: 0.1,
+ ),
+ blurRadius: 8,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ child: Column(
+ crossAxisAlignment:
+ CrossAxisAlignment.start,
+ children: [
+ _buildDropdownItem(
+ 'Option 1',
+ Icons.star,
+ ),
+ _buildDropdownItem(
+ 'Option 2',
+ Icons.favorite,
+ ),
+ _buildDropdownItem(
+ 'Option 3',
+ Icons.settings,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Modal Demo
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🪟 Modal Example',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 16),
+
+ Text(
+ 'Click the button to show a modal, then click outside the modal content to close it.',
+ style: TextStyle(color: Colors.grey[700]),
+ ),
+ const SizedBox(height: 20),
+
+ ElevatedButton.icon(
+ onPressed: () {
+ isModalOpen.value = true;
+ },
+ icon: const Icon(Icons.open_in_new),
+ label: const Text('Open Modal'),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Click Counter Demo
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎯 Click Counter Example',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 16),
+
+ Text(
+ 'This box tracks clicks outside of it. Click anywhere outside the blue area below.',
+ style: TextStyle(color: Colors.grey[700]),
+ ),
+ const SizedBox(height: 20),
+
+ Container(
+ key: counterClickAway.ref,
+ width: double.infinity,
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ color: Colors.blue.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: Colors.blue, width: 2),
+ ),
+ child: Column(
+ children: [
+ const Text(
+ 'Protected Area',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ 'Clicks outside this area: ${clickCount.value}',
+ style: TextStyle(
+ fontSize: 14,
+ color: Colors.grey[700],
+ ),
+ ),
+ const SizedBox(height: 12),
+ ElevatedButton(
+ onPressed: () {
+ clickCount.value = 0;
+ },
+ child: const Text('Reset Counter'),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ // Usage Info
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Returns a GlobalKey to attach to your target widget\n'
+ '• Listens for clicks anywhere on the screen\n'
+ '• Calls your callback when clicks occur outside the target\n'
+ '• Perfect for dropdowns, modals, and tooltips\n'
+ '• Automatically handles cleanup when widget unmounts',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 100), // Extra space
+ ],
+ ),
+ ),
+
+ // Modal Overlay
+ if (isModalOpen.value)
+ Positioned.fill(
+ child: Container(
+ color: Colors.black.withValues(alpha: 0.5),
+ child: Center(
+ child: Container(
+ key: modalClickAway.ref,
+ width: 300,
+ height: 200,
+ margin: const EdgeInsets.all(20),
+ padding: const EdgeInsets.all(24),
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.surface,
+ borderRadius: BorderRadius.circular(16),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withValues(alpha: 0.3),
+ blurRadius: 20,
+ offset: const Offset(0, 8),
+ ),
+ ],
+ ),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(
+ Icons.info_outline,
+ size: 48,
+ color: Colors.blue,
+ ),
+ const SizedBox(height: 16),
+ const Text(
+ 'Modal Content',
+ style: TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Click outside this modal to close it',
+ textAlign: TextAlign.center,
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 20),
+ ElevatedButton(
+ onPressed: () {
+ isModalOpen.value = false;
+ },
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildDropdownItem(String text, IconData icon) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 4),
+ child: Row(
+ children: [
+ Icon(icon, size: 16, color: Colors.grey[600]),
+ const SizedBox(width: 8),
+ Text(text),
+ ],
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useClickAway Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''final isOpen = useState(false);
+
+final clickAway = useClickAway(() {
+ if (isOpen.value) {
+ isOpen.value = false;
+ }
+});
+
+// Use the ref with your target widget
+Container(
+ key: clickAway.ref,
+ child: Column(
+ children: [
+ ElevatedButton(
+ onPressed: () => isOpen.value = true,
+ child: Text('Open Dropdown'),
+ ),
+
+ if (isOpen.value)
+ Container(
+ // Dropdown content
+ child: Text('Dropdown Content'),
+ ),
+ ],
+ ),
+)
+
+// Callback is called when user clicks outside
+// the widget with the clickAway.ref key''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_copy_to_clipboard_demo.dart b/demo/lib/hooks/use_copy_to_clipboard_demo.dart
new file mode 100644
index 0000000..1ccbb75
--- /dev/null
+++ b/demo/lib/hooks/use_copy_to_clipboard_demo.dart
@@ -0,0 +1,323 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseCopyToClipboardDemo extends HookWidget {
+ const UseCopyToClipboardDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final textController = useTextEditingController(
+ text: 'Hello from Flutter Use! 🎯',
+ );
+ final copyToClipboard = useCopyToClipboard();
+
+ // Sample texts for quick copy
+ final sampleTexts = [
+ 'flutter pub add flutter_use',
+ 'https://github.com/wasabeef/flutter_use',
+ 'contact@example.com',
+ '+1 (555) 123-4567',
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
+ ];
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useCopyToClipboard Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Header
+ const Text(
+ '📋 useCopyToClipboard Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Copy text to clipboard with status feedback',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ // Live Example
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '📝 Live Example',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Input Field
+ TextField(
+ controller: textController,
+ maxLines: 3,
+ decoration: const InputDecoration(
+ labelText: 'Text to copy',
+ hintText:
+ 'Enter any text you want to copy to clipboard',
+ border: OutlineInputBorder(),
+ ),
+ ),
+ const SizedBox(height: 16),
+
+ // Copy Button
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton.icon(
+ onPressed: () {
+ if (textController.text.isNotEmpty) {
+ copyToClipboard.copy(textController.text);
+ }
+ },
+ icon: const Icon(Icons.content_copy),
+ label: const Text('Copy to Clipboard'),
+ style: ElevatedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ ),
+ ),
+ ),
+ const SizedBox(height: 24),
+
+ // Status Display
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: _getStatusColor(copyToClipboard),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: Colors.grey[300]!),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Icon(
+ _getStatusIcon(copyToClipboard),
+ color: _getStatusIconColor(copyToClipboard),
+ size: 20,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Status: ${_getStatusText(copyToClipboard)}',
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ if (copyToClipboard.copied != null) ...[
+ const SizedBox(height: 8),
+ Text(
+ 'Last copied: "${copyToClipboard.copied}"',
+ style: TextStyle(
+ color: Colors.grey[700],
+ fontStyle: FontStyle.italic,
+ ),
+ ),
+ ],
+ if (copyToClipboard.error != null) ...[
+ const SizedBox(height: 8),
+ Text(
+ 'Error: ${copyToClipboard.error}',
+ style: const TextStyle(
+ color: Colors.red,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Quick Copy Section
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '⚡ Quick Copy Options',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 16),
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: sampleTexts.map((text) {
+ return ActionChip(
+ label: Text(
+ text.length > 30
+ ? '${text.substring(0, 30)}...'
+ : text,
+ style: const TextStyle(fontSize: 12),
+ ),
+ onPressed: () {
+ copyToClipboard.copy(text);
+ textController.text = text;
+ },
+ avatar: const Icon(Icons.content_copy, size: 16),
+ );
+ }).toList(),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Usage Info
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Provides a simple interface for clipboard operations\n'
+ '• Tracks the last successfully copied text\n'
+ '• Handles errors gracefully (permissions, platform issues)\n'
+ '• Perfect for share buttons, code snippets, and user content',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Color _getStatusColor(CopyToClipboardState state) {
+ if (state.error != null) return Colors.red[50]!;
+ if (state.copied != null) return Colors.green[50]!;
+ return Colors.grey[50]!;
+ }
+
+ IconData _getStatusIcon(CopyToClipboardState state) {
+ if (state.error != null) return Icons.error;
+ if (state.copied != null) return Icons.check_circle;
+ return Icons.info;
+ }
+
+ Color _getStatusIconColor(CopyToClipboardState state) {
+ if (state.error != null) return Colors.red;
+ if (state.copied != null) return Colors.green;
+ return Colors.grey;
+ }
+
+ String _getStatusText(CopyToClipboardState state) {
+ if (state.error != null) return 'Error occurred';
+ if (state.copied != null) return 'Successfully copied';
+ return 'Ready to copy';
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useCopyToClipboard Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''final copyToClipboard = useCopyToClipboard();
+
+// Copy text to clipboard
+ElevatedButton(
+ onPressed: () {
+ copyToClipboard.copy('Hello, World!');
+ },
+ child: Text('Copy Text'),
+)
+
+// Check status
+if (copyToClipboard.copied != null) {
+ print('Last copied: \${copyToClipboard.copied}');
+}
+
+if (copyToClipboard.error != null) {
+ print('Error: \${copyToClipboard.error}');
+}
+
+// Show feedback to user
+Text(copyToClipboard.copied != null
+ ? 'Copied!'
+ : 'Ready to copy')''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_counter_demo.dart b/demo/lib/hooks/use_counter_demo.dart
new file mode 100644
index 0000000..92a925a
--- /dev/null
+++ b/demo/lib/hooks/use_counter_demo.dart
@@ -0,0 +1,355 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseCounterDemo extends HookWidget {
+ const UseCounterDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final counter = useCounter(0);
+ final customCounter = useCounter(10, min: 0, max: 20);
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useCounter Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Header
+ const Text(
+ '🔢 useCounter Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Enhanced counter with increment, decrement, set, and reset',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ // Basic Counter
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎯 Basic Counter',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Counter Display
+ Center(
+ child: Container(
+ padding: const EdgeInsets.all(24),
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.primaryContainer,
+ borderRadius: BorderRadius.circular(16),
+ ),
+ child: Text(
+ '${counter.value}',
+ style: TextStyle(
+ fontSize: 48,
+ fontWeight: FontWeight.bold,
+ color: Theme.of(
+ context,
+ ).colorScheme.onPrimaryContainer,
+ ),
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Control Buttons
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ Column(
+ children: [
+ FloatingActionButton(
+ onPressed: counter.dec,
+ heroTag: 'dec1',
+ child: const Icon(Icons.remove),
+ ),
+ const SizedBox(height: 8),
+ const Text('Decrement'),
+ ],
+ ),
+ Column(
+ children: [
+ FloatingActionButton(
+ onPressed: counter.inc,
+ heroTag: 'inc1',
+ backgroundColor: Theme.of(
+ context,
+ ).colorScheme.primary,
+ child: const Icon(Icons.add),
+ ),
+ const SizedBox(height: 8),
+ const Text('Increment'),
+ ],
+ ),
+ Column(
+ children: [
+ FloatingActionButton(
+ onPressed: counter.reset,
+ heroTag: 'reset1',
+ backgroundColor: Colors.orange,
+ child: const Icon(Icons.refresh),
+ ),
+ const SizedBox(height: 8),
+ const Text('Reset'),
+ ],
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 16),
+
+ // Set Value
+ Row(
+ children: [
+ Expanded(
+ child: OutlinedButton(
+ onPressed: () => counter.setter(42),
+ child: const Text('Set to 42'),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: OutlinedButton(
+ onPressed: () => counter.setter(100),
+ child: const Text('Set to 100'),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Custom Counter with Limits
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎮 Counter with Limits',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ 'Min: 0, Max: 20, Initial: 10',
+ style: TextStyle(color: Colors.grey[600]),
+ ),
+ const SizedBox(height: 20),
+
+ // Counter Display with Progress
+ Stack(
+ alignment: Alignment.center,
+ children: [
+ SizedBox(
+ width: 120,
+ height: 120,
+ child: CircularProgressIndicator(
+ value: customCounter.value / 20,
+ strokeWidth: 8,
+ backgroundColor: Colors.grey[300],
+ ),
+ ),
+ Text(
+ '${customCounter.value}',
+ style: const TextStyle(
+ fontSize: 36,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ // Control Buttons
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ ElevatedButton.icon(
+ onPressed: customCounter.value > 0
+ ? customCounter.dec
+ : null,
+ icon: const Icon(Icons.remove),
+ label: const Text('Dec'),
+ ),
+ ElevatedButton.icon(
+ onPressed: customCounter.value < 20
+ ? customCounter.inc
+ : null,
+ icon: const Icon(Icons.add),
+ label: const Text('Inc'),
+ ),
+ ElevatedButton.icon(
+ onPressed: customCounter.reset,
+ icon: const Icon(Icons.refresh),
+ label: const Text('Reset'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.orange,
+ ),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 16),
+
+ // Custom Increment/Decrement
+ Row(
+ children: [
+ Expanded(
+ child: OutlinedButton(
+ onPressed: () => customCounter.inc(5),
+ child: const Text('Inc by 5'),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: OutlinedButton(
+ onPressed: () => customCounter.dec(3),
+ child: const Text('Dec by 3'),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ // Usage Info
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Provides a counter with increment/decrement functions\n'
+ '• Supports custom initial value, min, and max limits\n'
+ '• Includes set() and reset() methods\n'
+ '• Custom step values for inc() and dec()\n'
+ '• Automatically enforces min/max constraints',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useCounter Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Basic counter
+final counter = useCounter(0);
+
+// Counter with options
+final customCounter = useCounter(
+ 10,
+ min: 0,
+ max: 20,
+);
+
+// Usage
+Text('Count: \${counter.value}');
+
+ElevatedButton(
+ onPressed: counter.inc,
+ child: Text('Increment'),
+)
+
+// Custom increment
+counter.inc(5); // Increment by 5
+counter.dec(3); // Decrement by 3
+counter.setter(42); // Set to specific value
+counter.reset(); // Reset to initial value''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_custom_compare_effect_demo.dart b/demo/lib/hooks/use_custom_compare_effect_demo.dart
new file mode 100644
index 0000000..718d3e8
--- /dev/null
+++ b/demo/lib/hooks/use_custom_compare_effect_demo.dart
@@ -0,0 +1,452 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+import 'package:collection/collection.dart';
+
+class UseCustomCompareEffectDemo extends HookWidget {
+ const UseCustomCompareEffectDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ // Demo 1: Deep equality for lists
+ final list1 = useState>([1, 2, 3]);
+ final deepEffectCount = useState(0);
+ final normalEffectCount = useState(0);
+
+ // Normal effect - runs on every state update even if values are same
+ useEffect(() {
+ normalEffectCount.value++;
+ return null;
+ }, [list1.value]);
+
+ // Custom compare effect - only runs when list contents actually change
+ useCustomCompareEffect(
+ () {
+ deepEffectCount.value++;
+ return null;
+ },
+ [list1.value],
+ (prev, next) {
+ if (prev == null && next == null) return true;
+ if (prev == null || next == null) return false;
+ return const DeepCollectionEquality().equals(prev[0], next[0]);
+ },
+ );
+
+ // Demo 2: Threshold-based comparison
+ final sliderValue = useState(50.0);
+ final thresholdEffectCount = useState(0);
+
+ useCustomCompareEffect(
+ () {
+ thresholdEffectCount.value++;
+ return null;
+ },
+ [sliderValue.value],
+ (prev, next) {
+ if (prev == null || next == null) return false;
+ final prevValue = prev[0] as double;
+ final nextValue = next[0] as double;
+ // Only trigger if change is greater than 10
+ return (prevValue - nextValue).abs() < 10;
+ },
+ );
+
+ // Demo 3: Complex object comparison
+ final user = useState>({
+ 'name': 'John',
+ 'age': 30,
+ 'email': 'john@example.com',
+ });
+ final userEffectCount = useState(0);
+
+ useCustomCompareEffect(
+ () {
+ userEffectCount.value++;
+ return null;
+ },
+ [user.value],
+ (prev, next) {
+ if (prev == null || next == null) return false;
+ final prevUser = prev[0] as Map;
+ final nextUser = next[0] as Map;
+ // Only trigger on name or email changes, ignore age
+ return prevUser['name'] == nextUser['name'] &&
+ prevUser['email'] == nextUser['email'];
+ },
+ );
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useCustomCompareEffect Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🔍 useCustomCompareEffect Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Custom dependency comparison for useEffect',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ // Demo 1: Deep List Equality
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '📊 Deep List Equality',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ Text('List 1: ${list1.value}'),
+ const SizedBox(height: 8),
+
+ Row(
+ children: [
+ ElevatedButton(
+ onPressed: () {
+ // Creates new list with same values
+ list1.value = [1, 2, 3];
+ },
+ child: const Text('Same Values (New List)'),
+ ),
+ const SizedBox(width: 12),
+ ElevatedButton(
+ onPressed: () {
+ list1.value = [1, 2, 3, 4];
+ },
+ child: const Text('Add Item'),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 20),
+
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Normal useEffect runs: ${normalEffectCount.value}',
+ ),
+ Text('Custom compare runs: ${deepEffectCount.value}'),
+ const SizedBox(height: 8),
+ Text(
+ 'Custom effect only runs when list contents change!',
+ style: TextStyle(
+ fontSize: 12,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Demo 2: Threshold Comparison
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎚️ Threshold-Based Updates',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ Text('Value: ${sliderValue.value.round()}'),
+ Slider(
+ value: sliderValue.value,
+ min: 0,
+ max: 100,
+ divisions: 100,
+ label: sliderValue.value.round().toString(),
+ onChanged: (value) => sliderValue.value = value,
+ ),
+
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.amber.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: Colors.amber),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Effect runs: ${thresholdEffectCount.value}'),
+ const SizedBox(height: 8),
+ const Text(
+ 'Effect only triggers when change > 10 units',
+ style: TextStyle(fontSize: 12),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Demo 3: Selective Property Comparison
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '👤 Selective Property Updates',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ Text('User: ${user.value}'),
+ const SizedBox(height: 16),
+
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ ElevatedButton(
+ onPressed: () {
+ user.value = {...user.value, 'name': 'Jane'};
+ },
+ child: const Text('Change Name'),
+ ),
+ ElevatedButton(
+ onPressed: () {
+ user.value = {
+ ...user.value,
+ 'age': user.value['age'] + 1,
+ };
+ },
+ child: const Text('Increase Age'),
+ ),
+ ElevatedButton(
+ onPressed: () {
+ user.value = {
+ ...user.value,
+ 'email': 'jane@example.com',
+ };
+ },
+ child: const Text('Change Email'),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 16),
+
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.primaryContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Effect runs: ${userEffectCount.value}'),
+ const SizedBox(height: 8),
+ Text(
+ 'Effect ignores age changes, only tracks name & email',
+ style: TextStyle(
+ fontSize: 12,
+ color: Theme.of(
+ context,
+ ).colorScheme.onPrimaryContainer,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Accepts custom equality function\n'
+ '• Compares dependencies your way\n'
+ '• Prevents unnecessary effect runs\n'
+ '• Perfect for complex objects\n'
+ '• Optimizes performance',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useCustomCompareEffect Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Deep equality for lists
+useCustomCompareEffect(
+ () {
+ print('List contents changed');
+ return null;
+ },
+ [myList],
+ (prev, next) {
+ return DeepCollectionEquality()
+ .equals(prev?[0], next?[0]);
+ },
+);
+
+// Threshold-based updates
+final temperature = useState(20.0);
+
+useCustomCompareEffect(
+ () {
+ // Only alert on significant changes
+ showTemperatureAlert();
+ return null;
+ },
+ [temperature.value],
+ (prev, next) {
+ final diff = (prev![0] - next![0]).abs();
+ return diff < 5; // Ignore < 5 degree changes
+ },
+);
+
+// Selective property tracking
+final formData = useState({
+ 'username': '',
+ 'password': '',
+ 'timestamp': DateTime.now(),
+});
+
+useCustomCompareEffect(
+ () {
+ validateCredentials();
+ return null;
+ },
+ [formData.value],
+ (prev, next) {
+ final p = prev?[0] as Map;
+ final n = next?[0] as Map;
+ // Only run on username/password change
+ return p['username'] == n['username'] &&
+ p['password'] == n['password'];
+ },
+);
+
+// Debounced comparison
+useCustomCompareEffect(
+ () => saveToDatabase(),
+ [searchQuery],
+ (prev, next) {
+ // Custom debounce logic
+ return isSimilar(prev, next);
+ },
+);''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_debounce_demo.dart b/demo/lib/hooks/use_debounce_demo.dart
new file mode 100644
index 0000000..e5b77eb
--- /dev/null
+++ b/demo/lib/hooks/use_debounce_demo.dart
@@ -0,0 +1,374 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseDebounceDemo extends HookWidget {
+ const UseDebounceDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final textInput = useState('');
+ final debouncedValue = useState('');
+ final searchCount = useState(0);
+ final debounceDuration = useState(500);
+
+ // Debounce the update to debouncedValue
+ useDebounce(
+ () {
+ debouncedValue.value = textInput.value;
+ if (textInput.value.isNotEmpty) {
+ searchCount.value++;
+ }
+ },
+ Duration(milliseconds: debounceDuration.value),
+ [textInput.value],
+ );
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useDebounce Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '⏳ useDebounce Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Delay value updates until user stops changing it',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🔍 Search Example',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ TextField(
+ onChanged: (value) => textInput.value = value,
+ decoration: const InputDecoration(
+ labelText: 'Search',
+ hintText: 'Type to search (debounced)...',
+ prefixIcon: Icon(Icons.search),
+ border: OutlineInputBorder(),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Real-time vs Debounced
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(
+ Icons.keyboard,
+ color: Colors.orange,
+ size: 20,
+ ),
+ const SizedBox(width: 8),
+ const Text(
+ 'Real-time: ',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ Expanded(
+ child: Text(
+ textInput.value.isEmpty
+ ? '(empty)'
+ : textInput.value,
+ style: TextStyle(
+ color: textInput.value.isEmpty
+ ? Colors.grey
+ : null,
+ fontStyle: textInput.value.isEmpty
+ ? FontStyle.italic
+ : null,
+ ),
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ const Icon(
+ Icons.timer,
+ color: Colors.blue,
+ size: 20,
+ ),
+ const SizedBox(width: 8),
+ const Text(
+ 'Debounced: ',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ Expanded(
+ child: Text(
+ debouncedValue.value.isEmpty
+ ? '(empty)'
+ : debouncedValue.value,
+ style: TextStyle(
+ color: debouncedValue.value.isEmpty
+ ? Colors.grey
+ : Colors.blue,
+ fontWeight: FontWeight.w500,
+ fontStyle: debouncedValue.value.isEmpty
+ ? FontStyle.italic
+ : null,
+ ),
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 20),
+
+ // Search count
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.green.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: Colors.green),
+ ),
+ child: Row(
+ children: [
+ const Icon(Icons.search, color: Colors.green),
+ const SizedBox(width: 8),
+ Text(
+ 'API calls made: $searchCount',
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ color: Colors.green,
+ ),
+ ),
+ const Spacer(),
+ const Text(
+ 'Saved by debouncing!',
+ style: TextStyle(fontSize: 12, color: Colors.green),
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Debounce duration control
+ const Text(
+ 'Debounce Duration:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ Expanded(
+ child: Slider(
+ value: debounceDuration.value.toDouble(),
+ min: 100,
+ max: 2000,
+ divisions: 19,
+ label: '${debounceDuration.value}ms',
+ onChanged: (value) =>
+ debounceDuration.value = value.round(),
+ ),
+ ),
+ SizedBox(
+ width: 80,
+ child: Text(
+ '${debounceDuration.value}ms',
+ style: const TextStyle(fontWeight: FontWeight.bold),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Comparison with throttle
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.compare_arrows, color: Colors.blue),
+ SizedBox(width: 8),
+ Text(
+ 'Debounce vs Throttle',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Debounce: Waits until user stops changing value\n'
+ '• Throttle: Limits updates to fixed intervals\n'
+ '• Debounce is ideal for search inputs\n'
+ '• Throttle is better for scroll/resize events',
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Delays value updates until stable\n'
+ '• Resets timer on each change\n'
+ '• Perfect for search, validation, auto-save\n'
+ '• Reduces API calls and improves performance',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useDebounce Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Debounce a search function
+final searchQuery = useState('');
+
+useDebounce(
+ () {
+ // This function only executes 500ms after
+ // the user stops typing
+ performSearch(searchQuery.value);
+ },
+ Duration(milliseconds: 500),
+ [searchQuery.value], // Reset timer when query changes
+);
+
+// Auto-save example
+final documentContent = useState('');
+
+useDebounce(
+ () {
+ // Auto-save 2 seconds after user stops editing
+ saveDocument(documentContent.value);
+ },
+ Duration(seconds: 2),
+ [documentContent.value],
+);
+
+// Form validation
+final email = useState('');
+final isValid = useState(null);
+
+useDebounce(
+ () {
+ isValid.value = validateEmail(email.value);
+ },
+ Duration(milliseconds: 800),
+ [email.value],
+);''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_default_demo.dart b/demo/lib/hooks/use_default_demo.dart
new file mode 100644
index 0000000..d1922f5
--- /dev/null
+++ b/demo/lib/hooks/use_default_demo.dart
@@ -0,0 +1,380 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseDefaultDemo extends HookWidget {
+ const UseDefaultDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ // Use default with initial values
+ final textInput = useDefault('', 'Enter text here...');
+ final numberInput = useDefault(null, 0);
+ final selectedOption = useDefault(null, 'Option A');
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useDefault Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Header
+ const Text(
+ '📝 useDefault Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Provide default values for nullable or empty states',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ // Text Input Example
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '✏️ Text Input with Default',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ TextField(
+ onChanged: (value) => textInput.value = value,
+ decoration: InputDecoration(
+ labelText: 'Your Text',
+ hintText: 'Type something...',
+ border: const OutlineInputBorder(),
+ suffixIcon: textInput.value.isNotEmpty
+ ? IconButton(
+ icon: const Icon(Icons.clear),
+ onPressed: () => textInput.value = '',
+ )
+ : null,
+ ),
+ ),
+
+ const SizedBox(height: 16),
+
+ // Display with default
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ 'Displayed Value:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ textInput.value,
+ style: TextStyle(
+ fontSize: 16,
+ fontStyle: textInput.value == 'Enter text here...'
+ ? FontStyle.italic
+ : FontStyle.normal,
+ color: textInput.value == 'Enter text here...'
+ ? Colors.grey
+ : null,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Number Selection Example
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🔢 Number Selection with Default',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ const Text('Select a quantity:'),
+ const SizedBox(height: 12),
+
+ // Number options
+ Wrap(
+ spacing: 8,
+ children: [
+ for (int? num in [null, 1, 5, 10, 25, 50])
+ ChoiceChip(
+ label: Text(num?.toString() ?? 'None'),
+ selected: numberInput.value == num,
+ onSelected: (_) => numberInput.value = num,
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 20),
+
+ // Result display
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.blue.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Row(
+ children: [
+ const Icon(Icons.shopping_cart, color: Colors.blue),
+ const SizedBox(width: 12),
+ Text(
+ 'Quantity: ${numberInput.value ?? 5}',
+ style: const TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const Spacer(),
+ if (numberInput.value == null)
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 4,
+ ),
+ decoration: BoxDecoration(
+ color: Colors.orange,
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: const Text(
+ 'DEFAULT',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 10,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Dropdown Example
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '📋 Dropdown with Default',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ DropdownButtonFormField(
+ value: selectedOption.value,
+ decoration: const InputDecoration(
+ labelText: 'Select Option',
+ border: OutlineInputBorder(),
+ ),
+ items: [
+ const DropdownMenuItem(
+ value: null,
+ child: Text('None selected'),
+ ),
+ ...['Option A', 'Option B', 'Option C', 'Option D'].map(
+ (option) => DropdownMenuItem(
+ value: option,
+ child: Text(option),
+ ),
+ ),
+ ],
+ onChanged: (value) => selectedOption.value = value,
+ ),
+
+ const SizedBox(height: 20),
+
+ // Display selection
+ Row(
+ children: [
+ const Text('Selected: '),
+ Chip(
+ label: Text(selectedOption.value ?? 'Option A'),
+ backgroundColor: selectedOption.value == null
+ ? Colors.orange.withValues(alpha: 0.2)
+ : Theme.of(context).colorScheme.primaryContainer,
+ ),
+ if (selectedOption.value == null) ...[
+ const SizedBox(width: 8),
+ const Text(
+ '(using default)',
+ style: TextStyle(
+ fontSize: 12,
+ color: Colors.orange,
+ ),
+ ),
+ ],
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ // Usage Info
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Provides a default value for nullable or empty states\n'
+ '• Returns DefaultState with value getter/setter only\n'
+ '• Useful for forms, settings, and optional configurations\n'
+ '• Helps avoid null checks and provides fallback values\n'
+ '• Can handle any type: String, int, custom objects, etc.',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useDefault Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Basic usage - (defaultValue, initialValue)
+final username = useDefault('Anonymous', '');
+final quantity = useDefault(1, 5);
+
+// Access current value
+print(username.value); // Current value or default
+
+// Update value
+username.value = 'John Doe';
+quantity.value = 10;
+
+// Set to null triggers default fallback
+username.value = null; // Falls back to 'Anonymous'
+quantity.value = null; // Falls back to 1
+
+// Display pattern
+Text(username.value), // Always non-null
+
+// With nullable types
+final selectedId = useDefault('default-id', 'user-123');
+
+// Form field with default
+TextField(
+ onChanged: (value) => username.value = value,
+ decoration: InputDecoration(
+ hintText: 'Enter name (default: Anonymous)',
+ ),
+),
+
+// Reset by setting to null
+ElevatedButton(
+ onPressed: () => username.value = null,
+ child: Text('Reset to Default'),
+)''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_effect_once_demo.dart b/demo/lib/hooks/use_effect_once_demo.dart
new file mode 100644
index 0000000..bacdc20
--- /dev/null
+++ b/demo/lib/hooks/use_effect_once_demo.dart
@@ -0,0 +1,304 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseEffectOnceDemo extends HookWidget {
+ const UseEffectOnceDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final counter = useState(0);
+ final effectLog = useState>([]);
+ final initData = useState(null);
+
+ useEffectOnce(() {
+ // This runs only once
+ effectLog.value = [
+ ...effectLog.value,
+ '🟢 Effect executed once at ${DateTime.now().toString().substring(11, 19)}',
+ ];
+
+ // Simulate API call
+ Future.delayed(const Duration(seconds: 1), () {
+ initData.value = 'Data loaded successfully!';
+ effectLog.value = [...effectLog.value, '✅ Data fetched'];
+ });
+
+ // Cleanup function
+ return () {
+ effectLog.value = [...effectLog.value, '🔴 Cleanup executed'];
+ };
+ });
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useEffectOnce Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '1️⃣ useEffectOnce Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Run effect only once with optional cleanup',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎯 Effect Behavior',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Counter to show rebuilds don't trigger effect
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Column(
+ children: [
+ Text(
+ 'Rebuild Count: ${counter.value}',
+ style: const TextStyle(fontSize: 16),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Notice: Effect runs only once despite rebuilds',
+ style: TextStyle(fontSize: 12, color: Colors.grey),
+ ),
+ const SizedBox(height: 12),
+ ElevatedButton(
+ onPressed: () => counter.value++,
+ child: const Text('Trigger Rebuild'),
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 20),
+
+ // Data status
+ if (initData.value != null) ...[
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.green.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: Colors.green),
+ ),
+ child: Row(
+ children: [
+ const Icon(Icons.check_circle, color: Colors.green),
+ const SizedBox(width: 8),
+ Text(
+ initData.value!,
+ style: const TextStyle(color: Colors.green),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 20),
+ ],
+
+ // Effect log
+ const Text(
+ 'Effect Log:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+ Container(
+ height: 150,
+ width: double.infinity,
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: ListView.builder(
+ itemCount: effectLog.value.length,
+ itemBuilder: (context, index) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 2),
+ child: Text(
+ effectLog.value[index],
+ style: const TextStyle(
+ color: Colors.white,
+ fontFamily: 'monospace',
+ fontSize: 12,
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Comparison card
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.compare_arrows, color: Colors.blue),
+ SizedBox(width: 8),
+ Text(
+ 'useEffectOnce vs useEffect',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• useEffectOnce: Runs only once, empty deps internally\n'
+ '• useEffect: Can run multiple times based on dependencies\n'
+ '• Both support cleanup functions\n'
+ '• useEffectOnce is cleaner for one-time operations',
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Runs effect only once after mount\n'
+ '• Equivalent to useEffect(() => {}, [])\n'
+ '• Perfect for initialization and data fetching\n'
+ '• Supports cleanup function for unmount',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useEffectOnce Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''useEffectOnce(() {
+ // This runs only once after mount
+ print('Component mounted!');
+
+ // Fetch initial data
+ fetchUserData();
+
+ // Setup subscriptions
+ final subscription = stream.listen((data) {
+ updateState(data);
+ });
+
+ // Return cleanup function
+ return () {
+ print('Cleaning up!');
+ subscription.cancel();
+ };
+});
+
+// Equivalent to:
+useEffect(() {
+ // Your code here
+ return () {
+ // Cleanup
+ };
+}, []); // Empty dependencies''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_error_demo.dart b/demo/lib/hooks/use_error_demo.dart
new file mode 100644
index 0000000..02cc272
--- /dev/null
+++ b/demo/lib/hooks/use_error_demo.dart
@@ -0,0 +1,479 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseErrorDemo extends HookWidget {
+ const UseErrorDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final errorState = useError();
+ final exceptionState = useException();
+ final operationCount = useState(0);
+ final successCount = useState(0);
+ final errorHistory = useState>([]);
+
+ void performRiskyOperation(String operation) {
+ operationCount.value++;
+ try {
+ final random = Random();
+ final shouldFail = random.nextBool();
+
+ if (shouldFail) {
+ switch (operation) {
+ case 'network':
+ throw StateError('Network connection failed');
+ case 'parse':
+ throw ArgumentError('Invalid JSON format');
+ case 'auth':
+ throw UnsupportedError('Authentication failed');
+ case 'custom':
+ throw CustomError('Custom operation failed');
+ }
+ }
+
+ successCount.value++;
+ errorHistory.value = [
+ '✅ ${operation.toUpperCase()} succeeded',
+ ...errorHistory.value.take(9),
+ ];
+ } catch (e) {
+ errorHistory.value = [
+ '❌ ${operation.toUpperCase()} failed: $e',
+ ...errorHistory.value.take(9),
+ ];
+
+ if (e is Error) {
+ errorState.dispatch(e);
+ } else if (e is Exception) {
+ exceptionState.dispatch(e);
+ }
+ }
+ }
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useError & useException Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '⚠️ useError & useException Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Manage error and exception states in your app',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ // Error State Display
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🚨 Current Error State',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Error display
+ if (errorState.value != null) ...[
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.red.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: Colors.red),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.error, color: Colors.red),
+ const SizedBox(width: 8),
+ Text(
+ 'Error: ${errorState.value.runtimeType}',
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ color: Colors.red,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Text(
+ errorState.value.toString(),
+ style: const TextStyle(color: Colors.red),
+ ),
+ ],
+ ),
+ ),
+ ] else ...[
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.green.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: Colors.green),
+ ),
+ child: const Row(
+ children: [
+ Icon(Icons.check_circle, color: Colors.green),
+ SizedBox(width: 8),
+ Text(
+ 'No errors',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ color: Colors.green,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+
+ const SizedBox(height: 20),
+
+ // Exception display
+ if (exceptionState.value != null) ...[
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.orange.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: Colors.orange),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.warning, color: Colors.orange),
+ const SizedBox(width: 8),
+ Text(
+ 'Exception: ${exceptionState.value.runtimeType}',
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ color: Colors.orange,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Text(
+ exceptionState.value.toString(),
+ style: const TextStyle(color: Colors.orange),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Operations
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎮 Risky Operations',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Each operation has a 50% chance of failure',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 20),
+
+ // Operation buttons
+ Wrap(
+ spacing: 12,
+ runSpacing: 12,
+ children: [
+ ElevatedButton.icon(
+ onPressed: () => performRiskyOperation('network'),
+ icon: const Icon(Icons.wifi),
+ label: const Text('Network Call'),
+ ),
+ ElevatedButton.icon(
+ onPressed: () => performRiskyOperation('parse'),
+ icon: const Icon(Icons.code),
+ label: const Text('Parse Data'),
+ ),
+ ElevatedButton.icon(
+ onPressed: () => performRiskyOperation('auth'),
+ icon: const Icon(Icons.lock),
+ label: const Text('Authenticate'),
+ ),
+ ElevatedButton.icon(
+ onPressed: () => performRiskyOperation('custom'),
+ icon: const Icon(Icons.build),
+ label: const Text('Custom Task'),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ // Stats
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ Column(
+ children: [
+ const Text('Operations'),
+ Text(
+ '${operationCount.value}',
+ style: const TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ Column(
+ children: [
+ const Text('Successes'),
+ Text(
+ '${successCount.value}',
+ style: const TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ color: Colors.green,
+ ),
+ ),
+ ],
+ ),
+ Column(
+ children: [
+ const Text('Failures'),
+ Text(
+ '${operationCount.value - successCount.value}',
+ style: const TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ color: Colors.red,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ // History
+ const Text(
+ 'Operation History:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ Container(
+ height: 120,
+ width: double.infinity,
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: errorHistory.value.isEmpty
+ ? const Center(
+ child: Text(
+ 'No operations yet',
+ style: TextStyle(color: Colors.grey),
+ ),
+ )
+ : ListView.builder(
+ itemCount: errorHistory.value.length,
+ itemBuilder: (context, index) {
+ return Text(
+ errorHistory.value[index],
+ style: const TextStyle(
+ color: Colors.white,
+ fontFamily: 'monospace',
+ fontSize: 12,
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• useError manages Error objects\n'
+ '• useException manages Exception objects\n'
+ '• dispatch() stores the error/exception\n'
+ '• value property retrieves current state\n'
+ '• Perfect for error boundaries and recovery',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useError & useException Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Use error state
+final errorState = useError();
+
+// Dispatch errors
+try {
+ someRiskyOperation();
+} catch (e) {
+ if (e is Error) {
+ errorState.dispatch(e);
+ }
+}
+
+// Check for errors
+if (errorState.value != null) {
+ return ErrorWidget(errorState.value!);
+}
+
+// Use exception state
+final exceptionState = useException();
+
+// API call example
+Future fetchData() async {
+ try {
+ final response = await api.get('/data');
+ processData(response);
+ } on NetworkException catch (e) {
+ exceptionState.dispatch(e);
+ } on FormatException catch (e) {
+ exceptionState.dispatch(e);
+ }
+}
+
+// Error boundary pattern
+if (errorState.value != null) {
+ return ErrorRecoveryWidget(
+ error: errorState.value!,
+ onRetry: () {
+ // Clear error by creating new state
+ retry();
+ },
+ );
+}
+
+// Global error handling
+useEffect(() {
+ if (errorState.value != null) {
+ logError(errorState.value!);
+ showErrorSnackbar(context);
+ }
+ return null;
+}, [errorState.value]);''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class CustomError extends Error {
+ final String message;
+ CustomError(this.message);
+
+ @override
+ String toString() => message;
+}
diff --git a/demo/lib/hooks/use_first_mount_state_demo.dart b/demo/lib/hooks/use_first_mount_state_demo.dart
new file mode 100644
index 0000000..6d21dd6
--- /dev/null
+++ b/demo/lib/hooks/use_first_mount_state_demo.dart
@@ -0,0 +1,399 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseFirstMountStateDemo extends HookWidget {
+ const UseFirstMountStateDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final isFirstMount = useFirstMountState();
+ final renderCount = useState(0);
+ final message = useState('');
+ final actions = useState>([]);
+
+ // Track renders
+ useEffect(() {
+ renderCount.value++;
+ actions.value = [
+ '🔄 Render #${renderCount.value} - First mount: $isFirstMount',
+ ...actions.value.take(9),
+ ];
+ return null;
+ });
+
+ // Demonstrate first mount usage
+ useEffect(() {
+ if (isFirstMount) {
+ message.value = '🎉 Welcome! This is your first visit.';
+ } else {
+ message.value = '👋 Welcome back! Component has re-rendered.';
+ }
+ return null;
+ }, [isFirstMount]);
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useFirstMountState Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🚀 useFirstMountState Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Detect if component is in its first render',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎯 First Mount Detection',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // First mount indicator
+ Center(
+ child: Container(
+ padding: const EdgeInsets.all(32),
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: isFirstMount
+ ? Colors.green.withValues(alpha: 0.1)
+ : Colors.blue.withValues(alpha: 0.1),
+ border: Border.all(
+ color: isFirstMount ? Colors.green : Colors.blue,
+ width: 3,
+ ),
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ isFirstMount ? Icons.fiber_new : Icons.refresh,
+ size: 48,
+ color: isFirstMount ? Colors.green : Colors.blue,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ isFirstMount ? 'First Mount' : 'Re-render',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ color: isFirstMount
+ ? Colors.green
+ : Colors.blue,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Message display
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.primaryContainer,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Column(
+ children: [
+ Icon(
+ Icons.message,
+ color: Theme.of(
+ context,
+ ).colorScheme.onPrimaryContainer,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ message.value,
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.w500,
+ color: Theme.of(
+ context,
+ ).colorScheme.onPrimaryContainer,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Render info
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ Column(
+ children: [
+ const Text('Render Count'),
+ const SizedBox(height: 4),
+ Text(
+ '${renderCount.value}',
+ style: const TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ Column(
+ children: [
+ const Text('Is First Mount'),
+ const SizedBox(height: 4),
+ Text(
+ isFirstMount ? 'TRUE' : 'FALSE',
+ style: TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ color: isFirstMount
+ ? Colors.green
+ : Colors.orange,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ // Force re-render button
+ Center(
+ child: ElevatedButton.icon(
+ onPressed: () {
+ // Force a re-render by updating state
+ renderCount.value = renderCount.value;
+ },
+ icon: const Icon(Icons.refresh),
+ label: const Text('Force Re-render'),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Action log
+ const Text(
+ 'Action Log:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+ Container(
+ height: 120,
+ width: double.infinity,
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: actions.value.isEmpty
+ ? const Center(
+ child: Text(
+ 'No actions yet',
+ style: TextStyle(color: Colors.grey),
+ ),
+ )
+ : ListView.builder(
+ itemCount: actions.value.length,
+ itemBuilder: (context, index) {
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 2),
+ child: Text(
+ actions.value[index],
+ style: const TextStyle(
+ color: Colors.white,
+ fontFamily: 'monospace',
+ fontSize: 12,
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Use cases
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.tips_and_updates, color: Colors.amber),
+ SizedBox(width: 8),
+ Text(
+ 'Common Use Cases',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Show welcome messages on first load\n'
+ '• Skip animations on initial render\n'
+ '• Load data only on first mount\n'
+ '• Track user interactions differently\n'
+ '• Initialize third-party libraries once',
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Returns true on first render only\n'
+ '• Returns false on all subsequent renders\n'
+ '• Persists through state changes\n'
+ '• Resets when component unmounts/remounts\n'
+ '• Useful for one-time initialization',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useFirstMountState Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Check if first mount
+final isFirstMount = useFirstMountState();
+
+// Show welcome message
+if (isFirstMount) {
+ showWelcomeDialog();
+}
+
+// Skip animation on first render
+AnimatedContainer(
+ duration: isFirstMount
+ ? Duration.zero
+ : Duration(milliseconds: 300),
+ // ...
+)
+
+// Load data once
+useEffect(() {
+ if (isFirstMount) {
+ loadInitialData();
+ }
+ return null;
+}, []);
+
+// Track analytics differently
+useEffect(() {
+ analytics.track(
+ isFirstMount
+ ? 'page_first_view'
+ : 'page_return_view'
+ );
+ return null;
+}, []);
+
+// Initialize only once
+if (isFirstMount) {
+ ThirdPartySDK.initialize();
+}''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_future_retry_demo.dart b/demo/lib/hooks/use_future_retry_demo.dart
new file mode 100644
index 0000000..46c8343
--- /dev/null
+++ b/demo/lib/hooks/use_future_retry_demo.dart
@@ -0,0 +1,405 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseFutureRetryDemo extends HookWidget {
+ const UseFutureRetryDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final failureRate = useState(50);
+
+ // Simulated API call that can fail
+ Future fetchData() async {
+ await Future.delayed(const Duration(seconds: 2));
+ final random = Random();
+ if (random.nextInt(100) < failureRate.value) {
+ throw Exception('Network error: Failed to fetch data');
+ }
+ return 'Data loaded successfully at ${DateTime.now().toString().substring(11, 19)}';
+ }
+
+ final futureState = useFutureRetry(fetchData(), preserveState: false);
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useFutureRetry Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🔄 useFutureRetry Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Manage async operations with retry capability',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🌐 Network Request Simulator',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Failure rate control
+ const Text(
+ 'Failure Rate:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ Expanded(
+ child: Slider(
+ value: failureRate.value.toDouble(),
+ min: 0,
+ max: 100,
+ divisions: 10,
+ label: '${failureRate.value}%',
+ onChanged: (value) =>
+ failureRate.value = value.round(),
+ ),
+ ),
+ SizedBox(
+ width: 60,
+ child: Text(
+ '${failureRate.value}%',
+ style: const TextStyle(fontWeight: FontWeight.bold),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ // Status display
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(24),
+ decoration: BoxDecoration(
+ color: _getStatusColor(
+ futureState.snapshot,
+ ).withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color: _getStatusColor(futureState.snapshot),
+ width: 2,
+ ),
+ ),
+ child: Column(
+ children: [
+ Icon(
+ _getStatusIcon(futureState.snapshot),
+ size: 48,
+ color: _getStatusColor(futureState.snapshot),
+ ),
+ const SizedBox(height: 16),
+ Text(
+ _getStatusText(futureState.snapshot),
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ color: _getStatusColor(futureState.snapshot),
+ ),
+ ),
+ const SizedBox(height: 8),
+ if (futureState.snapshot.connectionState ==
+ ConnectionState.waiting)
+ const CircularProgressIndicator()
+ else if (futureState.snapshot.hasData)
+ Text(
+ futureState.snapshot.data!,
+ style: const TextStyle(color: Colors.green),
+ textAlign: TextAlign.center,
+ )
+ else if (futureState.snapshot.hasError)
+ Text(
+ futureState.snapshot.error.toString(),
+ style: const TextStyle(color: Colors.red),
+ textAlign: TextAlign.center,
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Action buttons
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ ElevatedButton.icon(
+ onPressed: futureState.retry,
+ icon: const Icon(Icons.refresh),
+ label: const Text('Retry'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.blue,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 24,
+ vertical: 12,
+ ),
+ ),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ // Connection state info
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.info_outline, size: 16),
+ const SizedBox(width: 8),
+ Text(
+ 'Connection State: ${futureState.snapshot.connectionState.name}',
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Text(
+ 'Has Data: ${futureState.snapshot.hasData}',
+ style: const TextStyle(fontSize: 12),
+ ),
+ Text(
+ 'Has Error: ${futureState.snapshot.hasError}',
+ style: const TextStyle(fontSize: 12),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Features
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.stars, color: Colors.amber),
+ SizedBox(width: 8),
+ Text(
+ 'Key Features',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Retry failed operations easily\n'
+ '• Access AsyncSnapshot state\n'
+ '• Preserve or reset state on retry\n'
+ '• Perfect for network requests\n'
+ '• Built on top of useFuture',
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Wraps Flutter\'s useFuture hook\n'
+ '• Provides retry() method\n'
+ '• Re-executes the future on retry\n'
+ '• Manages loading/error states\n'
+ '• Option to preserve previous data',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Color _getStatusColor(AsyncSnapshot snapshot) {
+ if (snapshot.connectionState == ConnectionState.waiting) {
+ return Colors.orange;
+ } else if (snapshot.hasError) {
+ return Colors.red;
+ } else if (snapshot.hasData) {
+ return Colors.green;
+ }
+ return Colors.grey;
+ }
+
+ IconData _getStatusIcon(AsyncSnapshot snapshot) {
+ if (snapshot.connectionState == ConnectionState.waiting) {
+ return Icons.hourglass_empty;
+ } else if (snapshot.hasError) {
+ return Icons.error_outline;
+ } else if (snapshot.hasData) {
+ return Icons.check_circle_outline;
+ }
+ return Icons.circle_outlined;
+ }
+
+ String _getStatusText(AsyncSnapshot snapshot) {
+ if (snapshot.connectionState == ConnectionState.waiting) {
+ return 'Loading...';
+ } else if (snapshot.hasError) {
+ return 'Error Occurred';
+ } else if (snapshot.hasData) {
+ return 'Success!';
+ }
+ return 'Ready';
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useFutureRetry Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Basic usage
+final futureState = useFutureRetry(
+ fetchUserData(),
+);
+
+// Check state
+if (futureState.snapshot.hasData) {
+ return Text(futureState.snapshot.data!);
+} else if (futureState.snapshot.hasError) {
+ return Column(
+ children: [
+ Text('Error: \${futureState.snapshot.error}'),
+ ElevatedButton(
+ onPressed: futureState.retry,
+ child: Text('Retry'),
+ ),
+ ],
+ );
+}
+
+// With initial data
+final userState = useFutureRetry(
+ fetchUser(id),
+ initialData: User.empty(),
+ preserveState: true, // Keep old data
+);
+
+// Network request with retry
+Future> fetchPosts() async {
+ final response = await http.get(...);
+ if (response.statusCode != 200) {
+ throw Exception('Failed to load');
+ }
+ return Post.fromJson(response.body);
+}
+
+final posts = useFutureRetry(fetchPosts());
+
+// Retry on pull to refresh
+RefreshIndicator(
+ onRefresh: () async => posts.retry(),
+ child: ListView(...),
+)''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_interval_demo.dart b/demo/lib/hooks/use_interval_demo.dart
new file mode 100644
index 0000000..b93488b
--- /dev/null
+++ b/demo/lib/hooks/use_interval_demo.dart
@@ -0,0 +1,344 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseIntervalDemo extends HookWidget {
+ const UseIntervalDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final counter = useState(0);
+ final delay = useState(1000);
+ final isRunning = useState(true);
+ final logs = useState>([]);
+
+ useInterval(() {
+ counter.value++;
+ logs.value = [
+ '⏱️ Tick #${counter.value} at ${DateTime.now().toString().substring(11, 19)}',
+ ...logs.value.take(9), // Keep last 10 logs
+ ];
+ }, isRunning.value ? Duration(milliseconds: delay.value) : null);
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useInterval Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '⏱️ useInterval Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Execute functions at regular intervals',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎯 Live Timer',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Counter display
+ Center(
+ child: Container(
+ width: 150,
+ height: 150,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ gradient: LinearGradient(
+ colors: [
+ Theme.of(context).colorScheme.primary,
+ Theme.of(context).colorScheme.secondary,
+ ],
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: Theme.of(
+ context,
+ ).colorScheme.primary.withValues(alpha: 0.3),
+ blurRadius: 20,
+ offset: const Offset(0, 5),
+ ),
+ ],
+ ),
+ child: Center(
+ child: Text(
+ '${counter.value}',
+ style: const TextStyle(
+ fontSize: 48,
+ fontWeight: FontWeight.bold,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Controls
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ ElevatedButton.icon(
+ onPressed: () => isRunning.value = !isRunning.value,
+ icon: Icon(
+ isRunning.value ? Icons.pause : Icons.play_arrow,
+ ),
+ label: Text(isRunning.value ? 'Pause' : 'Start'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: isRunning.value
+ ? Colors.orange
+ : Colors.green,
+ ),
+ ),
+ const SizedBox(width: 12),
+ OutlinedButton.icon(
+ onPressed: () {
+ counter.value = 0;
+ logs.value = ['🔄 Counter reset'];
+ },
+ icon: const Icon(Icons.refresh),
+ label: const Text('Reset'),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ // Interval control
+ const Text(
+ 'Interval Duration:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ Expanded(
+ child: Slider(
+ value: delay.value.toDouble(),
+ min: 100,
+ max: 5000,
+ divisions: 49,
+ label: '${delay.value}ms',
+ onChanged: (value) => delay.value = value.round(),
+ ),
+ ),
+ SizedBox(
+ width: 80,
+ child: Text(
+ '${delay.value}ms',
+ style: const TextStyle(fontWeight: FontWeight.bold),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ ],
+ ),
+
+ // Quick presets
+ Wrap(
+ spacing: 8,
+ children: [
+ ActionChip(
+ label: const Text('100ms'),
+ onPressed: () => delay.value = 100,
+ backgroundColor: delay.value == 100
+ ? Colors.blue
+ : null,
+ ),
+ ActionChip(
+ label: const Text('500ms'),
+ onPressed: () => delay.value = 500,
+ backgroundColor: delay.value == 500
+ ? Colors.blue
+ : null,
+ ),
+ ActionChip(
+ label: const Text('1s'),
+ onPressed: () => delay.value = 1000,
+ backgroundColor: delay.value == 1000
+ ? Colors.blue
+ : null,
+ ),
+ ActionChip(
+ label: const Text('2s'),
+ onPressed: () => delay.value = 2000,
+ backgroundColor: delay.value == 2000
+ ? Colors.blue
+ : null,
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ // Activity log
+ const Text(
+ 'Activity Log:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+ Container(
+ height: 120,
+ width: double.infinity,
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: logs.value.isEmpty
+ ? const Center(
+ child: Text(
+ 'No activity yet',
+ style: TextStyle(color: Colors.grey),
+ ),
+ )
+ : ListView.builder(
+ itemCount: logs.value.length,
+ itemBuilder: (context, index) {
+ return Text(
+ logs.value[index],
+ style: const TextStyle(
+ color: Colors.white,
+ fontFamily: 'monospace',
+ fontSize: 12,
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Executes callback at specified intervals\n'
+ '• Pass null duration to pause/stop\n'
+ '• Automatically cleans up on unmount\n'
+ '• Handles interval changes seamlessly\n'
+ '• Perfect for timers, polling, animations',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useInterval Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Basic interval
+useInterval(() {
+ print('This runs every second');
+}, Duration(seconds: 1));
+
+// With state updates
+final counter = useState(0);
+useInterval(() {
+ counter.value++;
+}, Duration(milliseconds: 100));
+
+// Conditional interval
+final isRunning = useState(true);
+useInterval(
+ () => updateData(),
+ isRunning.value
+ ? Duration(seconds: 5)
+ : null, // null stops interval
+);
+
+// Dynamic delay
+final delay = useState(1000);
+useInterval(
+ () => doWork(),
+ Duration(milliseconds: delay.value),
+);''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_latest_demo.dart b/demo/lib/hooks/use_latest_demo.dart
new file mode 100644
index 0000000..fcd95e9
--- /dev/null
+++ b/demo/lib/hooks/use_latest_demo.dart
@@ -0,0 +1,380 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseLatestDemo extends HookWidget {
+ const UseLatestDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final count = useState(0);
+ final logs = useState>([]);
+
+ // Keep latest value in a ref
+ final latestCount = useLatest(count.value);
+
+ // Demonstrate stale closure problem vs useLatest solution
+ useEffect(() {
+ // This captures the initial count value (stale closure)
+ final capturedCount = count.value;
+
+ Timer? timer;
+ timer = Timer.periodic(const Duration(seconds: 2), (_) {
+ logs.value = [
+ '⏱️ Timer tick at ${DateTime.now().toString().substring(11, 19)}:',
+ ' - Captured value (stale): $capturedCount',
+ ' - Latest value (fresh): $latestCount',
+ ' - Current state value: ${count.value}',
+ '',
+ ...logs.value.take(15),
+ ];
+ });
+
+ return timer.cancel;
+ }, const []); // Empty deps - only run once
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useLatest Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '📌 useLatest Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Always access the latest value in callbacks',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎯 Stale Closure Fix',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Counter control
+ Container(
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Row(
+ children: [
+ const Icon(Icons.countertops, size: 32),
+ const SizedBox(width: 16),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ 'Counter Value:',
+ style: TextStyle(
+ fontSize: 14,
+ color: Colors.grey,
+ ),
+ ),
+ Text(
+ '${count.value}',
+ style: const TextStyle(
+ fontSize: 32,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ ),
+ Column(
+ children: [
+ IconButton.filled(
+ onPressed: () => count.value++,
+ icon: const Icon(Icons.add),
+ ),
+ const SizedBox(height: 8),
+ IconButton.filled(
+ onPressed: () => count.value--,
+ icon: const Icon(Icons.remove),
+ style: IconButton.styleFrom(
+ backgroundColor: Colors.grey,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 16),
+
+ // Info box
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.blue.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: Colors.blue),
+ ),
+ child: const Row(
+ children: [
+ Icon(Icons.info, color: Colors.blue),
+ SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ 'Change the counter and watch how the timer logs show different values!',
+ style: TextStyle(color: Colors.blue),
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Timer log
+ const Text(
+ '📊 Timer Log (updates every 2 seconds):',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+ Container(
+ height: 250,
+ width: double.infinity,
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: logs.value.isEmpty
+ ? const Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ CircularProgressIndicator(),
+ SizedBox(height: 16),
+ Text(
+ 'Waiting for first timer tick...',
+ style: TextStyle(color: Colors.grey),
+ ),
+ ],
+ ),
+ )
+ : ListView.builder(
+ itemCount: logs.value.length,
+ itemBuilder: (context, index) {
+ final log = logs.value[index];
+ Color color = Colors.white;
+ if (log.contains('stale')) {
+ color = Colors.orange;
+ } else if (log.contains('fresh')) {
+ color = Colors.green;
+ } else if (log.contains('Current')) {
+ color = Colors.blue;
+ }
+
+ return Text(
+ log,
+ style: TextStyle(
+ color: color,
+ fontFamily: 'monospace',
+ fontSize: 12,
+ fontWeight: log.contains('Timer tick')
+ ? FontWeight.bold
+ : null,
+ ),
+ );
+ },
+ ),
+ ),
+
+ const SizedBox(height: 16),
+
+ OutlinedButton.icon(
+ onPressed: () => logs.value = [],
+ icon: const Icon(Icons.clear),
+ label: const Text('Clear Log'),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Explanation
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.school, color: Colors.purple),
+ SizedBox(width: 8),
+ Text(
+ 'Understanding the Problem',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Captured value: Stuck at 0 (initial value)\n'
+ '• Latest value: Always current, updates correctly\n'
+ '• Closures capture values at creation time\n'
+ '• useLatest provides a stable ref to current value\n'
+ '• Essential for callbacks with stale closures',
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Returns the latest value directly\n'
+ '• Updates on each render\n'
+ '• Solves stale closure problem\n'
+ '• Perfect for event handlers and timers\n'
+ '• No need to access .value property',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useLatest Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Problem: Stale closure
+final count = useState(0);
+
+useEffect(() {
+ Timer.periodic(Duration(seconds: 1), (_) {
+ // This always prints 0!
+ print(count.value); // Stale value
+ });
+ return null;
+}, []); // Empty deps
+
+// Solution: useLatest
+final count = useState(0);
+final latestCount = useLatest(count.value);
+
+useEffect(() {
+ Timer.periodic(Duration(seconds: 1), (_) {
+ // This prints current value!
+ print(latestCount); // Fresh value
+ });
+ return null;
+}, []); // Empty deps
+
+// Event handlers
+final handleClick = useCallback(() {
+ // Access latest state
+ doSomething(latestCount);
+}, []); // No deps needed!
+
+// Async operations
+useEffect(() {
+ fetchData().then((_) {
+ // Use latest value after async
+ updateUI(latestValue);
+ });
+ return null;
+}, []);''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_lifecycles_demo.dart b/demo/lib/hooks/use_lifecycles_demo.dart
new file mode 100644
index 0000000..13ed47c
--- /dev/null
+++ b/demo/lib/hooks/use_lifecycles_demo.dart
@@ -0,0 +1,390 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseLifecyclesDemo extends HookWidget {
+ const UseLifecyclesDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final lifecycleEvents = useState>([]);
+ final updateCount = useState(0);
+ final isVisible = useState(true);
+
+ // Add initial mount event
+ useEffect(() {
+ lifecycleEvents.value = ['🚀 Widget initialized at ${_timestamp()}'];
+ return null;
+ }, const []);
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useLifecycles Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🔄 useLifecycles Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Manage component lifecycle with mount and unmount callbacks',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎯 Lifecycle Component',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Toggle visibility button
+ Center(
+ child: ElevatedButton.icon(
+ onPressed: () => isVisible.value = !isVisible.value,
+ icon: Icon(
+ isVisible.value
+ ? Icons.visibility_off
+ : Icons.visibility,
+ ),
+ label: Text(
+ isVisible.value ? 'Hide Component' : 'Show Component',
+ ),
+ style: ElevatedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 24,
+ vertical: 12,
+ ),
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Lifecycle component
+ AnimatedContainer(
+ duration: const Duration(milliseconds: 300),
+ height: isVisible.value ? null : 0,
+ child: isVisible.value
+ ? _LifecycleComponent(
+ onLifecycleEvent: (event) {
+ lifecycleEvents.value = [
+ event,
+ ...lifecycleEvents.value.take(19),
+ ];
+ },
+ updateCount: updateCount.value,
+ )
+ : const SizedBox.shrink(),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Update trigger
+ if (isVisible.value) ...[
+ Center(
+ child: OutlinedButton.icon(
+ onPressed: () => updateCount.value++,
+ icon: const Icon(Icons.refresh),
+ label: Text('Trigger Update (${updateCount.value})'),
+ ),
+ ),
+ const SizedBox(height: 24),
+ ],
+
+ // Lifecycle events log
+ const Text(
+ 'Lifecycle Events:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+ Container(
+ height: 200,
+ width: double.infinity,
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: lifecycleEvents.value.isEmpty
+ ? const Center(
+ child: Text(
+ 'No events yet',
+ style: TextStyle(color: Colors.grey),
+ ),
+ )
+ : ListView.builder(
+ itemCount: lifecycleEvents.value.length,
+ itemBuilder: (context, index) {
+ final event = lifecycleEvents.value[index];
+ Color color = Colors.white;
+ if (event.contains('Mounted')) {
+ color = Colors.green;
+ } else if (event.contains('Unmounted')) {
+ color = Colors.red;
+ } else if (event.contains('Updated')) {
+ color = Colors.blue;
+ }
+
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 4),
+ child: Text(
+ event,
+ style: TextStyle(
+ color: color,
+ fontFamily: 'monospace',
+ fontSize: 12,
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+
+ const SizedBox(height: 16),
+
+ Row(
+ children: [
+ OutlinedButton.icon(
+ onPressed: () => lifecycleEvents.value = [],
+ icon: const Icon(Icons.clear),
+ label: const Text('Clear Log'),
+ ),
+ const SizedBox(width: 12),
+ Text(
+ 'Total events: ${lifecycleEvents.value.length}',
+ style: const TextStyle(
+ fontSize: 12,
+ color: Colors.grey,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Combines mount and unmount in one hook\n'
+ '• Mount callback runs after first render\n'
+ '• Unmount callback runs on disposal\n'
+ '• Perfect for resource management\n'
+ '• Useful for subscriptions, timers, listeners',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ String _timestamp() => DateTime.now().toString().substring(11, 19);
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useLifecycles Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Combined mount/unmount management
+useLifecycles(
+ mount: () {
+ print('Component mounted');
+ // Initialize resources
+ controller.init();
+ subscription = stream.listen(handler);
+ },
+ unmount: () {
+ print('Component unmounting');
+ // Cleanup resources
+ controller.dispose();
+ subscription?.cancel();
+ },
+);
+
+// WebSocket connection example
+useLifecycles(
+ mount: () {
+ websocket = WebSocket.connect(url);
+ websocket.listen(onMessage);
+ },
+ unmount: () {
+ websocket?.close();
+ },
+);
+
+// Analytics tracking
+useLifecycles(
+ mount: () {
+ analytics.screenView('UserProfile');
+ startTime = DateTime.now();
+ },
+ unmount: () {
+ final duration = DateTime.now()
+ .difference(startTime);
+ analytics.timing('screen_time', duration);
+ },
+);''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _LifecycleComponent extends HookWidget {
+ final Function(String) onLifecycleEvent;
+ final int updateCount;
+
+ const _LifecycleComponent({
+ required this.onLifecycleEvent,
+ required this.updateCount,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ useLifecycles(
+ mount: () {
+ onLifecycleEvent(
+ '✅ Component Mounted at ${DateTime.now().toString().substring(11, 19)}',
+ );
+ },
+ unmount: () {
+ onLifecycleEvent(
+ '❌ Component Unmounted at ${DateTime.now().toString().substring(11, 19)}',
+ );
+ },
+ );
+
+ useEffect(() {
+ if (updateCount > 0) {
+ onLifecycleEvent(
+ '🔄 Component Updated (count: $updateCount) at ${DateTime.now().toString().substring(11, 19)}',
+ );
+ }
+ return null;
+ }, [updateCount]);
+
+ return Container(
+ padding: const EdgeInsets.all(24),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ Theme.of(context).colorScheme.primary.withValues(alpha: 0.1),
+ Theme.of(context).colorScheme.secondary.withValues(alpha: 0.1),
+ ],
+ ),
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3),
+ ),
+ ),
+ child: Column(
+ children: [
+ const Icon(Icons.widgets, size: 48),
+ const SizedBox(height: 12),
+ const Text(
+ 'Lifecycle Component',
+ style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ 'Update count: $updateCount',
+ style: const TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 12),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ decoration: BoxDecoration(
+ color: Colors.green.withValues(alpha: 0.2),
+ borderRadius: BorderRadius.circular(20),
+ ),
+ child: const Text(
+ 'Component is mounted',
+ style: TextStyle(
+ color: Colors.green,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_list_demo.dart b/demo/lib/hooks/use_list_demo.dart
new file mode 100644
index 0000000..f41ce94
--- /dev/null
+++ b/demo/lib/hooks/use_list_demo.dart
@@ -0,0 +1,374 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseListDemo extends HookWidget {
+ const UseListDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final todoList = useList([
+ 'Buy groceries',
+ 'Read a book',
+ 'Exercise',
+ ]);
+ final newItemController = useTextEditingController();
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useList Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '📝 useList Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Advanced list state management with utility methods',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '✅ Todo List Example',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Add new item
+ Row(
+ children: [
+ Expanded(
+ child: TextField(
+ controller: newItemController,
+ decoration: const InputDecoration(
+ labelText: 'New Todo',
+ hintText: 'Enter a new task...',
+ border: OutlineInputBorder(),
+ ),
+ onSubmitted: (value) {
+ if (value.isNotEmpty) {
+ todoList.add(value);
+ newItemController.clear();
+ }
+ },
+ ),
+ ),
+ const SizedBox(width: 12),
+ IconButton.filled(
+ onPressed: () {
+ if (newItemController.text.isNotEmpty) {
+ todoList.add(newItemController.text);
+ newItemController.clear();
+ }
+ },
+ icon: const Icon(Icons.add),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 20),
+
+ // List display
+ Container(
+ constraints: const BoxConstraints(maxHeight: 300),
+ decoration: BoxDecoration(
+ border: Border.all(color: Colors.grey[300]!),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: todoList.list.isEmpty
+ ? const Center(
+ child: Padding(
+ padding: EdgeInsets.all(40),
+ child: Text(
+ 'No items yet. Add some!',
+ style: TextStyle(color: Colors.grey),
+ ),
+ ),
+ )
+ : ReorderableListView.builder(
+ shrinkWrap: true,
+ itemCount: todoList.list.length,
+ itemBuilder: (context, index) {
+ final item = todoList.list[index];
+ return ListTile(
+ key: ValueKey('$item$index'),
+ leading: CircleAvatar(
+ child: Text('${index + 1}'),
+ ),
+ title: Text(item),
+ trailing: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ IconButton(
+ icon: const Icon(Icons.edit, size: 20),
+ onPressed: () =>
+ _editItem(context, todoList, index),
+ ),
+ IconButton(
+ icon: const Icon(
+ Icons.delete,
+ size: 20,
+ color: Colors.red,
+ ),
+ onPressed: () =>
+ todoList.removeAt(index),
+ ),
+ ],
+ ),
+ );
+ },
+ onReorder: (oldIndex, newIndex) {
+ if (newIndex > oldIndex) newIndex--;
+ final item = todoList.list[oldIndex];
+ todoList.removeAt(oldIndex);
+ todoList.insert(newIndex, item);
+ },
+ ),
+ ),
+
+ const SizedBox(height: 20),
+
+ // List actions
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ ElevatedButton.icon(
+ onPressed: todoList.list.isEmpty
+ ? null
+ : todoList.clear,
+ icon: const Icon(Icons.clear_all),
+ label: const Text('Clear All'),
+ ),
+ OutlinedButton.icon(
+ onPressed: () => todoList.reset(),
+ icon: const Icon(Icons.refresh),
+ label: const Text('Reset'),
+ ),
+ OutlinedButton.icon(
+ onPressed: todoList.list.isEmpty
+ ? null
+ : () {
+ final reversed = todoList.list.reversed
+ .toList();
+ todoList.clear();
+ todoList.addAll(reversed);
+ },
+ icon: const Icon(Icons.swap_vert),
+ label: const Text('Reverse'),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 20),
+
+ // List info
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ Column(
+ children: [
+ const Text('Items'),
+ Text(
+ '${todoList.list.length}',
+ style: const TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ Container(
+ width: 1,
+ height: 40,
+ color: Colors.grey[400],
+ ),
+ Column(
+ children: [
+ const Text('Total Chars'),
+ Text(
+ '${todoList.list.join().length}',
+ style: const TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Enhanced list with push, pop, insert, removeAt\n'
+ '• set() to replace entire list\n'
+ '• clear() to empty the list\n'
+ '• Automatically triggers rebuilds on changes\n'
+ '• Perfect for dynamic lists and collections',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _editItem(BuildContext context, ListAction list, int index) {
+ final controller = TextEditingController(text: list.list[index]);
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('Edit Item'),
+ content: TextField(
+ controller: controller,
+ autofocus: true,
+ decoration: const InputDecoration(labelText: 'Item text'),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Cancel'),
+ ),
+ ElevatedButton(
+ onPressed: () {
+ final value = controller.text;
+ list.removeAt(index);
+ list.insert(index, value);
+ Navigator.pop(context);
+ },
+ child: const Text('Save'),
+ ),
+ ],
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useList Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Initialize list
+final items = useList(['A', 'B', 'C']);
+
+// Access list
+print(items.list); // ['A', 'B', 'C']
+
+// Add items
+items.add('D'); // Add to end
+items.insert(0, 'Z'); // Add at index
+
+// Remove items
+items.removeLast(); // Remove last
+items.removeAt(1); // Remove at index
+
+// Update items
+items.removeAt(0);
+items.insert(0, 'New Value');
+
+// Bulk operations
+items.clear(); // Remove all
+items.addAll(['X', 'Y', 'Z']); // Add multiple
+
+// Use in ListView
+ListView.builder(
+ itemCount: items.list.length,
+ itemBuilder: (context, index) {
+ return Text(items.list[index]);
+ },
+)''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_logger_demo.dart b/demo/lib/hooks/use_logger_demo.dart
new file mode 100644
index 0000000..a5856be
--- /dev/null
+++ b/demo/lib/hooks/use_logger_demo.dart
@@ -0,0 +1,384 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseLoggerDemo extends HookWidget {
+ const UseLoggerDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final count = useState(0);
+ final text = useState('');
+ final enabled = useState(true);
+ final logs = useState>([]);
+
+ // Log component lifecycle and props changes
+ useLogger(
+ 'UseLoggerDemo',
+ props: {
+ 'count': count.value,
+ 'text': text.value,
+ 'enabled': enabled.value,
+ },
+ );
+
+ // Capture console output for display
+ useEffect(() {
+ // In a real app, you'd capture actual console output
+ // For demo, we'll simulate logs
+ logs.value = [
+ '🔄 UseLoggerDemo updated',
+ 'Props: {count: ${count.value}, text: "${text.value}", enabled: ${enabled.value}}',
+ ...logs.value.take(18),
+ ];
+ return null;
+ }, [count.value, text.value, enabled.value]);
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useLogger Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '📝 useLogger Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Debug component lifecycle and state changes',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🔍 Component Debugging',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Interactive controls
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ // Counter control
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Row(
+ children: [
+ const Icon(Icons.numbers),
+ const SizedBox(width: 12),
+ const Text(
+ 'Count:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const Spacer(),
+ IconButton(
+ onPressed: () => count.value--,
+ icon: const Icon(Icons.remove),
+ ),
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.primary.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(20),
+ ),
+ child: Text(
+ '${count.value}',
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ IconButton(
+ onPressed: () => count.value++,
+ icon: const Icon(Icons.add),
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 12),
+
+ // Text input
+ TextField(
+ decoration: const InputDecoration(
+ labelText: 'Text Input',
+ hintText: 'Type to see prop changes...',
+ border: OutlineInputBorder(),
+ prefixIcon: Icon(Icons.text_fields),
+ ),
+ onChanged: (value) => text.value = value,
+ ),
+
+ const SizedBox(height: 12),
+
+ // Toggle control
+ SwitchListTile(
+ title: const Text('Enabled'),
+ subtitle: const Text('Toggle to log state change'),
+ value: enabled.value,
+ onChanged: (value) => enabled.value = value,
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ // Console output
+ const Text(
+ 'Console Output:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+ Container(
+ height: 200,
+ width: double.infinity,
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.black,
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: Colors.grey[700]!),
+ ),
+ child: logs.value.isEmpty
+ ? const Center(
+ child: Text(
+ 'No logs yet',
+ style: TextStyle(
+ color: Colors.grey,
+ fontFamily: 'monospace',
+ ),
+ ),
+ )
+ : ListView.builder(
+ itemCount: logs.value.length,
+ itemBuilder: (context, index) {
+ final log = logs.value[index];
+ Color color = Colors.white;
+ if (log.contains('mounted')) {
+ color = Colors.green;
+ } else if (log.contains('unmounted')) {
+ color = Colors.red;
+ } else if (log.contains('updated')) {
+ color = Colors.blue;
+ }
+
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 2),
+ child: Text(
+ log,
+ style: TextStyle(
+ color: color,
+ fontFamily: 'monospace',
+ fontSize: 12,
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+
+ const SizedBox(height: 16),
+
+ Row(
+ children: [
+ OutlinedButton.icon(
+ onPressed: () => logs.value = [],
+ icon: const Icon(Icons.clear),
+ label: const Text('Clear Logs'),
+ ),
+ const SizedBox(width: 12),
+ Text(
+ 'Total logs: ${logs.value.length}',
+ style: const TextStyle(
+ fontSize: 12,
+ color: Colors.grey,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Debug tips
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.tips_and_updates, color: Colors.amber),
+ SizedBox(width: 8),
+ Text(
+ 'Debug Tips',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Logs mount/unmount lifecycle\n'
+ '• Shows prop changes in console\n'
+ '• Useful for debugging renders\n'
+ '• Disable in production builds\n'
+ '• Great for understanding hook flow',
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Logs component name on mount\n'
+ '• Tracks prop changes between renders\n'
+ '• Shows unmount for cleanup tracking\n'
+ '• Conditional logging in dev mode\n'
+ '• Helps identify unnecessary renders',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useLogger Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Basic logging
+useLogger('MyComponent');
+
+// Log with props
+final count = useState(0);
+final name = useState('John');
+
+useLogger('UserProfile', props: {
+ 'count': count.value,
+ 'name': name.value,
+ 'timestamp': DateTime.now(),
+});
+
+// Conditional logging
+if (kDebugMode) {
+ useLogger('DebugComponent', props: {
+ 'state': currentState,
+ 'errors': errorList,
+ });
+}
+
+// Track specific values
+useLogger('FormWidget', props: {
+ 'isValid': form.isValid,
+ 'isDirty': form.isDirty,
+ 'fields': form.fields.length,
+});
+
+// Console output:
+// MyComponent mounted
+// UserProfile mounted
+// UserProfile updated:
+// {count: 0 → 1}
+// UserProfile unmounted''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_map_demo.dart b/demo/lib/hooks/use_map_demo.dart
new file mode 100644
index 0000000..0281735
--- /dev/null
+++ b/demo/lib/hooks/use_map_demo.dart
@@ -0,0 +1,367 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseMapDemo extends HookWidget {
+ const UseMapDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final settings = useMap({
+ 'theme': 'dark',
+ 'notifications': true,
+ 'fontSize': 16,
+ 'language': 'en',
+ });
+
+ final keyController = useTextEditingController();
+ final valueController = useTextEditingController();
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useMap Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🗺️ useMap Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Key-value state management with Map utilities',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '⚙️ Settings Example',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Settings display
+ ...settings.map.entries.map(
+ (entry) => Padding(
+ padding: const EdgeInsets.only(bottom: 12),
+ child: Row(
+ children: [
+ SizedBox(
+ width: 120,
+ child: Text(
+ entry.key,
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ Expanded(child: _buildValueWidget(entry, settings)),
+ IconButton(
+ icon: const Icon(
+ Icons.delete,
+ size: 20,
+ color: Colors.red,
+ ),
+ onPressed: () => settings.remove(entry.key),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ if (settings.map.isEmpty)
+ const Center(
+ child: Padding(
+ padding: EdgeInsets.all(20),
+ child: Text(
+ 'No settings. Add some!',
+ style: TextStyle(color: Colors.grey),
+ ),
+ ),
+ ),
+
+ const Divider(height: 32),
+
+ // Add new entry
+ const Text(
+ 'Add New Setting:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ Expanded(
+ child: TextField(
+ controller: keyController,
+ decoration: const InputDecoration(
+ labelText: 'Key',
+ hintText: 'e.g., color',
+ border: OutlineInputBorder(),
+ ),
+ ),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: TextField(
+ controller: valueController,
+ decoration: const InputDecoration(
+ labelText: 'Value',
+ hintText: 'e.g., blue',
+ border: OutlineInputBorder(),
+ ),
+ ),
+ ),
+ const SizedBox(width: 8),
+ IconButton.filled(
+ onPressed: () {
+ if (keyController.text.isNotEmpty &&
+ valueController.text.isNotEmpty) {
+ settings.add(
+ keyController.text,
+ valueController.text,
+ );
+ keyController.clear();
+ valueController.clear();
+ }
+ },
+ icon: const Icon(Icons.add),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 20),
+
+ // Map actions
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ ElevatedButton.icon(
+ onPressed: settings.map.isEmpty
+ ? null
+ : settings.reset,
+ icon: const Icon(Icons.clear_all),
+ label: const Text('Clear All'),
+ ),
+ OutlinedButton.icon(
+ onPressed: () => settings.replace({
+ 'theme': 'light',
+ 'notifications': false,
+ 'fontSize': 14,
+ }),
+ icon: const Icon(Icons.refresh),
+ label: const Text('Reset'),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 20),
+
+ // Map info
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Column(
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.info_outline, size: 16),
+ const SizedBox(width: 8),
+ Text('Size: ${settings.map.length} entries'),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ const Icon(Icons.key, size: 16),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ 'Keys: ${settings.map.keys.join(", ")}',
+ style: const TextStyle(fontSize: 12),
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Enhanced Map with set, remove, clear methods\n'
+ '• setAll() to replace entire map\n'
+ '• get() with optional default value\n'
+ '• Automatically triggers rebuilds on changes\n'
+ '• Perfect for settings, configurations, key-value data',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildValueWidget(
+ MapEntry entry,
+ MapAction settings,
+ ) {
+ final value = entry.value;
+
+ if (value is bool) {
+ return Switch(
+ value: value,
+ onChanged: (newValue) => settings.add(entry.key, newValue),
+ );
+ } else if (value is int || value is double) {
+ return Slider(
+ value: (value as num).toDouble(),
+ min: 10,
+ max: 24,
+ divisions: 14,
+ label: value.toString(),
+ onChanged: (newValue) => settings.add(entry.key, newValue.round()),
+ );
+ } else {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ decoration: BoxDecoration(
+ color: Colors.grey[200],
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: Text(value.toString()),
+ );
+ }
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useMap Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Initialize map
+final config = useMap({
+ 'apiUrl': 'https://api.example.com',
+ 'timeout': 30,
+ 'retries': 3,
+});
+
+// Access values
+print(config.map['apiUrl']);
+print(config.get('apiUrl'));
+
+// Add/update values
+config.add('apiUrl', 'https://new-api.com');
+config.add('debug', true);
+
+// Remove entries
+config.remove('debug');
+
+// Bulk operations
+config.addAll({
+ 'feature1': true,
+ 'feature2': false,
+});
+
+// Replace entire map
+config.replace({
+ 'apiUrl': 'https://prod.api.com',
+ 'timeout': 60,
+});
+
+// Reset to initial
+config.reset();
+
+// Check existence
+if (config.map.containsKey('apiUrl')) {
+ // Use the value
+}''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_mount_demo.dart b/demo/lib/hooks/use_mount_demo.dart
new file mode 100644
index 0000000..70f2f10
--- /dev/null
+++ b/demo/lib/hooks/use_mount_demo.dart
@@ -0,0 +1,234 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseMountDemo extends HookWidget {
+ const UseMountDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final mountTime = useState(null);
+ final logMessages = useState>([]);
+
+ useMount(() {
+ mountTime.value = DateTime.now();
+ logMessages.value = [
+ ...logMessages.value,
+ '🟢 Component mounted at ${DateTime.now().toString().substring(11, 19)}',
+ ];
+
+ // Simulate initialization
+ Future.delayed(const Duration(milliseconds: 500), () {
+ logMessages.value = [
+ ...logMessages.value,
+ '✅ Initialization completed',
+ ];
+ });
+ });
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useMount Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🚀 useMount Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Run side effects when component mounts',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '📊 Mount Lifecycle',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ if (mountTime.value != null) ...[
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.green.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: Colors.green),
+ ),
+ child: Row(
+ children: [
+ const Icon(Icons.check_circle, color: Colors.green),
+ const SizedBox(width: 12),
+ Text(
+ 'Mounted at: ${mountTime.value!.toString().substring(11, 19)}',
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 20),
+ ],
+
+ const Text(
+ 'Event Log:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+
+ Container(
+ height: 200,
+ width: double.infinity,
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: ListView.builder(
+ itemCount: logMessages.value.length,
+ itemBuilder: (context, index) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 4),
+ child: Text(
+ logMessages.value[index],
+ style: const TextStyle(
+ color: Colors.white,
+ fontFamily: 'monospace',
+ fontSize: 12,
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Runs only once when the component mounts\n'
+ '• Perfect for initialization logic\n'
+ '• API calls, subscriptions, setup tasks\n'
+ '• No cleanup needed (use useUnmount for cleanup)',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useMount Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''useMount(() {
+ // This runs only once when mounted
+ print('Component mounted!');
+
+ // Initialize data
+ fetchUserData();
+
+ // Setup subscriptions
+ startListening();
+
+ // Log analytics
+ analytics.logScreenView('MyScreen');
+});
+
+// Common use cases:
+useMount(() {
+ // API call on mount
+ fetchInitialData();
+
+ // Start animations
+ animationController.forward();
+
+ // Focus text field
+ focusNode.requestFocus();
+});''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_number_demo.dart b/demo/lib/hooks/use_number_demo.dart
new file mode 100644
index 0000000..31afb4f
--- /dev/null
+++ b/demo/lib/hooks/use_number_demo.dart
@@ -0,0 +1,570 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseNumberDemo extends HookWidget {
+ const UseNumberDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ // Different number use cases
+ final score = useNumber(0, min: 0, max: 1000);
+ final temperature = useNumber(20, min: -50, max: 50);
+ final volume = useNumber(50, min: 0, max: 100);
+ final progress = useNumber(0, min: 0, max: 100);
+
+ final history = useState>([]);
+
+ void addToHistory(String action) {
+ history.value = [
+ '${DateTime.now().toString().substring(11, 19)}: $action',
+ ...history.value.take(9),
+ ];
+ }
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useNumber Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🔢 useNumber Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Numeric value management with bounds (alias for useCounter)',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ // Score counter
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎯 Game Score',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ Center(
+ child: Container(
+ padding: const EdgeInsets.all(32),
+ decoration: BoxDecoration(
+ gradient: RadialGradient(
+ colors: [
+ Colors.blue.withValues(alpha: 0.2),
+ Colors.purple.withValues(alpha: 0.1),
+ ],
+ ),
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(color: Colors.blue, width: 2),
+ ),
+ child: Column(
+ children: [
+ const Text(
+ 'SCORE',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.w500,
+ color: Colors.grey,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ '${score.value}',
+ style: const TextStyle(
+ fontSize: 48,
+ fontWeight: FontWeight.bold,
+ color: Colors.blue,
+ ),
+ ),
+ Text(
+ 'Max: ${score.max ?? 'No limit'}',
+ style: const TextStyle(
+ color: Colors.grey,
+ fontSize: 12,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 20),
+
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ ElevatedButton(
+ onPressed: () {
+ score.inc(10);
+ addToHistory('Score +10 (${score.value})');
+ },
+ child: const Text('+10'),
+ ),
+ ElevatedButton(
+ onPressed: () {
+ score.inc(50);
+ addToHistory('Score +50 (${score.value})');
+ },
+ child: const Text('+50'),
+ ),
+ ElevatedButton(
+ onPressed: () {
+ score.inc(100);
+ addToHistory('Score +100 (${score.value})');
+ },
+ child: const Text('+100'),
+ ),
+ OutlinedButton(
+ onPressed: () {
+ score.reset();
+ addToHistory('Score reset to ${score.value}');
+ },
+ child: const Text('Reset'),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Temperature control
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🌡️ Temperature Control',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ Row(
+ children: [
+ Expanded(
+ child: Column(
+ children: [
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: _getTemperatureColor(
+ temperature.value,
+ ).withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color: _getTemperatureColor(
+ temperature.value,
+ ),
+ ),
+ ),
+ child: Column(
+ children: [
+ Icon(
+ _getTemperatureIcon(temperature.value),
+ size: 40,
+ color: _getTemperatureColor(
+ temperature.value,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ '${temperature.value}°C',
+ style: TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ color: _getTemperatureColor(
+ temperature.value,
+ ),
+ ),
+ ),
+ Text(
+ 'Range: ${temperature.min}°C to ${temperature.max}°C',
+ style: const TextStyle(
+ fontSize: 10,
+ color: Colors.grey,
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 16),
+ Row(
+ mainAxisAlignment:
+ MainAxisAlignment.spaceEvenly,
+ children: [
+ IconButton(
+ onPressed: () {
+ temperature.dec(5);
+ addToHistory(
+ 'Temperature -5°C (${temperature.value}°C)',
+ );
+ },
+ icon: const Icon(Icons.remove),
+ style: IconButton.styleFrom(
+ backgroundColor: Colors.blue,
+ ),
+ ),
+ IconButton(
+ onPressed: () {
+ temperature.inc(5);
+ addToHistory(
+ 'Temperature +5°C (${temperature.value}°C)',
+ );
+ },
+ icon: const Icon(Icons.add),
+ style: IconButton.styleFrom(
+ backgroundColor: Colors.red,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Volume and Progress
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🔊 Volume & Progress Controls',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Volume control
+ Row(
+ children: [
+ const Icon(Icons.volume_down),
+ Expanded(
+ child: Slider(
+ value: volume.value.toDouble(),
+ min: volume.min!.toDouble(),
+ max: volume.max!.toDouble(),
+ divisions: 20,
+ label: '${volume.value}%',
+ onChanged: (value) {
+ volume.setter(value.round());
+ addToHistory('Volume set to ${volume.value}%');
+ },
+ ),
+ ),
+ const Icon(Icons.volume_up),
+ Text('${volume.value}%'),
+ ],
+ ),
+
+ const SizedBox(height: 20),
+
+ // Progress simulation
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text('Progress'),
+ Text('${progress.value}%'),
+ ],
+ ),
+ const SizedBox(height: 8),
+ LinearProgressIndicator(
+ value: progress.value / 100,
+ backgroundColor: Colors.grey[300],
+ valueColor: AlwaysStoppedAnimation(
+ progress.value == 100 ? Colors.green : Colors.blue,
+ ),
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ ElevatedButton(
+ onPressed: progress.value >= 100
+ ? null
+ : () {
+ progress.inc(10);
+ addToHistory(
+ 'Progress +10% (${progress.value}%)',
+ );
+ },
+ child: const Text('+10%'),
+ ),
+ const SizedBox(width: 8),
+ ElevatedButton(
+ onPressed: () {
+ // Simulate random progress
+ final randomProgress = Random().nextInt(101);
+ progress.setter(randomProgress);
+ addToHistory(
+ 'Progress set to $randomProgress%',
+ );
+ },
+ child: const Text('Random'),
+ ),
+ const SizedBox(width: 8),
+ OutlinedButton(
+ onPressed: () {
+ progress.reset();
+ addToHistory(
+ 'Progress reset to ${progress.value}%',
+ );
+ },
+ child: const Text('Reset'),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Action history
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '📜 Action History',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 12),
+ Container(
+ height: 120,
+ width: double.infinity,
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: history.value.isEmpty
+ ? const Center(
+ child: Text(
+ 'Interact with controls to see history',
+ style: TextStyle(color: Colors.grey),
+ ),
+ )
+ : ListView.builder(
+ itemCount: history.value.length,
+ itemBuilder: (context, index) {
+ return Text(
+ history.value[index],
+ style: const TextStyle(
+ color: Colors.white,
+ fontFamily: 'monospace',
+ fontSize: 12,
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'useNumber vs useCounter',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• useNumber is an alias for useCounter\n'
+ '• Same functionality, different semantic meaning\n'
+ '• Use useNumber for mathematical operations\n'
+ '• Use useCounter for counting/tallying\n'
+ '• Both support min/max bounds',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Color _getTemperatureColor(int temp) {
+ if (temp < 0) return Colors.cyan;
+ if (temp < 10) return Colors.blue;
+ if (temp < 20) return Colors.green;
+ if (temp < 30) return Colors.orange;
+ return Colors.red;
+ }
+
+ IconData _getTemperatureIcon(int temp) {
+ if (temp < 0) return Icons.ac_unit;
+ if (temp < 15) return Icons.thermostat;
+ if (temp < 25) return Icons.wb_sunny_outlined;
+ return Icons.whatshot;
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useNumber Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Basic number with bounds
+final score = useNumber(0, min: 0, max: 1000);
+
+// Temperature control
+final temperature = useNumber(20, min: -50, max: 50);
+
+// Volume control
+final volume = useNumber(50, min: 0, max: 100);
+
+// Increment/decrement
+score.inc(10); // Add 10
+score.dec(5); // Subtract 5
+score.inc(); // Add 1 (default)
+
+// Set specific value
+temperature.setter(25);
+
+// Reset to initial value
+score.reset(); // Back to 0
+
+// Access properties
+print('Current: \${score.value}');
+print('Min: \${score.min}');
+print('Max: \${score.max}');
+
+// Mathematical operations
+final calculator = useNumber(0);
+
+calculator.inc(calculator.value); // Double
+calculator.setter(calculator.value * 2); // Multiply
+
+// Progress tracking
+final progress = useNumber(0, min: 0, max: 100);
+
+// Increment by percentage
+progress.inc(25); // 25%
+
+// Set completion
+if (taskCompleted) {
+ progress.setter(100);
+}
+
+// Bounded operations (automatically clamped)
+final health = useNumber(100, min: 0, max: 100);
+health.dec(150); // Will be clamped to 0
+health.inc(50); // Will be clamped to 100
+
+// Use in sliders
+Slider(
+ value: volume.value.toDouble(),
+ min: volume.min!.toDouble(),
+ max: volume.max!.toDouble(),
+ onChanged: (value) => volume.setter(value.round()),
+)''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_orientation_demo.dart b/demo/lib/hooks/use_orientation_demo.dart
new file mode 100644
index 0000000..81d51b2
--- /dev/null
+++ b/demo/lib/hooks/use_orientation_demo.dart
@@ -0,0 +1,390 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseOrientationDemo extends HookWidget {
+ const UseOrientationDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final orientation = useOrientation();
+ final rotationCount = useState(0);
+ final lastOrientation = useState(null);
+
+ // Track orientation changes
+ useEffect(() {
+ if (lastOrientation.value != null &&
+ lastOrientation.value != orientation) {
+ rotationCount.value++;
+ }
+ lastOrientation.value = orientation;
+ return null;
+ }, [orientation]);
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useOrientation Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '📱 useOrientation Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Track device orientation changes in real-time',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🔄 Device Orientation',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Orientation visualization
+ Center(
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 300),
+ width: orientation == Orientation.portrait ? 150 : 250,
+ height: orientation == Orientation.portrait ? 250 : 150,
+ decoration: BoxDecoration(
+ color: orientation == Orientation.portrait
+ ? Colors.blue.withValues(alpha: 0.2)
+ : Colors.green.withValues(alpha: 0.2),
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(
+ color: orientation == Orientation.portrait
+ ? Colors.blue
+ : Colors.green,
+ width: 3,
+ ),
+ ),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ orientation == Orientation.portrait
+ ? Icons.stay_current_portrait
+ : Icons.stay_current_landscape,
+ size: 64,
+ color: orientation == Orientation.portrait
+ ? Colors.blue
+ : Colors.green,
+ ),
+ const SizedBox(height: 16),
+ Text(
+ orientation == Orientation.portrait
+ ? 'PORTRAIT'
+ : 'LANDSCAPE',
+ style: TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ color: orientation == Orientation.portrait
+ ? Colors.blue
+ : Colors.green,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Orientation info
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ 'Current Orientation:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ Chip(
+ label: Text(
+ orientation.name.toUpperCase(),
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ backgroundColor:
+ orientation == Orientation.portrait
+ ? Colors.blue.withValues(alpha: 0.2)
+ : Colors.green.withValues(alpha: 0.2),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text('Rotation Count:'),
+ Text(
+ '${rotationCount.value}',
+ style: const TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Instructions
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.amber.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: Colors.amber),
+ ),
+ child: const Row(
+ children: [
+ Icon(Icons.info, color: Colors.amber),
+ SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ 'Rotate your device to see orientation changes!',
+ style: TextStyle(color: Colors.amber),
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Responsive layout example
+ const Text(
+ 'Responsive Layout:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+ GridView.count(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ crossAxisCount: orientation == Orientation.portrait
+ ? 2
+ : 4,
+ mainAxisSpacing: 8,
+ crossAxisSpacing: 8,
+ children: List.generate(8, (index) {
+ return Container(
+ decoration: BoxDecoration(
+ color: Colors
+ .primaries[index % Colors.primaries.length]
+ .withValues(alpha: 0.2),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(
+ color: Colors
+ .primaries[index % Colors.primaries.length],
+ ),
+ ),
+ child: Center(
+ child: Text(
+ '${index + 1}',
+ style: const TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ );
+ }),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Use cases
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.tips_and_updates, color: Colors.amber),
+ SizedBox(width: 8),
+ Text(
+ 'Common Use Cases',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Responsive layouts\n'
+ '• Adaptive UI components\n'
+ '• Video player controls\n'
+ '• Gallery view adjustments\n'
+ '• Form layout optimization',
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Tracks MediaQuery orientation\n'
+ '• Updates automatically on rotation\n'
+ '• Returns Orientation.portrait or .landscape\n'
+ '• No configuration needed\n'
+ '• Lightweight and efficient',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useOrientation Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Get current orientation
+final orientation = useOrientation();
+
+// Responsive layout
+if (orientation == Orientation.portrait) {
+ return Column(
+ children: widgets,
+ );
+} else {
+ return Row(
+ children: widgets,
+ );
+}
+
+// Adaptive grid
+GridView.count(
+ crossAxisCount: orientation == Orientation.portrait ? 2 : 4,
+ children: items,
+)
+
+// Video player example
+Container(
+ width: orientation == Orientation.landscape
+ ? double.infinity
+ : 300,
+ child: VideoPlayer(),
+)
+
+// Conditional rendering
+orientation == Orientation.landscape
+ ? LandscapeLayout()
+ : PortraitLayout()
+
+// With callback (useOrientationFn)
+useOrientationFn((orientation) {
+ print('Rotated to: \$orientation');
+ analytics.track('orientation_change', {
+ 'orientation': orientation.name,
+ });
+});''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_orientation_fn_demo.dart b/demo/lib/hooks/use_orientation_fn_demo.dart
new file mode 100644
index 0000000..cbbf9a5
--- /dev/null
+++ b/demo/lib/hooks/use_orientation_fn_demo.dart
@@ -0,0 +1,564 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseOrientationFnDemo extends HookWidget {
+ const UseOrientationFnDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final orientationHistory = useState>([]);
+ final orientationChangeCount = useState(0);
+ final currentOrientation = useState(null);
+
+ // Track orientation changes with callback
+ useOrientationFn((orientation) {
+ currentOrientation.value = orientation;
+ orientationChangeCount.value++;
+
+ final timestamp = DateTime.now().toString().substring(11, 19);
+ orientationHistory.value = [
+ '📱 $timestamp: Changed to ${orientation.name.toUpperCase()}',
+ ...orientationHistory.value.take(9),
+ ];
+ });
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useOrientationFn Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🔄 useOrientationFn Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Callback-based orientation change detection',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '📡 Orientation Listener',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Current orientation display
+ if (currentOrientation.value != null) ...[
+ Center(
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 500),
+ curve: Curves.elasticOut,
+ padding: const EdgeInsets.all(32),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors:
+ currentOrientation.value ==
+ Orientation.portrait
+ ? [
+ Colors.blue.withValues(alpha: 0.2),
+ Colors.indigo.withValues(alpha: 0.2),
+ ]
+ : [
+ Colors.green.withValues(alpha: 0.2),
+ Colors.teal.withValues(alpha: 0.2),
+ ],
+ ),
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(
+ color:
+ currentOrientation.value ==
+ Orientation.portrait
+ ? Colors.blue
+ : Colors.green,
+ width: 3,
+ ),
+ boxShadow: [
+ BoxShadow(
+ color:
+ (currentOrientation.value ==
+ Orientation.portrait
+ ? Colors.blue
+ : Colors.green)
+ .withValues(alpha: 0.3),
+ blurRadius: 15,
+ spreadRadius: 2,
+ ),
+ ],
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ AnimatedRotation(
+ turns:
+ currentOrientation.value ==
+ Orientation.portrait
+ ? 0
+ : 0.25,
+ duration: const Duration(milliseconds: 300),
+ child: Icon(
+ Icons.phone_android,
+ size: 80,
+ color:
+ currentOrientation.value ==
+ Orientation.portrait
+ ? Colors.blue
+ : Colors.green,
+ ),
+ ),
+ const SizedBox(height: 16),
+ Text(
+ currentOrientation.value!.name.toUpperCase(),
+ style: TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ color:
+ currentOrientation.value ==
+ Orientation.portrait
+ ? Colors.blue
+ : Colors.green,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ] else ...[
+ const Center(
+ child: Column(
+ children: [
+ CircularProgressIndicator(),
+ SizedBox(height: 16),
+ Text('Waiting for orientation data...'),
+ ],
+ ),
+ ),
+ ],
+
+ const SizedBox(height: 32),
+
+ // Statistics
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ _buildStatCard(
+ 'Total Changes',
+ '${orientationChangeCount.value}',
+ Icons.swap_horiz,
+ Colors.orange,
+ ),
+ _buildStatCard(
+ 'Current Mode',
+ currentOrientation.value?.name.toUpperCase() ??
+ 'Unknown',
+ currentOrientation.value == Orientation.portrait
+ ? Icons.stay_current_portrait
+ : Icons.stay_current_landscape,
+ currentOrientation.value == Orientation.portrait
+ ? Colors.blue
+ : Colors.green,
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ // Instructions
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ Colors.amber.withValues(alpha: 0.1),
+ Colors.orange.withValues(alpha: 0.1),
+ ],
+ ),
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: Colors.amber),
+ ),
+ child: Row(
+ children: [
+ const Icon(
+ Icons.rotate_right,
+ color: Colors.amber,
+ size: 24,
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ 'Try rotating your device!',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ color: Colors.amber,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ 'The callback will be triggered automatically on orientation changes',
+ style: TextStyle(
+ fontSize: 12,
+ color: Colors.amber[700],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // History log
+ const Text(
+ '📋 Orientation Change History:',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Container(
+ height: 150,
+ width: double.infinity,
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: Colors.grey[700]!),
+ ),
+ child: orientationHistory.value.isEmpty
+ ? const Center(
+ child: Text(
+ 'Rotate device to see orientation changes...',
+ style: TextStyle(color: Colors.grey),
+ ),
+ )
+ : ListView.builder(
+ itemCount: orientationHistory.value.length,
+ itemBuilder: (context, index) {
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 4),
+ child: Text(
+ orientationHistory.value[index],
+ style: const TextStyle(
+ color: Colors.white,
+ fontFamily: 'monospace',
+ fontSize: 12,
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+
+ const SizedBox(height: 16),
+
+ // Clear history button
+ Center(
+ child: OutlinedButton.icon(
+ onPressed: () {
+ orientationHistory.value = [];
+ orientationChangeCount.value = 0;
+ },
+ icon: const Icon(Icons.clear_all),
+ label: const Text('Clear History'),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Comparison with useOrientation
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.compare_arrows, color: Colors.blue),
+ SizedBox(width: 8),
+ Text(
+ 'useOrientation vs useOrientationFn',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Table(
+ border: TableBorder.all(color: Colors.grey[300]!),
+ children: const [
+ TableRow(
+ decoration: BoxDecoration(color: Colors.black12),
+ children: [
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text(
+ 'useOrientation',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ ),
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text(
+ 'useOrientationFn',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ ),
+ ],
+ ),
+ TableRow(
+ children: [
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text('Returns current orientation'),
+ ),
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text('Calls function on change'),
+ ),
+ ],
+ ),
+ TableRow(
+ children: [
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text('Reactive value'),
+ ),
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text('Event-driven callback'),
+ ),
+ ],
+ ),
+ TableRow(
+ children: [
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text('Use for UI rendering'),
+ ),
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text('Use for side effects'),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Registers callback for orientation changes\n'
+ '• Triggers callback immediately on change\n'
+ '• Perfect for side effects and analytics\n'
+ '• More performant for event handling\n'
+ '• Use when you don\'t need the current value',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildStatCard(
+ String label,
+ String value,
+ IconData icon,
+ Color color,
+ ) {
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: color.withValues(alpha: 0.3)),
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(icon, color: color, size: 24),
+ const SizedBox(height: 8),
+ Text(
+ value,
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ color: color,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ label,
+ style: const TextStyle(fontSize: 12),
+ textAlign: TextAlign.center,
+ ),
+ ],
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useOrientationFn Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Basic callback usage
+useOrientationFn((orientation) {
+ print('Orientation changed to: \$orientation');
+});
+
+// Analytics tracking
+useOrientationFn((orientation) {
+ analytics.track('orientation_change', {
+ 'orientation': orientation.name,
+ 'timestamp': DateTime.now().toIso8601String(),
+ });
+});
+
+// Update app settings
+useOrientationFn((orientation) {
+ if (orientation == Orientation.landscape) {
+ hideUI();
+ enterFullscreen();
+ } else {
+ showUI();
+ exitFullscreen();
+ }
+});
+
+// Side effects on change
+final orientationHistory = useState>([]);
+
+useOrientationFn((orientation) {
+ final timestamp = DateTime.now();
+ orientationHistory.value = [
+ 'Changed to \${orientation.name} at \$timestamp',
+ ...orientationHistory.value.take(9),
+ ];
+});
+
+// Conditional actions
+useOrientationFn((orientation) {
+ if (orientation == Orientation.landscape) {
+ // Landscape-specific logic
+ videoPlayer.enterFullscreen();
+ systemChrome.hideSystemUI();
+ } else {
+ // Portrait-specific logic
+ videoPlayer.exitFullscreen();
+ systemChrome.showSystemUI();
+ }
+});
+
+// Performance monitoring
+useOrientationFn((orientation) {
+ final stopwatch = Stopwatch()..start();
+
+ // Perform expensive operation
+ rebuildExpensiveWidget();
+
+ stopwatch.stop();
+ print('Orientation change handling took: '
+ '\${stopwatch.elapsedMilliseconds}ms');
+});''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_previous_distinct_demo.dart b/demo/lib/hooks/use_previous_distinct_demo.dart
new file mode 100644
index 0000000..81ee75b
--- /dev/null
+++ b/demo/lib/hooks/use_previous_distinct_demo.dart
@@ -0,0 +1,533 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UsePreviousDistinctDemo extends HookWidget {
+ const UsePreviousDistinctDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ // Basic example with primitive value
+ final counter = useState(0);
+ final previousCounter = usePreviousDistinct(counter.value);
+
+ // Example with object comparison
+ final user = useState(User(id: 1, name: 'John', age: 25));
+ final previousUser = usePreviousDistinct(
+ user.value,
+ (prev, next) => prev.id == next.id && prev.name == next.name,
+ );
+
+ // Example with case-insensitive string comparison
+ final text = useState('Hello');
+ final previousText = usePreviousDistinct(
+ text.value,
+ (prev, next) => prev.toLowerCase() == next.toLowerCase(),
+ );
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('usePreviousDistinct Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Header
+ const Text(
+ '⏮️ usePreviousDistinct Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Track previous distinct values with custom comparison logic',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ // Basic Counter Example
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🔢 Basic Counter Example',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ Column(
+ children: [
+ Container(
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.primaryContainer,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Text(
+ '${counter.value}',
+ style: TextStyle(
+ fontSize: 32,
+ fontWeight: FontWeight.bold,
+ color: Theme.of(
+ context,
+ ).colorScheme.onPrimaryContainer,
+ ),
+ ),
+ ),
+ const SizedBox(height: 8),
+ const Text('Current'),
+ ],
+ ),
+ const Icon(Icons.arrow_forward, size: 32),
+ Column(
+ children: [
+ Container(
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ color: Colors.grey[300],
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Text(
+ previousCounter != null
+ ? '$previousCounter'
+ : 'null',
+ style: TextStyle(
+ fontSize: 32,
+ fontWeight: FontWeight.bold,
+ color: Colors.grey[700],
+ ),
+ ),
+ ),
+ const SizedBox(height: 8),
+ const Text('Previous'),
+ ],
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ ElevatedButton.icon(
+ onPressed: () => counter.value++,
+ icon: const Icon(Icons.add),
+ label: const Text('Increment'),
+ ),
+ ElevatedButton.icon(
+ onPressed: () => counter.value--,
+ icon: const Icon(Icons.remove),
+ label: const Text('Decrement'),
+ ),
+ ElevatedButton.icon(
+ onPressed: () => counter.value = counter.value,
+ icon: const Icon(Icons.refresh),
+ label: const Text('Same Value'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.orange,
+ ),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 16),
+ Text(
+ 'Notice: Setting the same value doesn\'t update the previous value',
+ style: TextStyle(color: Colors.grey[600], fontSize: 12),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Object Comparison Example
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '👤 Object Comparison Example',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ 'Custom comparison: only tracks changes if ID or name changes',
+ style: TextStyle(color: Colors.grey[600]),
+ ),
+ const SizedBox(height: 20),
+
+ Row(
+ children: [
+ Expanded(
+ child: _buildUserCard(
+ 'Current User',
+ user.value,
+ Theme.of(context).colorScheme.primaryContainer,
+ Theme.of(context).colorScheme.onPrimaryContainer,
+ ),
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: _buildUserCard(
+ 'Previous User',
+ previousUser,
+ Colors.grey[300]!,
+ Colors.grey[700]!,
+ ),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ ElevatedButton(
+ onPressed: () {
+ user.value = User(
+ id: user.value.id + 1,
+ name: user.value.name,
+ age: user.value.age,
+ );
+ },
+ child: const Text('Change ID'),
+ ),
+ ElevatedButton(
+ onPressed: () {
+ user.value = User(
+ id: user.value.id,
+ name: 'Jane',
+ age: user.value.age,
+ );
+ },
+ child: const Text('Change Name'),
+ ),
+ ElevatedButton(
+ onPressed: () {
+ user.value = User(
+ id: user.value.id,
+ name: user.value.name,
+ age: user.value.age + 1,
+ );
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.orange,
+ ),
+ child: const Text('Change Age (Ignored)'),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 16),
+ Text(
+ 'Age changes are ignored by our custom comparison function',
+ style: TextStyle(color: Colors.grey[600], fontSize: 12),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Case-Insensitive String Example
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '📝 Case-Insensitive String Example',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ Row(
+ children: [
+ Expanded(
+ child: Column(
+ children: [
+ const Text(
+ 'Current',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.primaryContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Text(
+ text.value,
+ style: TextStyle(
+ fontSize: 18,
+ color: Theme.of(
+ context,
+ ).colorScheme.onPrimaryContainer,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 16),
+ child: Icon(Icons.arrow_forward),
+ ),
+ Expanded(
+ child: Column(
+ children: [
+ const Text(
+ 'Previous',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[300],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Text(
+ previousText ?? 'null',
+ style: TextStyle(
+ fontSize: 18,
+ color: Colors.grey[700],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ TextField(
+ onChanged: (value) => text.value = value,
+ decoration: const InputDecoration(
+ labelText: 'Enter text',
+ hintText: 'Try changing case...',
+ border: OutlineInputBorder(),
+ ),
+ controller: TextEditingController(text: text.value),
+ ),
+
+ const SizedBox(height: 16),
+
+ Row(
+ children: [
+ Expanded(
+ child: OutlinedButton(
+ onPressed: () =>
+ text.value = text.value.toUpperCase(),
+ child: const Text('UPPERCASE'),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: OutlinedButton(
+ onPressed: () =>
+ text.value = text.value.toLowerCase(),
+ child: const Text('lowercase'),
+ ),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 16),
+ Text(
+ 'Case changes alone won\'t update the previous value',
+ style: TextStyle(color: Colors.grey[600], fontSize: 12),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ // Usage Info
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Tracks the previous distinct value of a variable\n'
+ '• Only updates when the value actually changes\n'
+ '• Uses default equality (==) or custom comparison function\n'
+ '• Returns null on first render (no previous value)\n'
+ '• Perfect for detecting meaningful changes in complex objects',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildUserCard(
+ String title,
+ User? user,
+ Color bgColor,
+ Color textColor,
+ ) {
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: bgColor,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ title,
+ style: TextStyle(fontWeight: FontWeight.bold, color: textColor),
+ ),
+ const SizedBox(height: 8),
+ if (user != null) ...[
+ Text('ID: ${user.id}', style: TextStyle(color: textColor)),
+ Text('Name: ${user.name}', style: TextStyle(color: textColor)),
+ Text('Age: ${user.age}', style: TextStyle(color: textColor)),
+ ] else
+ Text('null', style: TextStyle(color: textColor)),
+ ],
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('usePreviousDistinct Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Basic usage with primitive value
+final counter = useState(0);
+final previousCounter = usePreviousDistinct(counter.value);
+
+// Custom comparison for objects
+final user = useState(User(id: 1, name: 'John'));
+final previousUser = usePreviousDistinct(
+ user.value,
+ (prev, next) => prev.id == next.id &&
+ prev.name == next.name,
+);
+
+// Case-insensitive string comparison
+final text = useState('Hello');
+final previousText = usePreviousDistinct(
+ text.value,
+ (prev, next) =>
+ prev.toLowerCase() == next.toLowerCase(),
+);
+
+// Usage in widget
+Text('Current: \${counter.value}');
+Text('Previous: \${previousCounter ?? "none"}');
+
+// The hook only updates previous value when
+// the comparison function returns false''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+// Simple User class for demo
+class User {
+ final int id;
+ final String name;
+ final int age;
+
+ User({required this.id, required this.name, required this.age});
+}
diff --git a/demo/lib/hooks/use_scroll_demo.dart b/demo/lib/hooks/use_scroll_demo.dart
new file mode 100644
index 0000000..2e4d3be
--- /dev/null
+++ b/demo/lib/hooks/use_scroll_demo.dart
@@ -0,0 +1,342 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseScrollDemo extends HookWidget {
+ const UseScrollDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final scroll = useScroll();
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useScroll Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: Column(
+ children: [
+ // Status Bar
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(16),
+ color: Theme.of(context).colorScheme.surfaceContainerHighest,
+ child: Column(
+ children: [
+ const Text(
+ '📊 Scroll Position Info',
+ style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ _buildInfoItem(
+ 'X Position',
+ '${scroll.x.toStringAsFixed(1)}px',
+ Icons.height,
+ Colors.blue,
+ ),
+ _buildInfoItem(
+ 'Y Position',
+ '${scroll.y.toStringAsFixed(1)}px',
+ Icons.swap_vert,
+ Colors.green,
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+
+ // Scrollable Content
+ Expanded(
+ child: SingleChildScrollView(
+ controller: scroll.controller,
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '📜 useScroll Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Track scroll position in real-time',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ // Scroll Content
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎯 Real-time Scroll Tracking',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 16),
+
+ Text(
+ 'Scroll this content to see the position values update in real-time above.',
+ style: TextStyle(
+ fontSize: 16,
+ color: Colors.grey[700],
+ ),
+ ),
+ const SizedBox(height: 24),
+
+ // Progress Indicators
+ Row(
+ children: [
+ const Text('Y Position: '),
+ Expanded(
+ child: LinearProgressIndicator(
+ value: scroll.controller.hasClients
+ ? (scroll.y /
+ scroll
+ .controller
+ .position
+ .maxScrollExtent)
+ .clamp(0.0, 1.0)
+ : 0.0,
+ backgroundColor: Colors.grey[300],
+ valueColor: AlwaysStoppedAnimation(
+ Theme.of(context).colorScheme.primary,
+ ),
+ ),
+ ),
+ const SizedBox(width: 8),
+ Text(
+ '${(scroll.controller.hasClients ? (scroll.y / scroll.controller.position.maxScrollExtent * 100).clamp(0.0, 100.0) : 0.0).toStringAsFixed(1)}%',
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ // Content Blocks to enable scrolling
+ ...List.generate(
+ 10,
+ (index) => Card(
+ margin: const EdgeInsets.only(bottom: 16),
+ child: Padding(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Content Block ${index + 1}',
+ style: const TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ 'This is content block ${index + 1}. Keep scrolling to see how the scroll position updates in real-time. '
+ 'The useScroll hook tracks both X and Y coordinates of the scroll position, making it easy to create '
+ 'scroll-aware components and animations.',
+ style: TextStyle(color: Colors.grey[700]),
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ Icon(
+ Icons.info_outline,
+ color: Theme.of(context).colorScheme.primary,
+ size: 16,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Current Y position: ${scroll.y.toStringAsFixed(1)}px',
+ style: TextStyle(
+ color: Theme.of(
+ context,
+ ).colorScheme.primary,
+ fontSize: 12,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ // Usage Info
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Tracks scroll position in real-time\n'
+ '• Returns x and y coordinates\n'
+ '• Works with any ScrollController\n'
+ '• Perfect for scroll-based animations and effects\n'
+ '• Automatically updates when scroll position changes',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 100), // Extra space for scrolling
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+
+ // Floating Action Buttons for scroll control
+ floatingActionButton: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ FloatingActionButton.small(
+ onPressed: () {
+ scroll.controller.animateTo(
+ 0,
+ duration: const Duration(milliseconds: 500),
+ curve: Curves.easeInOut,
+ );
+ },
+ heroTag: 'scroll_top',
+ child: const Icon(Icons.keyboard_arrow_up),
+ ),
+ const SizedBox(height: 8),
+ FloatingActionButton.small(
+ onPressed: () {
+ scroll.controller.animateTo(
+ scroll.controller.position.maxScrollExtent,
+ duration: const Duration(milliseconds: 500),
+ curve: Curves.easeInOut,
+ );
+ },
+ heroTag: 'scroll_bottom',
+ child: const Icon(Icons.keyboard_arrow_down),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildInfoItem(
+ String label,
+ String value,
+ IconData icon,
+ Color color,
+ ) {
+ return Column(
+ children: [
+ Icon(icon, color: color, size: 24),
+ const SizedBox(height: 4),
+ Text(
+ value,
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ color: color,
+ ),
+ ),
+ Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)),
+ ],
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useScroll Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''final scroll = useScroll();
+
+// Use in your widget
+SingleChildScrollView(
+ controller: scroll.controller,
+ child: Column(
+ children: [
+ Text('X: \${scroll.x.toStringAsFixed(1)}'),
+ Text('Y: \${scroll.y.toStringAsFixed(1)}'),
+
+ // Your scrollable content here
+ ...buildContent(),
+ ],
+ ),
+)
+
+// scroll.x - horizontal scroll position
+// scroll.y - vertical scroll position
+// Both update automatically as user scrolls''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_scrolling_demo.dart b/demo/lib/hooks/use_scrolling_demo.dart
new file mode 100644
index 0000000..86dcb85
--- /dev/null
+++ b/demo/lib/hooks/use_scrolling_demo.dart
@@ -0,0 +1,395 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseScrollingDemo extends HookWidget {
+ const UseScrollingDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final scrolling = useScrolling();
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useScrolling Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: Column(
+ children: [
+ // Status Bar
+ AnimatedContainer(
+ duration: const Duration(milliseconds: 200),
+ width: double.infinity,
+ padding: const EdgeInsets.all(16),
+ color: scrolling.isScrolling
+ ? Colors.orange.withValues(alpha: 0.1)
+ : Theme.of(context).colorScheme.surfaceContainerHighest,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ AnimatedSwitcher(
+ duration: const Duration(milliseconds: 200),
+ child: Icon(
+ scrolling.isScrolling
+ ? Icons.directions_run
+ : Icons.accessibility,
+ key: ValueKey(scrolling.isScrolling),
+ color: scrolling.isScrolling ? Colors.orange : Colors.grey,
+ size: 24,
+ ),
+ ),
+ const SizedBox(width: 12),
+ Text(
+ scrolling.isScrolling
+ ? 'Currently Scrolling...'
+ : 'Not Scrolling',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ color: scrolling.isScrolling ? Colors.orange : Colors.grey,
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ // Scrollable Content
+ Expanded(
+ child: SingleChildScrollView(
+ controller: scrolling.controller,
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🏃 useScrolling Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Detect when the user is actively scrolling',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ // Live Demo Card
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎯 Real-time Scroll Detection',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 16),
+
+ Text(
+ 'Start scrolling this content to see the status change above. '
+ 'The hook detects when scrolling starts and stops.',
+ style: TextStyle(
+ fontSize: 16,
+ color: Colors.grey[700],
+ ),
+ ),
+ const SizedBox(height: 24),
+
+ // Status Indicator
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: scrolling.isScrolling
+ ? Colors.orange.withValues(alpha: 0.1)
+ : Colors.green.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(
+ color: scrolling.isScrolling
+ ? Colors.orange
+ : Colors.green,
+ width: 2,
+ ),
+ ),
+ child: Row(
+ children: [
+ Icon(
+ scrolling.isScrolling
+ ? Icons.play_arrow
+ : Icons.pause,
+ color: scrolling.isScrolling
+ ? Colors.orange
+ : Colors.green,
+ ),
+ const SizedBox(width: 12),
+ Text(
+ scrolling.isScrolling
+ ? 'Scrolling Active'
+ : 'Scrolling Inactive',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ color: scrolling.isScrolling
+ ? Colors.orange
+ : Colors.green,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ // Use Cases Card
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.build, color: Colors.blue),
+ SizedBox(width: 8),
+ Text(
+ 'Common Use Cases',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Show/hide floating action buttons during scroll\n'
+ '• Pause expensive animations while scrolling\n'
+ '• Display scroll indicators or progress bars\n'
+ '• Optimize performance by disabling effects during scroll\n'
+ '• Trigger analytics events for scroll interactions',
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ // Content Blocks to enable scrolling
+ ...List.generate(
+ 15,
+ (index) => Card(
+ margin: const EdgeInsets.only(bottom: 16),
+ child: Padding(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ CircleAvatar(
+ backgroundColor: Theme.of(
+ context,
+ ).colorScheme.primary,
+ radius: 16,
+ child: Text(
+ '${index + 1}',
+ style: const TextStyle(
+ color: Colors.white,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Text(
+ 'Scroll Content Item ${index + 1}',
+ style: const TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Text(
+ 'This is scroll content item ${index + 1}. Notice how the scrolling status updates '
+ 'in real-time as you scroll through this content. The useScrolling hook makes it easy '
+ 'to detect scroll activity and respond accordingly in your UI.',
+ style: TextStyle(color: Colors.grey[700]),
+ ),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ Icon(
+ scrolling.isScrolling
+ ? Icons.visibility
+ : Icons.visibility_off,
+ color: scrolling.isScrolling
+ ? Colors.orange
+ : Colors.grey,
+ size: 16,
+ ),
+ const SizedBox(width: 6),
+ Text(
+ scrolling.isScrolling
+ ? 'Scroll detected!'
+ : 'No scroll activity',
+ style: TextStyle(
+ color: scrolling.isScrolling
+ ? Colors.orange
+ : Colors.grey,
+ fontSize: 12,
+ fontStyle: FontStyle.italic,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ // Usage Info
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Returns true when user is actively scrolling\n'
+ '• Returns false when scroll stops\n'
+ '• Works with any ScrollController\n'
+ '• Useful for performance optimizations\n'
+ '• Perfect for conditional UI updates during scroll',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 100), // Extra space for scrolling
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+
+ // Conditional FAB based on scroll state
+ floatingActionButton: AnimatedSwitcher(
+ duration: const Duration(milliseconds: 300),
+ child: scrolling.isScrolling
+ ? null // Hide FAB while scrolling
+ : FloatingActionButton(
+ onPressed: () {
+ scrolling.controller.animateTo(
+ 0,
+ duration: const Duration(milliseconds: 500),
+ curve: Curves.easeInOut,
+ );
+ },
+ child: const Icon(Icons.keyboard_arrow_up),
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useScrolling Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''final scrolling = useScrolling();
+
+// Use in your widget
+Scaffold(
+ body: SingleChildScrollView(
+ controller: scrolling.controller,
+ child: Column(
+ children: [
+ // Your content here
+ ],
+ ),
+ ),
+
+ // Conditional FAB - hide while scrolling
+ floatingActionButton: scrolling.isScrolling
+ ? null
+ : FloatingActionButton(
+ onPressed: () => scrollToTop(),
+ child: Icon(Icons.arrow_upward),
+ ),
+)
+
+// Performance optimization example
+if (!scrolling.isScrolling) {
+ // Run expensive animations only when not scrolling
+ startComplexAnimation();
+}''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_set_demo.dart b/demo/lib/hooks/use_set_demo.dart
new file mode 100644
index 0000000..4205a21
--- /dev/null
+++ b/demo/lib/hooks/use_set_demo.dart
@@ -0,0 +1,394 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseSetDemo extends HookWidget {
+ const UseSetDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final tags = useSet({'flutter', 'dart', 'mobile'});
+ final newTagController = useTextEditingController();
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useSet Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🏷️ useSet Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Manage unique values with Set operations',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🏷️ Tag Manager',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Add new tag
+ Row(
+ children: [
+ Expanded(
+ child: TextField(
+ controller: newTagController,
+ decoration: const InputDecoration(
+ labelText: 'New Tag',
+ hintText: 'Enter a unique tag...',
+ border: OutlineInputBorder(),
+ prefixIcon: Icon(Icons.label),
+ ),
+ onSubmitted: (value) {
+ if (value.isNotEmpty) {
+ final exists = tags.set.contains(value);
+ if (!exists) {
+ tags.add(value);
+ newTagController.clear();
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Added tag: $value'),
+ backgroundColor: Colors.green,
+ duration: const Duration(seconds: 2),
+ ),
+ );
+ } else {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ 'Tag already exists: $value',
+ ),
+ backgroundColor: Colors.orange,
+ duration: const Duration(seconds: 2),
+ ),
+ );
+ }
+ }
+ },
+ ),
+ ),
+ const SizedBox(width: 12),
+ IconButton.filled(
+ onPressed: () {
+ final value = newTagController.text;
+ if (value.isNotEmpty) {
+ final exists = tags.set.contains(value);
+ if (!exists) {
+ tags.add(value);
+ newTagController.clear();
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Added tag: $value'),
+ backgroundColor: Colors.green,
+ duration: const Duration(seconds: 2),
+ ),
+ );
+ } else {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Tag already exists: $value'),
+ backgroundColor: Colors.orange,
+ duration: const Duration(seconds: 2),
+ ),
+ );
+ }
+ }
+ },
+ icon: const Icon(Icons.add),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ // Tags display
+ const Text(
+ 'Current Tags:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+ Container(
+ constraints: const BoxConstraints(minHeight: 100),
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ border: Border.all(color: Colors.grey[300]!),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: tags.set.isEmpty
+ ? const Center(
+ child: Text(
+ 'No tags yet. Add some!',
+ style: TextStyle(color: Colors.grey),
+ ),
+ )
+ : Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: tags.set.map((tag) {
+ return Chip(
+ label: Text(tag),
+ deleteIcon: const Icon(Icons.close, size: 18),
+ onDeleted: () {
+ tags.remove(tag);
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Removed tag: $tag'),
+ duration: const Duration(seconds: 2),
+ ),
+ );
+ },
+ );
+ }).toList(),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Set operations
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ ElevatedButton.icon(
+ onPressed: tags.set.isEmpty ? null : tags.reset,
+ icon: const Icon(Icons.clear_all),
+ label: const Text('Clear All'),
+ ),
+ OutlinedButton.icon(
+ onPressed: () =>
+ tags.replace({'web', 'app', 'ui', 'ux'}),
+ icon: const Icon(Icons.refresh),
+ label: const Text('Replace Set'),
+ ),
+ OutlinedButton.icon(
+ onPressed: () => tags.toggle('featured'),
+ icon: const Icon(Icons.star),
+ label: const Text('Toggle "featured"'),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ // Set info
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Column(
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.info_outline, size: 16),
+ const SizedBox(width: 8),
+ Text('Unique tags: ${tags.set.length}'),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ const Icon(
+ Icons.check_circle,
+ size: 16,
+ color: Colors.green,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ tags.set.contains('flutter')
+ ? 'Has "flutter" tag'
+ : 'No "flutter" tag',
+ style: const TextStyle(fontSize: 12),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Set operations demo
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.functions, color: Colors.blue),
+ SizedBox(width: 8),
+ Text(
+ 'Set Operations',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• add() - Returns true if added, false if exists\n'
+ '• remove() - Removes a value from the set\n'
+ '• has() - Check if value exists\n'
+ '• toggle() - Add if absent, remove if present\n'
+ '• clear() - Remove all values\n'
+ '• replace() - Replace entire set',
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Manages unique values only\n'
+ '• No duplicates allowed\n'
+ '• Reactive updates on changes\n'
+ '• Perfect for tags, categories, selections\n'
+ '• Preserves insertion order',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useSet Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Initialize set
+final tags = useSet({'flutter', 'dart'});
+
+// Access set
+print(tags.value); // {flutter, dart}
+
+// Add values
+tags.add('mobile'); // Returns true
+tags.add('flutter'); // Returns false (exists)
+
+// Check existence
+if (tags.has('web')) {
+ print('Has web tag');
+}
+
+// Toggle value
+tags.toggle('featured'); // Add if absent
+tags.toggle('featured'); // Remove if present
+
+// Remove value
+tags.remove('dart');
+
+// Replace entire set
+tags.replace({'ios', 'android'});
+
+// Clear all
+tags.clear();
+
+// Use in UI
+Wrap(
+ children: tags.value.map((tag) {
+ return Chip(
+ label: Text(tag),
+ onDeleted: () => tags.remove(tag),
+ );
+ }).toList(),
+)''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_state_list_demo.dart b/demo/lib/hooks/use_state_list_demo.dart
new file mode 100644
index 0000000..917a46e
--- /dev/null
+++ b/demo/lib/hooks/use_state_list_demo.dart
@@ -0,0 +1,389 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseStateListDemo extends HookWidget {
+ const UseStateListDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ // UseStateList manages a list of states with circular navigation
+ final colorStates = useStateList([
+ {'color': Colors.red, 'name': 'Red'},
+ {'color': Colors.green, 'name': 'Green'},
+ {'color': Colors.blue, 'name': 'Blue'},
+ {'color': Colors.purple, 'name': 'Purple'},
+ {'color': Colors.orange, 'name': 'Orange'},
+ ]);
+
+ final history = useState>([]);
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useStateList Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🔄 useStateList Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Circular iteration through a list of states',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎨 Color Carousel',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Color display
+ Center(
+ child: Container(
+ width: 200,
+ height: 200,
+ decoration: BoxDecoration(
+ color: (colorStates.state['color'] as Color),
+ borderRadius: BorderRadius.circular(16),
+ boxShadow: [
+ BoxShadow(
+ color: (colorStates.state['color'] as Color)
+ .withValues(alpha: 0.3),
+ blurRadius: 20,
+ spreadRadius: 5,
+ ),
+ ],
+ ),
+ child: Center(
+ child: Text(
+ colorStates.state['name'] as String,
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ shadows: [
+ Shadow(blurRadius: 10, color: Colors.black45),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Navigation controls
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ IconButton.filled(
+ onPressed: () {
+ colorStates.prev();
+ history.value = [
+ '⬅️ Previous: ${colorStates.state['name']}',
+ ...history.value.take(9),
+ ];
+ },
+ icon: const Icon(Icons.arrow_back),
+ iconSize: 32,
+ ),
+ const SizedBox(width: 32),
+ Column(
+ children: [
+ Text(
+ '${colorStates.currentIndex + 1} / ${colorStates.list.length}',
+ style: const TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 4),
+ // Progress indicator
+ SizedBox(
+ width: 100,
+ child: LinearProgressIndicator(
+ value:
+ (colorStates.currentIndex + 1) /
+ colorStates.list.length,
+ backgroundColor: Colors.grey[300],
+ valueColor: AlwaysStoppedAnimation(
+ colorStates.state['color'] as Color,
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(width: 32),
+ IconButton.filled(
+ onPressed: () {
+ colorStates.next();
+ history.value = [
+ '➡️ Next: ${colorStates.state['name']}',
+ ...history.value.take(9),
+ ];
+ },
+ icon: const Icon(Icons.arrow_forward),
+ iconSize: 32,
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ // Quick jump buttons
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: List.generate(colorStates.list.length, (index) {
+ final item = colorStates.list[index];
+ final isActive = colorStates.currentIndex == index;
+ return ActionChip(
+ label: Text(item['name'] as String),
+ onPressed: () {
+ colorStates.setStateAt(index);
+ history.value = [
+ '🎯 Jumped to: ${item['name']}',
+ ...history.value.take(9),
+ ];
+ },
+ backgroundColor: isActive
+ ? (item['color'] as Color)
+ : null,
+ labelStyle: TextStyle(
+ color: isActive ? Colors.white : null,
+ fontWeight: isActive ? FontWeight.bold : null,
+ ),
+ );
+ }),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Activity log
+ const Text(
+ 'Activity Log:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ Container(
+ height: 100,
+ width: double.infinity,
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: history.value.isEmpty
+ ? const Center(
+ child: Text(
+ 'Navigate to see history',
+ style: TextStyle(color: Colors.grey),
+ ),
+ )
+ : ListView.builder(
+ itemCount: history.value.length,
+ itemBuilder: (context, index) {
+ return Text(
+ history.value[index],
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Feature showcase
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.stars, color: Colors.amber),
+ SizedBox(width: 8),
+ Text(
+ 'Key Features',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Circular navigation (wraps around)\n'
+ '• Direct access by index\n'
+ '• Forward/backward navigation\n'
+ '• Current position tracking\n'
+ '• Perfect for carousels, wizards, tours',
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Manages a list of predefined states\n'
+ '• Provides circular iteration methods\n'
+ '• Tracks current position in the list\n'
+ '• Allows direct jumping to any state\n'
+ '• No duplicate states - pure navigation',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useStateList Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Initialize with state list
+final carousel = useStateList([
+ 'Slide 1',
+ 'Slide 2',
+ 'Slide 3',
+ 'Slide 4',
+]);
+
+// Access current state
+print(carousel.state); // 'Slide 1'
+print(carousel.currentIndex); // 0
+
+// Navigate forward
+carousel.next(); // Goes to 'Slide 2'
+carousel.next(); // Goes to 'Slide 3'
+carousel.next(); // Goes to 'Slide 4'
+carousel.next(); // Wraps to 'Slide 1'
+
+// Navigate backward
+carousel.prev(); // Goes to 'Slide 4'
+
+// Jump to specific index
+carousel.setStateAt(2); // Goes to 'Slide 3'
+
+// Set by value
+carousel.setState('Slide 1'); // Finds and sets
+
+// Image carousel example
+final images = useStateList([
+ 'assets/img1.jpg',
+ 'assets/img2.jpg',
+ 'assets/img3.jpg',
+]);
+
+Image.asset(
+ images.state,
+ fit: BoxFit.cover,
+)
+
+// Wizard steps
+final wizard = useStateList([
+ WizardStep.personal,
+ WizardStep.contact,
+ WizardStep.review,
+ WizardStep.complete,
+]);
+
+// Check if can go next
+final canGoNext =
+ wizard.currentIndex < wizard.list.length - 1;''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_text_form_validator_demo.dart b/demo/lib/hooks/use_text_form_validator_demo.dart
new file mode 100644
index 0000000..c454c29
--- /dev/null
+++ b/demo/lib/hooks/use_text_form_validator_demo.dart
@@ -0,0 +1,441 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseTextFormValidatorDemo extends HookWidget {
+ const UseTextFormValidatorDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ // Email validation
+ final emailController = useTextEditingController();
+ final emailError = useTextFormValidator(
+ validator: (value) {
+ if (value.isEmpty) return 'Email is required';
+ final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
+ if (!emailRegex.hasMatch(value)) return 'Invalid email format';
+ return null;
+ },
+ controller: emailController,
+ initialValue: null,
+ );
+
+ // Password validation with multiple rules
+ final passwordController = useTextEditingController();
+ final passwordErrors = useTextFormValidator>(
+ validator: (value) {
+ final errors = [];
+ if (value.isEmpty) {
+ errors.add('Password is required');
+ } else {
+ if (value.length < 8) errors.add('At least 8 characters');
+ if (!value.contains(RegExp(r'[A-Z]'))) {
+ errors.add('One uppercase letter');
+ }
+ if (!value.contains(RegExp(r'[a-z]'))) {
+ errors.add('One lowercase letter');
+ }
+ if (!value.contains(RegExp(r'[0-9]'))) errors.add('One number');
+ if (!value.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'))) {
+ errors.add('One special character');
+ }
+ }
+ return errors;
+ },
+ controller: passwordController,
+ initialValue: [],
+ );
+
+ // Username validation
+ final usernameController = useTextEditingController();
+ final usernameValid = useTextFormValidator(
+ validator: (value) {
+ if (value.isEmpty) return false;
+ if (value.length < 3) return false;
+ return RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value);
+ },
+ controller: usernameController,
+ initialValue: false,
+ );
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useTextFormValidator Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '✅ useTextFormValidator Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Reactive form validation with real-time feedback',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '📝 Registration Form',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Username field
+ TextField(
+ controller: usernameController,
+ decoration: InputDecoration(
+ labelText: 'Username',
+ hintText: 'Enter username (3+ chars, alphanumeric)',
+ border: const OutlineInputBorder(),
+ prefixIcon: const Icon(Icons.person),
+ suffixIcon: usernameController.text.isNotEmpty
+ ? Icon(
+ usernameValid
+ ? Icons.check_circle
+ : Icons.error,
+ color: usernameValid
+ ? Colors.green
+ : Colors.red,
+ )
+ : null,
+ ),
+ ),
+ if (usernameController.text.isNotEmpty && !usernameValid)
+ Padding(
+ padding: const EdgeInsets.only(top: 8, left: 12),
+ child: Text(
+ 'Username must be 3+ characters, alphanumeric only',
+ style: TextStyle(
+ color: Colors.red[700],
+ fontSize: 12,
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 20),
+
+ // Email field
+ TextField(
+ controller: emailController,
+ decoration: InputDecoration(
+ labelText: 'Email',
+ hintText: 'Enter your email address',
+ border: const OutlineInputBorder(),
+ prefixIcon: const Icon(Icons.email),
+ errorText: emailError,
+ ),
+ keyboardType: TextInputType.emailAddress,
+ ),
+
+ const SizedBox(height: 20),
+
+ // Password field
+ TextField(
+ controller: passwordController,
+ decoration: InputDecoration(
+ labelText: 'Password',
+ hintText: 'Enter a strong password',
+ border: const OutlineInputBorder(),
+ prefixIcon: const Icon(Icons.lock),
+ suffixIcon: passwordController.text.isNotEmpty
+ ? Icon(
+ passwordErrors.isEmpty
+ ? Icons.check_circle
+ : Icons.error,
+ color: passwordErrors.isEmpty
+ ? Colors.green
+ : Colors.red,
+ )
+ : null,
+ ),
+ obscureText: true,
+ ),
+
+ // Password requirements
+ if (passwordController.text.isNotEmpty) ...[
+ const SizedBox(height: 12),
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: passwordErrors.isEmpty
+ ? Colors.green.withValues(alpha: 0.1)
+ : Colors.orange.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(
+ color: passwordErrors.isEmpty
+ ? Colors.green
+ : Colors.orange,
+ ),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ 'Password Requirements:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ _buildRequirement(
+ 'At least 8 characters',
+ !passwordErrors.contains('At least 8 characters'),
+ ),
+ _buildRequirement(
+ 'One uppercase letter',
+ !passwordErrors.contains('One uppercase letter'),
+ ),
+ _buildRequirement(
+ 'One lowercase letter',
+ !passwordErrors.contains('One lowercase letter'),
+ ),
+ _buildRequirement(
+ 'One number',
+ !passwordErrors.contains('One number'),
+ ),
+ _buildRequirement(
+ 'One special character',
+ !passwordErrors.contains('One special character'),
+ ),
+ ],
+ ),
+ ),
+ ],
+
+ const SizedBox(height: 24),
+
+ // Submit button
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed:
+ (usernameValid &&
+ emailError == null &&
+ emailController.text.isNotEmpty &&
+ passwordErrors.isEmpty &&
+ passwordController.text.isNotEmpty)
+ ? () {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Form is valid! ✅'),
+ backgroundColor: Colors.green,
+ ),
+ );
+ }
+ : null,
+ child: const Text('Register'),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Features
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.stars, color: Colors.amber),
+ SizedBox(width: 8),
+ Text(
+ 'Key Features',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Real-time validation feedback\n'
+ '• Flexible return types (String?, bool, List, etc.)\n'
+ '• Reactive updates on text change\n'
+ '• Multiple validation rules support\n'
+ '• Works with TextEditingController',
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Listens to TextEditingController changes\n'
+ '• Runs validator function on each change\n'
+ '• Returns validation result reactively\n'
+ '• Supports any return type for flexibility\n'
+ '• Automatically cleans up listeners',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildRequirement(String text, bool met) {
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 4),
+ child: Row(
+ children: [
+ Icon(
+ met ? Icons.check : Icons.close,
+ size: 16,
+ color: met ? Colors.green : Colors.red,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ text,
+ style: TextStyle(
+ color: met ? Colors.green : Colors.red,
+ decoration: met ? TextDecoration.none : null,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useTextFormValidator Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// String validation (error message)
+final controller = useTextEditingController();
+final error = useTextFormValidator(
+ validator: (value) {
+ if (value.isEmpty) return 'Required';
+ if (value.length < 3) return 'Too short';
+ return null; // Valid
+ },
+ controller: controller,
+ initialValue: null,
+);
+
+TextField(
+ controller: controller,
+ decoration: InputDecoration(
+ errorText: error,
+ ),
+)
+
+// Boolean validation
+final isValid = useTextFormValidator(
+ validator: (value) => value.length >= 8,
+ controller: passwordController,
+ initialValue: false,
+);
+
+// Multiple errors
+final errors = useTextFormValidator>(
+ validator: (value) {
+ final errors = [];
+ if (!hasUppercase(value)) {
+ errors.add('Need uppercase');
+ }
+ if (!hasNumber(value)) {
+ errors.add('Need number');
+ }
+ return errors;
+ },
+ controller: controller,
+ initialValue: [],
+);
+
+// Email validation
+final emailValid = useTextFormValidator(
+ validator: (value) {
+ final regex = RegExp(r'^[w-.]+@([w-]+.)+[w-]{2,4}\$');
+ return regex.hasMatch(value);
+ },
+ controller: emailController,
+ initialValue: false,
+);''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_throttle_demo.dart b/demo/lib/hooks/use_throttle_demo.dart
new file mode 100644
index 0000000..871a78b
--- /dev/null
+++ b/demo/lib/hooks/use_throttle_demo.dart
@@ -0,0 +1,330 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseThrottleDemo extends HookWidget {
+ const UseThrottleDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final textController = useTextEditingController();
+ final inputText = useState('');
+ final throttleDuration = useState(500);
+
+ // Listen to text changes
+ useEffect(() {
+ void listener() {
+ inputText.value = textController.text;
+ }
+
+ textController.addListener(listener);
+ return () => textController.removeListener(listener);
+ }, [textController]);
+
+ // Apply throttling
+ final throttledText = useThrottle(
+ inputText.value,
+ Duration(milliseconds: throttleDuration.value),
+ );
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useThrottle Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Header
+ const Text(
+ '🔄 useThrottle Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Throttle value updates to improve performance',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ // Live Example
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '📝 Live Example',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Input Field
+ TextField(
+ controller: textController,
+ decoration: const InputDecoration(
+ labelText: 'Type here...',
+ hintText: 'Start typing to see throttling in action',
+ border: OutlineInputBorder(),
+ ),
+ ),
+ const SizedBox(height: 24),
+
+ // Results Display
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[50],
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: Colors.grey[300]!),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(
+ Icons.keyboard,
+ color: Colors.green,
+ size: 20,
+ ),
+ const SizedBox(width: 8),
+ const Text(
+ 'Original: ',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ Expanded(
+ child: Text(
+ inputText.value.isEmpty
+ ? '(empty)'
+ : '"${inputText.value}"',
+ style: TextStyle(
+ color: inputText.value.isEmpty
+ ? Colors.grey
+ : Colors.black,
+ fontStyle: inputText.value.isEmpty
+ ? FontStyle.italic
+ : FontStyle.normal,
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ const Icon(
+ Icons.speed,
+ color: Colors.blue,
+ size: 20,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Throttled (${throttleDuration.value}ms): ',
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ Expanded(
+ child: Text(
+ throttledText.isEmpty
+ ? '(empty)'
+ : '"$throttledText"',
+ style: TextStyle(
+ color: throttledText.isEmpty
+ ? Colors.grey
+ : Colors.blue,
+ fontWeight: FontWeight.w500,
+ fontStyle: throttledText.isEmpty
+ ? FontStyle.italic
+ : FontStyle.normal,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 24),
+
+ // Duration Control
+ const Text(
+ '⏱️ Throttle Duration',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ Expanded(
+ child: Slider(
+ value: throttleDuration.value.toDouble(),
+ min: 100,
+ max: 2000,
+ divisions: 19,
+ label: '${throttleDuration.value}ms',
+ onChanged: (value) {
+ throttleDuration.value = value.round();
+ },
+ ),
+ ),
+ const SizedBox(width: 16),
+ SizedBox(
+ width: 80,
+ child: Text(
+ '${throttleDuration.value}ms',
+ style: const TextStyle(fontWeight: FontWeight.bold),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+
+ // Quick presets
+ Wrap(
+ spacing: 8,
+ children: [
+ _buildPresetChip('Fast (200ms)', 200, throttleDuration),
+ _buildPresetChip(
+ 'Normal (500ms)',
+ 500,
+ throttleDuration,
+ ),
+ _buildPresetChip(
+ 'Slow (1000ms)',
+ 1000,
+ throttleDuration,
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ // Usage Info
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• The first value update is immediate\n'
+ '• Subsequent updates are throttled based on duration\n'
+ '• Perfect for search inputs, API calls, and expensive operations\n'
+ '• Reduces unnecessary computations and improves performance',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildPresetChip(
+ String label,
+ int value,
+ ValueNotifier notifier,
+ ) {
+ return ActionChip(
+ label: Text(label),
+ onPressed: () => notifier.value = value,
+ backgroundColor: notifier.value == value ? Colors.blue[100] : null,
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useThrottle Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''final text = useState('');
+final throttledValue = useThrottle(
+ text.value,
+ Duration(milliseconds: 500),
+);
+
+// Use in TextField
+TextField(
+ onChanged: (value) => text.value = value,
+ decoration: InputDecoration(
+ labelText: 'Search...',
+ ),
+)
+
+// throttledValue updates at most once per 500ms
+useEffect(() {
+ // This expensive operation only runs when throttled value changes
+ performExpensiveSearch(throttledValue);
+ return null;
+}, [throttledValue]);''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_throttle_fn_demo.dart b/demo/lib/hooks/use_throttle_fn_demo.dart
new file mode 100644
index 0000000..09c93bd
--- /dev/null
+++ b/demo/lib/hooks/use_throttle_fn_demo.dart
@@ -0,0 +1,308 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseThrottleFnDemo extends HookWidget {
+ const UseThrottleFnDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final counter = useState(0);
+ final clickCounter = useState(0);
+ final throttleDuration = useState(500);
+
+ // Create throttled function
+ final throttledIncrement = useThrottleFn(() {
+ counter.value++;
+ }, Duration(milliseconds: throttleDuration.value));
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useThrottleFn Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Header
+ const Text(
+ '⚡ useThrottleFn Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Throttle function calls to prevent excessive execution',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ // Live Example
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎯 Live Example',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Counters Display
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ color: Colors.grey[50],
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: Colors.grey[300]!),
+ ),
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ _buildCounterDisplay(
+ 'Button Clicks',
+ clickCounter.value,
+ Colors.orange,
+ Icons.touch_app,
+ ),
+ Container(
+ width: 1,
+ height: 60,
+ color: Colors.grey[300],
+ ),
+ _buildCounterDisplay(
+ 'Throttled Calls',
+ counter.value,
+ Colors.blue,
+ Icons.speed,
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Action Button
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton.icon(
+ onPressed: () {
+ clickCounter.value++;
+ throttledIncrement.call();
+ },
+ icon: const Icon(Icons.add),
+ label: const Text('Click Me Fast!'),
+ style: ElevatedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ textStyle: const TextStyle(fontSize: 16),
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 16),
+
+ Text(
+ 'Try clicking rapidly! The button clicks are counted immediately, '
+ 'but the throttled function only executes once per ${throttleDuration.value}ms.',
+ style: TextStyle(
+ fontSize: 14,
+ color: Colors.grey[600],
+ fontStyle: FontStyle.italic,
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Duration Control
+ const Text(
+ '⏱️ Throttle Duration',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ Expanded(
+ child: Slider(
+ value: throttleDuration.value.toDouble(),
+ min: 100,
+ max: 2000,
+ divisions: 19,
+ label: '${throttleDuration.value}ms',
+ onChanged: (value) {
+ throttleDuration.value = value.round();
+ },
+ ),
+ ),
+ const SizedBox(width: 16),
+ SizedBox(
+ width: 80,
+ child: Text(
+ '${throttleDuration.value}ms',
+ style: const TextStyle(fontWeight: FontWeight.bold),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 16),
+
+ // Reset Button
+ OutlinedButton.icon(
+ onPressed: () {
+ counter.value = 0;
+ clickCounter.value = 0;
+ },
+ icon: const Icon(Icons.refresh),
+ label: const Text('Reset Counters'),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ // Usage Info
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Creates a throttled version of any function\n'
+ '• First call executes immediately\n'
+ '• Subsequent calls are throttled based on duration\n'
+ '• Perfect for button clicks, API calls, and expensive operations\n'
+ '• Prevents function spam and improves performance',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildCounterDisplay(
+ String label,
+ int value,
+ Color color,
+ IconData icon,
+ ) {
+ return Column(
+ children: [
+ Icon(icon, color: color, size: 28),
+ const SizedBox(height: 8),
+ Text(
+ value.toString(),
+ style: TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ color: color,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ label,
+ style: const TextStyle(fontSize: 12, color: Colors.grey),
+ textAlign: TextAlign.center,
+ ),
+ ],
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useThrottleFn Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''final counter = useState(0);
+
+final throttledIncrement = useThrottleFn(
+ () {
+ counter.value++;
+ print('Counter incremented: \${counter.value}');
+ },
+ Duration(milliseconds: 500),
+);
+
+// Usage in button
+ElevatedButton(
+ onPressed: throttledIncrement.call,
+ child: Text('Increment (Throttled)'),
+)
+
+// Even if clicked rapidly, the function
+// only executes once per 500ms
+''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_timeout_demo.dart b/demo/lib/hooks/use_timeout_demo.dart
new file mode 100644
index 0000000..d9d1984
--- /dev/null
+++ b/demo/lib/hooks/use_timeout_demo.dart
@@ -0,0 +1,395 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseTimeoutDemo extends HookWidget {
+ const UseTimeoutDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final message = useState('');
+ final delay = useState(3000);
+ final isRunning = useState(false);
+
+ // useTimeout just causes a rebuild after the delay
+ final timeoutState = useTimeout(
+ isRunning.value
+ ? Duration(milliseconds: delay.value)
+ : const Duration(days: 365),
+ );
+
+ // Check if timeout has fired
+ useEffect(() {
+ if (isRunning.value && timeoutState.isReady() == true) {
+ message.value =
+ '⏰ Timeout fired at ${DateTime.now().toString().substring(11, 19)}';
+ isRunning.value = false;
+ }
+ return null;
+ }, [timeoutState.isReady()]);
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useTimeout Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '⏲️ useTimeout Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Execute code after a specified delay',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎯 Delayed Action',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Timer visualization
+ Center(
+ child: Container(
+ width: 200,
+ height: 200,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ border: Border.all(
+ color: isRunning.value ? Colors.blue : Colors.grey,
+ width: 4,
+ ),
+ ),
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ isRunning.value ? Icons.timer : Icons.timer_off,
+ size: 48,
+ color: isRunning.value
+ ? Colors.blue
+ : Colors.grey,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ isRunning.value ? 'Running...' : 'Idle',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ color: isRunning.value
+ ? Colors.blue
+ : Colors.grey,
+ ),
+ ),
+ if (isRunning.value) ...[
+ const SizedBox(height: 4),
+ Text(
+ '${delay.value}ms',
+ style: const TextStyle(color: Colors.grey),
+ ),
+ ],
+ ],
+ ),
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Control button
+ Center(
+ child: ElevatedButton.icon(
+ onPressed: isRunning.value
+ ? null
+ : () {
+ message.value = '';
+ isRunning.value = true;
+ },
+ icon: const Icon(Icons.play_arrow),
+ label: const Text('Start Timeout'),
+ style: ElevatedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 24,
+ vertical: 12,
+ ),
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Delay control
+ const Text(
+ 'Timeout Duration:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ Expanded(
+ child: Slider(
+ value: delay.value.toDouble(),
+ min: 500,
+ max: 10000,
+ divisions: 19,
+ label: '${delay.value}ms',
+ onChanged: isRunning.value
+ ? null
+ : (value) => delay.value = value.round(),
+ ),
+ ),
+ SizedBox(
+ width: 80,
+ child: Text(
+ '${delay.value}ms',
+ style: const TextStyle(fontWeight: FontWeight.bold),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ ],
+ ),
+
+ // Quick presets
+ Wrap(
+ spacing: 8,
+ children: [
+ ActionChip(
+ label: const Text('1s'),
+ onPressed: isRunning.value
+ ? null
+ : () => delay.value = 1000,
+ backgroundColor: delay.value == 1000
+ ? Colors.blue
+ : null,
+ ),
+ ActionChip(
+ label: const Text('3s'),
+ onPressed: isRunning.value
+ ? null
+ : () => delay.value = 3000,
+ backgroundColor: delay.value == 3000
+ ? Colors.blue
+ : null,
+ ),
+ ActionChip(
+ label: const Text('5s'),
+ onPressed: isRunning.value
+ ? null
+ : () => delay.value = 5000,
+ backgroundColor: delay.value == 5000
+ ? Colors.blue
+ : null,
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ // Message display
+ if (message.value.isNotEmpty)
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.green.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: Colors.green),
+ ),
+ child: Row(
+ children: [
+ const Icon(Icons.check_circle, color: Colors.green),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ message.value,
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ color: Colors.green,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Use cases
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.tips_and_updates, color: Colors.amber),
+ SizedBox(width: 8),
+ Text(
+ 'Common Use Cases',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Show loading indicators for minimum time\n'
+ '• Delay navigation or redirects\n'
+ '• Auto-dismiss notifications\n'
+ '• Implement splash screens\n'
+ '• Add delays to animations',
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Executes callback after specified delay\n'
+ '• Pass null duration to cancel timeout\n'
+ '• Automatically cleans up on unmount\n'
+ '• One-shot execution (not repeating)\n'
+ '• Useful for delayed actions and transitions',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useTimeout Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Basic timeout - causes rebuild after delay
+final timeoutState = useTimeout(Duration(seconds: 3));
+
+// Check if timeout is ready
+useEffect(() {
+ if (timeoutState.isReady()) {
+ print('3 seconds have passed!');
+ // Perform action after timeout
+ }
+ return null;
+}, [timeoutState.isReady()]);
+
+// Conditional timeout
+final showLoading = useState(true);
+final loadingTimeout = useTimeout(
+ showLoading.value
+ ? Duration(seconds: 2)
+ : Duration(days: 365), // Effectively disabled
+);
+
+useEffect(() {
+ if (loadingTimeout.isReady() && showLoading.value) {
+ showLoading.value = false;
+ }
+ return null;
+}, [loadingTimeout.isReady()]);
+
+// Control timeout state
+loadingTimeout.reset(); // Reset timer
+loadingTimeout.cancel(); // Cancel timer
+
+// Minimum display time pattern
+final dataLoaded = useState(false);
+final minDisplayTime = useTimeout(Duration(seconds: 1));
+
+// Only hide when both data loaded AND min time passed
+final shouldHideLoading = dataLoaded.value &&
+ minDisplayTime.isReady();''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_timeout_fn_demo.dart b/demo/lib/hooks/use_timeout_fn_demo.dart
new file mode 100644
index 0000000..df322f0
--- /dev/null
+++ b/demo/lib/hooks/use_timeout_fn_demo.dart
@@ -0,0 +1,427 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseTimeoutFnDemo extends HookWidget {
+ const UseTimeoutFnDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final logs = useState>([]);
+ final delay = useState(2000);
+
+ // Create timeout function that can be started/stopped
+ final timeout = useTimeoutFn(() {
+ logs.value = [
+ '✅ Timeout executed at ${DateTime.now().toString().substring(11, 19)}',
+ ...logs.value.take(9),
+ ];
+ }, Duration(milliseconds: delay.value));
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useTimeoutFn Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '⏱️ useTimeoutFn Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Controllable timeout with start, stop, and reset',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎮 Timeout Controller',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Status display
+ Container(
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Row(
+ children: [
+ Icon(
+ timeout.isReady() == false
+ ? Icons.timer
+ : Icons.timer_off,
+ size: 32,
+ color: timeout.isReady() == false
+ ? Colors.orange
+ : Colors.grey,
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Status: ${timeout.isReady() == null
+ ? "Cancelled"
+ : timeout.isReady() == false
+ ? "Running"
+ : "Ready"}',
+ style: const TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ if (timeout.isReady() == false)
+ Text(
+ 'Will fire in ${delay.value}ms',
+ style: const TextStyle(color: Colors.grey),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Control buttons
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ ElevatedButton.icon(
+ onPressed: timeout.isReady() == false
+ ? null
+ : timeout.reset,
+ icon: const Icon(Icons.play_arrow),
+ label: const Text('Start'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.green,
+ ),
+ ),
+ ElevatedButton.icon(
+ onPressed: timeout.isReady() == false
+ ? timeout.cancel
+ : null,
+ icon: const Icon(Icons.stop),
+ label: const Text('Cancel'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.red,
+ ),
+ ),
+ ElevatedButton.icon(
+ onPressed: timeout.reset,
+ icon: const Icon(Icons.refresh),
+ label: const Text('Reset'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.blue,
+ ),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ // Delay control
+ const Text(
+ 'Timeout Duration:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ Expanded(
+ child: Slider(
+ value: delay.value.toDouble(),
+ min: 500,
+ max: 5000,
+ divisions: 9,
+ label: '${delay.value}ms',
+ onChanged: (value) => delay.value = value.round(),
+ ),
+ ),
+ SizedBox(
+ width: 80,
+ child: Text(
+ '${delay.value}ms',
+ style: const TextStyle(fontWeight: FontWeight.bold),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ // Activity log
+ const Text(
+ 'Activity Log:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+ Container(
+ height: 150,
+ width: double.infinity,
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: logs.value.isEmpty
+ ? const Center(
+ child: Text(
+ 'No timeouts executed yet',
+ style: TextStyle(color: Colors.grey),
+ ),
+ )
+ : ListView.builder(
+ itemCount: logs.value.length,
+ itemBuilder: (context, index) {
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 4),
+ child: Text(
+ logs.value[index],
+ style: const TextStyle(
+ color: Colors.white,
+ fontFamily: 'monospace',
+ fontSize: 12,
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+
+ const SizedBox(height: 16),
+
+ OutlinedButton.icon(
+ onPressed: () => logs.value = [],
+ icon: const Icon(Icons.clear),
+ label: const Text('Clear Log'),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Comparison with useTimeout
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.compare_arrows, color: Colors.blue),
+ SizedBox(width: 8),
+ Text(
+ 'useTimeout vs useTimeoutFn',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Table(
+ border: TableBorder.all(color: Colors.grey[300]!),
+ children: const [
+ TableRow(
+ decoration: BoxDecoration(color: Colors.black12),
+ children: [
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text(
+ 'useTimeout',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ ),
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text(
+ 'useTimeoutFn',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ ),
+ ],
+ ),
+ TableRow(
+ children: [
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text('Auto-starts'),
+ ),
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text('Manual control'),
+ ),
+ ],
+ ),
+ TableRow(
+ children: [
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text('No control methods'),
+ ),
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text('start(), stop(), reset()'),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Returns TimeoutState object with methods\n'
+ '• isReady() returns null (cancelled), false (running), or true (completed)\n'
+ '• cancel() stops the timeout\n'
+ '• reset() restarts the timeout\n'
+ '• Automatically starts on hook initialization',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useTimeoutFn Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Create controllable timeout
+final timeout = useTimeoutFn(
+ () => print('Timeout fired!'),
+ Duration(seconds: 3),
+);
+
+// Check status
+if (timeout.isActive) {
+ print('Timeout is running');
+}
+
+// Control methods
+ElevatedButton(
+ onPressed: timeout.start,
+ child: Text('Start Timer'),
+)
+
+ElevatedButton(
+ onPressed: timeout.stop,
+ child: Text('Stop Timer'),
+)
+
+// Auto-retry with timeout
+final retry = useTimeoutFn(() {
+ fetchData().catchError((e) {
+ // Retry after delay
+ timeout.reset();
+ });
+}, Duration(seconds: 5));
+
+// Delayed form submission
+final submitTimeout = useTimeoutFn(
+ () => submitForm(),
+ Duration(seconds: 2),
+);
+
+TextField(
+ onChanged: (_) => submitTimeout.reset(),
+)''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_toggle_demo.dart b/demo/lib/hooks/use_toggle_demo.dart
new file mode 100644
index 0000000..66b46e1
--- /dev/null
+++ b/demo/lib/hooks/use_toggle_demo.dart
@@ -0,0 +1,406 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseToggleDemo extends HookWidget {
+ const UseToggleDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final toggle = useToggle(false);
+
+ // For multiple checkbox example
+ final option1 = useToggle(false);
+ final option2 = useToggle(true);
+ final option3 = useToggle(false);
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useToggle Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Header
+ const Text(
+ '🔀 useToggle Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Toggle boolean states with optional custom setter',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ // Basic Toggle
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎯 Basic Toggle',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Visual Toggle
+ Center(
+ child: GestureDetector(
+ onTap: () => toggle.toggle(),
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 300),
+ width: 200,
+ height: 100,
+ decoration: BoxDecoration(
+ color: toggle.value
+ ? Theme.of(context).colorScheme.primary
+ : Colors.grey[400],
+ borderRadius: BorderRadius.circular(50),
+ boxShadow: [
+ BoxShadow(
+ color: toggle.value
+ ? Theme.of(context).colorScheme.primary
+ .withValues(alpha: 0.4)
+ : Colors.grey.withValues(alpha: 0.4),
+ blurRadius: 20,
+ offset: const Offset(0, 5),
+ ),
+ ],
+ ),
+ child: Stack(
+ children: [
+ AnimatedPositioned(
+ duration: const Duration(milliseconds: 300),
+ curve: Curves.easeInOut,
+ left: toggle.value ? 100 : 0,
+ child: Container(
+ width: 100,
+ height: 100,
+ padding: const EdgeInsets.all(10),
+ child: Container(
+ decoration: const BoxDecoration(
+ color: Colors.white,
+ shape: BoxShape.circle,
+ ),
+ ),
+ ),
+ ),
+ Center(
+ child: Text(
+ toggle.value ? 'ON' : 'OFF',
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Control Buttons
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ ElevatedButton(
+ onPressed: () => toggle.toggle(),
+ child: const Text('Toggle'),
+ ),
+ OutlinedButton(
+ onPressed: () => toggle.toggle(true),
+ child: const Text('Set ON'),
+ ),
+ OutlinedButton(
+ onPressed: () => toggle.toggle(false),
+ child: const Text('Set OFF'),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 16),
+
+ // Current State
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Text('Current state: '),
+ Text(
+ toggle.value.toString(),
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ fontFamily: 'monospace',
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Multiple Toggles Example
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '☑️ Multiple Toggles (Settings)',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Settings List
+ _buildSettingTile(
+ 'Enable Notifications',
+ 'Receive push notifications',
+ Icons.notifications,
+ option1.value,
+ option1,
+ ),
+ const Divider(),
+ _buildSettingTile(
+ 'Dark Mode',
+ 'Use dark theme',
+ Icons.dark_mode,
+ option2.value,
+ option2,
+ ),
+ const Divider(),
+ _buildSettingTile(
+ 'Auto-save',
+ 'Automatically save changes',
+ Icons.save,
+ option3.value,
+ option3,
+ ),
+
+ const SizedBox(height: 20),
+
+ // Summary
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.blue.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Column(
+ children: [
+ const Text(
+ 'Settings Summary:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ 'Notifications: ${option1.value ? "ON" : "OFF"}\n'
+ 'Dark Mode: ${option2.value ? "ON" : "OFF"}\n'
+ 'Auto-save: ${option3.value ? "ON" : "OFF"}',
+ style: const TextStyle(fontFamily: 'monospace'),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Toggle vs Boolean Comparison
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.compare_arrows, color: Colors.blue),
+ SizedBox(width: 8),
+ Text(
+ 'useToggle vs useBoolean',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• useToggle returns a function that toggles or sets the value\n'
+ '• useBoolean returns an object with multiple methods\n'
+ '• useToggle is simpler for basic toggle functionality\n'
+ '• useBoolean provides more explicit methods (setTrue, setFalse)\n'
+ '• Both work great with switches and checkboxes',
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ // Usage Info
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Returns a ToggleState object with value getter and setter function\n'
+ '• Call setter() to toggle the value\n'
+ '• Call setter(true/false) to set specific value\n'
+ '• More concise than useBoolean for simple toggles\n'
+ '• Perfect for switches, checkboxes, and visibility states',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildSettingTile(
+ String title,
+ String subtitle,
+ IconData icon,
+ bool value,
+ ToggleState toggle,
+ ) {
+ return ListTile(
+ leading: Icon(icon, color: value ? Colors.blue : Colors.grey),
+ title: Text(title),
+ subtitle: Text(subtitle),
+ trailing: Switch(value: value, onChanged: (_) => toggle.toggle()),
+ onTap: () => toggle.toggle(),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useToggle Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Initialize toggle
+final toggle = useToggle(false);
+
+// Access the value
+Text('Status: \${toggle.value}');
+
+// Toggle the value
+ElevatedButton(
+ onPressed: () => toggle.toggle(),
+ child: Text('Toggle'),
+)
+
+// Set specific value
+toggle.toggle(true); // Set to true
+toggle.toggle(false); // Set to false
+
+// Use with Switch
+Switch(
+ value: toggle.value,
+ onChanged: (_) => toggle.toggle(),
+)
+
+// Multiple toggles
+final darkMode = useToggle(false);
+final notifications = useToggle(true);
+
+// Conditional rendering
+if (darkMode.value) {
+ // Apply dark theme
+}''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_unmount_demo.dart b/demo/lib/hooks/use_unmount_demo.dart
new file mode 100644
index 0000000..0b6712f
--- /dev/null
+++ b/demo/lib/hooks/use_unmount_demo.dart
@@ -0,0 +1,252 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseUnmountDemo extends HookWidget {
+ const UseUnmountDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final showComponent = useState(true);
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useUnmount Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🔚 useUnmount Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Run cleanup when component unmounts',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎮 Component Control',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ Row(
+ children: [
+ Switch(
+ value: showComponent.value,
+ onChanged: (value) => showComponent.value = value,
+ ),
+ const SizedBox(width: 12),
+ Text(
+ showComponent.value
+ ? 'Component Mounted'
+ : 'Component Unmounted',
+ style: const TextStyle(fontSize: 16),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ AnimatedSwitcher(
+ duration: const Duration(milliseconds: 300),
+ child: showComponent.value
+ ? const _DemoComponent()
+ : Container(
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ color: Colors.red.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: Colors.red),
+ ),
+ child: const Row(
+ children: [
+ Icon(Icons.info, color: Colors.red),
+ SizedBox(width: 12),
+ Text(
+ 'Component unmounted - cleanup executed!',
+ style: TextStyle(color: Colors.red),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Runs when component is removed from widget tree\n'
+ '• Perfect for cleanup operations\n'
+ '• Cancel subscriptions, timers, animations\n'
+ '• Release resources and prevent memory leaks',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useUnmount Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''useUnmount(() {
+ // This runs when component unmounts
+ print('Component unmounting!');
+
+ // Cancel subscriptions
+ subscription?.cancel();
+
+ // Stop timers
+ timer?.cancel();
+
+ // Dispose controllers
+ animationController.dispose();
+
+ // Close streams
+ streamController.close();
+});
+
+// Common patterns:
+final timer = useRef(null);
+
+useMount(() {
+ timer.value = Timer.periodic(
+ Duration(seconds: 1),
+ (_) => updateTime(),
+ );
+});
+
+useUnmount(() {
+ timer.value?.cancel();
+});''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _DemoComponent extends HookWidget {
+ const _DemoComponent();
+
+ @override
+ Widget build(BuildContext context) {
+ useUnmount(() {
+ // This will be called when component unmounts
+ debugPrint('🔴 Cleanup executed at ${DateTime.now()}');
+ });
+
+ return Container(
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ color: Colors.green.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: Colors.green),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.check_circle, color: Colors.green),
+ SizedBox(width: 12),
+ Text(
+ 'Active Component',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ color: Colors.green,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Text(
+ 'This component has cleanup logic.\nToggle the switch to unmount and see cleanup.',
+ style: TextStyle(color: Colors.grey[700]),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_update_demo.dart b/demo/lib/hooks/use_update_demo.dart
new file mode 100644
index 0000000..8ff013f
--- /dev/null
+++ b/demo/lib/hooks/use_update_demo.dart
@@ -0,0 +1,447 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseUpdateDemo extends HookWidget {
+ const UseUpdateDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final update = useUpdate();
+ final renderCount = useState(0);
+ final logs = useState>([]);
+ final testValue = useState('Initial');
+
+ // Track renders
+ useEffect(() {
+ renderCount.value++;
+ logs.value = [
+ '🔄 Render #${renderCount.value} at ${DateTime.now().toString().substring(11, 19)}',
+ ...logs.value.take(9),
+ ];
+ return null;
+ });
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useUpdate Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🔄 useUpdate Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Force component re-render on demand',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎯 Manual Re-render Control',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Render count display
+ Center(
+ child: Container(
+ width: 150,
+ height: 150,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ gradient: LinearGradient(
+ colors: [
+ Theme.of(context).colorScheme.primary,
+ Theme.of(context).colorScheme.secondary,
+ ],
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: Theme.of(
+ context,
+ ).colorScheme.primary.withValues(alpha: 0.3),
+ blurRadius: 20,
+ offset: const Offset(0, 5),
+ ),
+ ],
+ ),
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(
+ Icons.refresh,
+ size: 32,
+ color: Colors.white,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ '${renderCount.value}',
+ style: const TextStyle(
+ fontSize: 36,
+ fontWeight: FontWeight.bold,
+ color: Colors.white,
+ ),
+ ),
+ const Text(
+ 'Renders',
+ style: TextStyle(
+ color: Colors.white70,
+ fontSize: 14,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Force update button
+ Center(
+ child: ElevatedButton.icon(
+ onPressed: update,
+ icon: const Icon(Icons.refresh),
+ label: const Text('Force Re-render'),
+ style: ElevatedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 24,
+ vertical: 12,
+ ),
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Test scenarios
+ const Text(
+ 'Test Scenarios:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+
+ // Scenario 1: Update without state change
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '1. Force update without state change',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ Text('Current value: ${testValue.value}'),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ OutlinedButton(
+ onPressed: () {
+ // Don't change state, just force update
+ update();
+ },
+ child: const Text('Update (no state change)'),
+ ),
+ const SizedBox(width: 8),
+ OutlinedButton(
+ onPressed: () {
+ testValue.value =
+ 'Changed at ${DateTime.now().toString().substring(14, 19)}';
+ },
+ child: const Text('Change State'),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 16),
+
+ // Scenario 2: Multiple updates
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '2. Multiple rapid updates',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ OutlinedButton.icon(
+ onPressed: () {
+ // Trigger multiple updates
+ for (int i = 0; i < 3; i++) {
+ update();
+ }
+ },
+ icon: const Icon(Icons.repeat),
+ label: const Text('Update 3x'),
+ ),
+ const SizedBox(width: 8),
+ OutlinedButton.icon(
+ onPressed: () async {
+ // Delayed updates
+ update();
+ await Future.delayed(
+ const Duration(milliseconds: 100),
+ );
+ update();
+ await Future.delayed(
+ const Duration(milliseconds: 100),
+ );
+ update();
+ },
+ icon: const Icon(Icons.timer),
+ label: const Text('Delayed 3x'),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Render log
+ const Text(
+ 'Render Log:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+ Container(
+ height: 120,
+ width: double.infinity,
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: logs.value.isEmpty
+ ? const Center(
+ child: Text(
+ 'No renders yet',
+ style: TextStyle(color: Colors.grey),
+ ),
+ )
+ : ListView.builder(
+ itemCount: logs.value.length,
+ itemBuilder: (context, index) {
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 2),
+ child: Text(
+ logs.value[index],
+ style: const TextStyle(
+ color: Colors.white,
+ fontFamily: 'monospace',
+ fontSize: 12,
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+
+ const SizedBox(height: 16),
+
+ OutlinedButton.icon(
+ onPressed: () => logs.value = [],
+ icon: const Icon(Icons.clear),
+ label: const Text('Clear Log'),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Use cases
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.tips_and_updates, color: Colors.amber),
+ SizedBox(width: 8),
+ Text(
+ 'Common Use Cases',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Force refresh after external data changes\n'
+ '• Update UI after imperative operations\n'
+ '• Sync with non-reactive data sources\n'
+ '• Trigger re-render for animations\n'
+ '• Debug rendering behavior',
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Returns a function to trigger re-render\n'
+ '• Calling update() forces component rebuild\n'
+ '• No state changes required\n'
+ '• Useful for imperative updates\n'
+ '• Should be used sparingly',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useUpdate Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Get update function
+final update = useUpdate();
+
+// Force re-render
+ElevatedButton(
+ onPressed: update,
+ child: Text('Refresh'),
+)
+
+// Update after external change
+void onExternalDataChange() {
+ // Some external data changed
+ externalDataSource.refresh();
+
+ // Force UI update
+ update();
+}
+
+// Sync with timer
+Timer.periodic(Duration(seconds: 1), (_) {
+ // Force update every second
+ update();
+});
+
+// Update after async operation
+Future performAction() async {
+ await someAsyncOperation();
+
+ // Force update to reflect changes
+ update();
+}
+
+// Conditional updates
+if (shouldRefresh) {
+ update();
+}''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/hooks/use_update_effect_demo.dart b/demo/lib/hooks/use_update_effect_demo.dart
new file mode 100644
index 0000000..4616b3c
--- /dev/null
+++ b/demo/lib/hooks/use_update_effect_demo.dart
@@ -0,0 +1,371 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_use/flutter_use.dart';
+
+class UseUpdateEffectDemo extends HookWidget {
+ const UseUpdateEffectDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final counter = useState(0);
+ final input = useState('');
+ final updateLog = useState>([]);
+ final ignoreFirstRender = useState(true);
+
+ // This effect runs only on updates, not on initial mount
+ useUpdateEffect(() {
+ final timestamp = DateTime.now().toString().substring(11, 19);
+ updateLog.value = [
+ '🔄 Update at $timestamp - Counter: ${counter.value}, Input: "${input.value}"',
+ ...updateLog.value.take(9),
+ ];
+ return null;
+ }, [counter.value, input.value]);
+
+ // Comparison with regular useEffect
+ useEffect(() {
+ if (!ignoreFirstRender.value) {
+ final timestamp = DateTime.now().toString().substring(11, 19);
+ updateLog.value = [
+ '📌 Regular effect at $timestamp (includes mount)',
+ ...updateLog.value.take(9),
+ ];
+ }
+ ignoreFirstRender.value = false;
+ return null;
+ }, [counter.value]);
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('useUpdateEffect Demo'),
+ actions: [
+ TextButton(
+ onPressed: () => _showCodeDialog(context),
+ child: const Text('📋 Code', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🔄 useUpdateEffect Demo',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Run effects only on updates, skip initial mount',
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '🎯 Update-Only Effects',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // Counter control
+ Container(
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Row(
+ children: [
+ const Icon(Icons.numbers, size: 32),
+ const SizedBox(width: 16),
+ Text(
+ 'Counter: ${counter.value}',
+ style: const TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const Spacer(),
+ IconButton.filled(
+ onPressed: () => counter.value--,
+ icon: const Icon(Icons.remove),
+ ),
+ const SizedBox(width: 8),
+ IconButton.filled(
+ onPressed: () => counter.value++,
+ icon: const Icon(Icons.add),
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 16),
+
+ // Text input
+ TextField(
+ decoration: const InputDecoration(
+ labelText: 'Text Input',
+ hintText: 'Type something to trigger update effect...',
+ border: OutlineInputBorder(),
+ prefixIcon: Icon(Icons.text_fields),
+ ),
+ onChanged: (value) => input.value = value,
+ ),
+
+ const SizedBox(height: 24),
+
+ // Update log
+ const Text(
+ 'Effect Log:',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+ Container(
+ height: 200,
+ width: double.infinity,
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: updateLog.value.isEmpty
+ ? const Center(
+ child: Text(
+ 'No updates yet. Change counter or input to see effects.',
+ style: TextStyle(color: Colors.grey),
+ textAlign: TextAlign.center,
+ ),
+ )
+ : ListView.builder(
+ itemCount: updateLog.value.length,
+ itemBuilder: (context, index) {
+ final log = updateLog.value[index];
+ final isUpdateEffect = log.startsWith('🔄');
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 4),
+ child: Text(
+ log,
+ style: TextStyle(
+ color: isUpdateEffect
+ ? Colors.blue
+ : Colors.orange,
+ fontFamily: 'monospace',
+ fontSize: 12,
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+
+ const SizedBox(height: 16),
+
+ OutlinedButton.icon(
+ onPressed: () => updateLog.value = [],
+ icon: const Icon(Icons.clear),
+ label: const Text('Clear Log'),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // Comparison card
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.compare_arrows, color: Colors.blue),
+ SizedBox(width: 8),
+ Text(
+ 'useEffect vs useUpdateEffect',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Table(
+ border: TableBorder.all(color: Colors.grey[300]!),
+ children: const [
+ TableRow(
+ decoration: BoxDecoration(color: Colors.black12),
+ children: [
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text(
+ 'useEffect',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ ),
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text(
+ 'useUpdateEffect',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ ),
+ ],
+ ),
+ TableRow(
+ children: [
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text('Runs on mount ✅'),
+ ),
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text('Skips mount ❌'),
+ ),
+ ],
+ ),
+ TableRow(
+ children: [
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text('Runs on updates ✅'),
+ ),
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text('Runs on updates ✅'),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 32),
+
+ Card(
+ elevation: 2,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Row(
+ children: [
+ Icon(Icons.lightbulb, color: Colors.orange),
+ SizedBox(width: 8),
+ Text(
+ 'How it works',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ const Text(
+ '• Skips effect execution on initial mount\n'
+ '• Only runs when dependencies change\n'
+ '• Perfect for logging updates, analytics\n'
+ '• Avoids unnecessary initial API calls\n'
+ '• Useful for form validation on change',
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: () => _showCodeDialog(context),
+ icon: const Icon(Icons.code),
+ label: const Text('View Code Example'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showCodeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('useUpdateEffect Code Example'),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey[900],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ '''// Only runs on updates, not mount
+final query = useState('');
+final results = useState>([]);
+
+useUpdateEffect(() {
+ // This won't run on initial render
+ // Only when query changes after mount
+ print('Searching for: \${query.value}');
+ searchAPI(query.value);
+ return null;
+}, [query.value]);
+
+// Track form changes (skip initial)
+final name = useState(initialName);
+final hasChanges = useState(false);
+
+useUpdateEffect(() {
+ // Mark form as dirty only on edits
+ hasChanges.value = true;
+ return null;
+}, [name.value]);
+
+// Log updates for analytics
+useUpdateEffect(() {
+ analytics.logEvent('value_changed', {
+ 'old': previousValue,
+ 'new': currentValue,
+ });
+ return null;
+}, [currentValue]);''',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/demo/lib/main.dart b/demo/lib/main.dart
new file mode 100644
index 0000000..2755afb
--- /dev/null
+++ b/demo/lib/main.dart
@@ -0,0 +1,791 @@
+import 'package:flutter/material.dart';
+import 'hooks/use_throttle_demo.dart';
+import 'hooks/use_copy_to_clipboard_demo.dart';
+import 'hooks/use_throttle_fn_demo.dart';
+import 'hooks/use_scroll_demo.dart';
+import 'hooks/use_scrolling_demo.dart';
+import 'hooks/use_click_away_demo.dart';
+import 'hooks/use_counter_demo.dart';
+import 'hooks/use_boolean_demo.dart';
+import 'hooks/use_toggle_demo.dart';
+import 'hooks/use_default_demo.dart';
+import 'hooks/use_mount_demo.dart';
+import 'hooks/use_unmount_demo.dart';
+import 'hooks/use_effect_once_demo.dart';
+import 'hooks/use_interval_demo.dart';
+import 'hooks/use_debounce_demo.dart';
+import 'hooks/use_list_demo.dart';
+import 'hooks/use_map_demo.dart';
+import 'hooks/use_update_effect_demo.dart';
+import 'hooks/use_lifecycles_demo.dart';
+import 'hooks/use_timeout_demo.dart';
+import 'hooks/use_timeout_fn_demo.dart';
+import 'hooks/use_set_demo.dart';
+import 'hooks/use_state_list_demo.dart';
+import 'hooks/use_logger_demo.dart';
+import 'hooks/use_first_mount_state_demo.dart';
+import 'hooks/use_previous_distinct_demo.dart';
+import 'hooks/use_update_demo.dart';
+import 'hooks/use_latest_demo.dart';
+import 'hooks/use_future_retry_demo.dart';
+import 'hooks/use_orientation_demo.dart';
+import 'hooks/use_text_form_validator_demo.dart';
+import 'hooks/use_error_demo.dart';
+import 'hooks/use_builds_count_demo.dart';
+import 'hooks/use_custom_compare_effect_demo.dart';
+import 'hooks/use_orientation_fn_demo.dart';
+import 'hooks/use_number_demo.dart';
+
+void main() {
+ runApp(const FlutterUseDemo());
+}
+
+class FlutterUseDemo extends StatelessWidget {
+ const FlutterUseDemo({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'Flutter Use - Interactive Demos',
+ theme: ThemeData(
+ useMaterial3: true,
+ colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
+ visualDensity: VisualDensity.adaptivePlatformDensity,
+ ),
+ initialRoute: '/',
+ routes: {
+ '/': (context) => const HomePage(),
+ '/use-throttle': (context) => const UseThrottleDemo(),
+ '/use-copy-to-clipboard': (context) => const UseCopyToClipboardDemo(),
+ '/use-throttle-fn': (context) => const UseThrottleFnDemo(),
+ '/use-scroll': (context) => const UseScrollDemo(),
+ '/use-scrolling': (context) => const UseScrollingDemo(),
+ '/use-click-away': (context) => const UseClickAwayDemo(),
+ '/use-counter': (context) => const UseCounterDemo(),
+ '/use-boolean': (context) => const UseBooleanDemo(),
+ '/use-toggle': (context) => const UseToggleDemo(),
+ '/use-default': (context) => const UseDefaultDemo(),
+ '/use-mount': (context) => const UseMountDemo(),
+ '/use-unmount': (context) => const UseUnmountDemo(),
+ '/use-effect-once': (context) => const UseEffectOnceDemo(),
+ '/use-update-effect': (context) => const UseUpdateEffectDemo(),
+ '/use-lifecycles': (context) => const UseLifecyclesDemo(),
+ '/use-interval': (context) => const UseIntervalDemo(),
+ '/use-debounce': (context) => const UseDebounceDemo(),
+ '/use-list': (context) => const UseListDemo(),
+ '/use-map': (context) => const UseMapDemo(),
+ '/use-timeout': (context) => const UseTimeoutDemo(),
+ '/use-timeout-fn': (context) => const UseTimeoutFnDemo(),
+ '/use-set': (context) => const UseSetDemo(),
+ '/use-state-list': (context) => const UseStateListDemo(),
+ '/use-logger': (context) => const UseLoggerDemo(),
+ '/use-first-mount-state': (context) => const UseFirstMountStateDemo(),
+ '/use-previous-distinct': (context) => const UsePreviousDistinctDemo(),
+ '/use-update': (context) => const UseUpdateDemo(),
+ '/use-latest': (context) => const UseLatestDemo(),
+ '/use-future-retry': (context) => const UseFutureRetryDemo(),
+ '/use-orientation': (context) => const UseOrientationDemo(),
+ '/use-text-form-validator': (context) =>
+ const UseTextFormValidatorDemo(),
+ '/use-error': (context) => const UseErrorDemo(),
+ '/use-builds-count': (context) => const UseBuildsCountDemo(),
+ '/use-custom-compare-effect': (context) =>
+ const UseCustomCompareEffectDemo(),
+ '/use-orientation-fn': (context) => const UseOrientationFnDemo(),
+ '/use-number': (context) => const UseNumberDemo(),
+ },
+ );
+ }
+}
+
+class HomePage extends StatelessWidget {
+ const HomePage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Theme.of(context).colorScheme.surface,
+ body: CustomScrollView(
+ slivers: [
+ // Sophisticated Hero App Bar
+ SliverAppBar.large(
+ title: const Text(
+ 'Flutter Use',
+ style: TextStyle(
+ fontWeight: FontWeight.w600,
+ letterSpacing: -0.5,
+ ),
+ ),
+ expandedHeight: 280,
+ pinned: true,
+ stretch: true,
+ elevation: 0,
+ backgroundColor: Colors.transparent,
+ flexibleSpace: FlexibleSpaceBar(
+ stretchModes: const [
+ StretchMode.zoomBackground,
+ StretchMode.blurBackground,
+ ],
+ background: Stack(
+ fit: StackFit.expand,
+ children: [
+ // Gradient background
+ Container(
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [
+ Theme.of(context).colorScheme.primary,
+ Theme.of(context).colorScheme.secondary,
+ Theme.of(context).colorScheme.tertiary,
+ ],
+ stops: const [0.0, 0.7, 1.0],
+ ),
+ ),
+ ),
+ // Floating shapes decoration
+ Positioned(
+ top: 40,
+ right: 30,
+ child: Container(
+ width: 100,
+ height: 100,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: Colors.white.withValues(alpha: 0.1),
+ ),
+ ),
+ ),
+ Positioned(
+ top: 120,
+ left: 20,
+ child: Container(
+ width: 60,
+ height: 60,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(16),
+ color: Colors.white.withValues(alpha: 0.1),
+ ),
+ ),
+ ),
+ // Main content
+ const Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SizedBox(height: 60),
+ // Icon with glow effect
+ Stack(
+ children: [
+ Text(
+ '⚡',
+ style: TextStyle(
+ fontSize: 64,
+ shadows: [
+ Shadow(blurRadius: 20, color: Colors.white30),
+ ],
+ ),
+ ),
+ ],
+ ),
+ SizedBox(height: 20),
+ Text(
+ 'Interactive Hook Demos',
+ style: TextStyle(
+ fontSize: 28,
+ fontWeight: FontWeight.w300,
+ color: Colors.white,
+ letterSpacing: -0.5,
+ shadows: [
+ Shadow(blurRadius: 10, color: Colors.black26),
+ ],
+ ),
+ ),
+ SizedBox(height: 8),
+ Text(
+ 'Test Flutter hooks live in your browser',
+ style: TextStyle(
+ fontSize: 16,
+ color: Colors.white70,
+ fontWeight: FontWeight.w400,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ // Content with refined spacing
+ SliverPadding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 32.0,
+ vertical: 40.0,
+ ),
+ sliver: SliverList(
+ delegate: SliverChildListDelegate([
+ // State Management Section
+ _buildRefinedSection(
+ context,
+ '🎯 State Management',
+ 'Core hooks for managing component state',
+ [
+ _buildEnhancedDemoCard(
+ context,
+ 'useCounter',
+ 'Enhanced counter with increment, decrement, set',
+ '',
+ Icons.add_circle,
+ Colors.green,
+ '/use-counter',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useBoolean',
+ 'Boolean state with toggle, setTrue, setFalse',
+ '',
+ Icons.toggle_on,
+ Colors.indigo,
+ '/use-boolean',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useToggle',
+ 'Simple toggle state management',
+ '',
+ Icons.swap_horiz,
+ Colors.amber,
+ '/use-toggle',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useDefault',
+ 'Provide default values for nullable states',
+ '',
+ Icons.settings_backup_restore,
+ Colors.brown,
+ '/use-default',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useList',
+ 'List state management with push, pop, insert',
+ '',
+ Icons.list_alt,
+ Colors.deepPurple,
+ '/use-list',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useMap',
+ 'Key-value state management with Map utilities',
+ '',
+ Icons.data_object,
+ Colors.cyan,
+ '/use-map',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useSet',
+ 'Unique value collection with Set operations',
+ '',
+ Icons.category,
+ Colors.pink,
+ '/use-set',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useStateList',
+ 'State history with undo/redo functionality',
+ '',
+ Icons.history,
+ Colors.lime,
+ '/use-state-list',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'usePreviousDistinct',
+ 'Track previous distinct values with custom comparison',
+ '',
+ Icons.compare_arrows,
+ Colors.blueGrey,
+ '/use-previous-distinct',
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 48),
+
+ // Effects & Lifecycle Section
+ _buildRefinedSection(
+ context,
+ '🎭 Effects & Lifecycle',
+ 'Manage side effects and component lifecycle',
+ [
+ _buildEnhancedDemoCard(
+ context,
+ 'useMount',
+ 'Run effects only on component mount',
+ '',
+ Icons.play_circle_filled,
+ Colors.green,
+ '/use-mount',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useUnmount',
+ 'Cleanup on component unmount',
+ '',
+ Icons.stop_circle,
+ Colors.red,
+ '/use-unmount',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useEffectOnce',
+ 'Run effect only once on mount',
+ '',
+ Icons.looks_one,
+ Colors.blue,
+ '/use-effect-once',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useUpdateEffect',
+ 'Skip effect on mount, run only on updates',
+ '',
+ Icons.update,
+ Colors.orange,
+ '/use-update-effect',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useLifecycles',
+ 'Combined mount and unmount management',
+ '',
+ Icons.sync_alt,
+ Colors.purple,
+ '/use-lifecycles',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useFirstMountState',
+ 'Detect if component is in first render',
+ '',
+ Icons.fiber_new,
+ Colors.teal,
+ '/use-first-mount-state',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useLogger',
+ 'Debug component lifecycle and state changes',
+ '',
+ Icons.bug_report,
+ Colors.deepOrange,
+ '/use-logger',
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 48),
+
+ // Enhanced Performance Hooks Section
+ _buildRefinedSection(
+ context,
+ '⚡ Performance & Optimization',
+ 'Control update frequency and optimize app performance',
+ [
+ _buildEnhancedDemoCard(
+ context,
+ 'useThrottle',
+ 'Throttle value updates to improve performance',
+ '',
+ Icons.speed,
+ Colors.blue,
+ '/use-throttle',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useThrottleFn',
+ 'Throttle function calls to prevent excessive execution',
+ '',
+ Icons.flash_on,
+ Colors.purple,
+ '/use-throttle-fn',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useDebounce',
+ 'Delay value updates until user stops changing',
+ '',
+ Icons.timer,
+ Colors.teal,
+ '/use-debounce',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useInterval',
+ 'Execute functions at regular intervals',
+ '',
+ Icons.repeat,
+ Colors.indigo,
+ '/use-interval',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useTimeout',
+ 'Execute code after a specified delay',
+ '',
+ Icons.timer_outlined,
+ Colors.amber,
+ '/use-timeout',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useTimeoutFn',
+ 'Controllable timeout with start/stop/reset',
+ '',
+ Icons.av_timer,
+ Colors.deepOrange,
+ '/use-timeout-fn',
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 48),
+
+ // Scroll Hooks Section
+ _buildRefinedSection(
+ context,
+ '📜 Scroll & Navigation',
+ 'Track and respond to scroll events and position',
+ [
+ _buildEnhancedDemoCard(
+ context,
+ 'useScroll',
+ 'Track scroll position in real-time',
+ '',
+ Icons.height,
+ Colors.teal,
+ '/use-scroll',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useScrolling',
+ 'Detect when user is actively scrolling',
+ '',
+ Icons.directions_run,
+ Colors.orange,
+ '/use-scrolling',
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 48),
+
+ // Enhanced Utility Hooks Section
+ _buildRefinedSection(
+ context,
+ '🛠️ Utility & Integration',
+ 'Common utility functions for everyday development',
+ [
+ _buildEnhancedDemoCard(
+ context,
+ 'useCopyToClipboard',
+ 'Copy text to clipboard with status feedback',
+ '',
+ Icons.content_copy,
+ Colors.green,
+ '/use-copy-to-clipboard',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useClickAway',
+ 'Detect clicks outside of specific elements',
+ '',
+ Icons.touch_app,
+ Colors.red,
+ '/use-click-away',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useUpdate',
+ 'Force component re-render on demand',
+ '',
+ Icons.refresh,
+ Colors.blue,
+ '/use-update',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useLatest',
+ 'Access latest value in stale closures',
+ '',
+ Icons.push_pin,
+ Colors.purple,
+ '/use-latest',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useFutureRetry',
+ 'Async operations with retry capability',
+ '',
+ Icons.replay,
+ Colors.indigo,
+ '/use-future-retry',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useOrientation',
+ 'Track device orientation changes',
+ '',
+ Icons.screen_rotation,
+ Colors.cyan,
+ '/use-orientation',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useTextFormValidator',
+ 'Reactive form field validation',
+ '',
+ Icons.check_box,
+ Colors.lime,
+ '/use-text-form-validator',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useError',
+ 'Error and exception state management',
+ '',
+ Icons.error_outline,
+ Colors.red,
+ '/use-error',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useBuildsCount',
+ 'Track widget rebuild count',
+ '',
+ Icons.numbers,
+ Colors.brown,
+ '/use-builds-count',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useCustomCompareEffect',
+ 'Custom dependency comparison for effects',
+ '',
+ Icons.compare,
+ Colors.deepPurple,
+ '/use-custom-compare-effect',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useOrientationFn',
+ 'Callback-based orientation change detection',
+ '',
+ Icons.screen_lock_rotation,
+ Colors.teal,
+ '/use-orientation-fn',
+ ),
+ _buildEnhancedDemoCard(
+ context,
+ 'useNumber',
+ 'Numeric value management (alias for useCounter)',
+ '',
+ Icons.calculate,
+ Colors.green,
+ '/use-number',
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 60),
+
+ // Elegant divider
+ Container(
+ height: 1,
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ Colors.transparent,
+ Theme.of(
+ context,
+ ).colorScheme.outline.withValues(alpha: 0.3),
+ Colors.transparent,
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 40),
+
+ // Enhanced Footer
+ Center(
+ child: Column(
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 24,
+ vertical: 16,
+ ),
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(24),
+ border: Border.all(
+ color: Theme.of(
+ context,
+ ).colorScheme.outline.withValues(alpha: 0.1),
+ ),
+ ),
+ child: Column(
+ children: [
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ Icons.rocket_launch_outlined,
+ size: 20,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Powered by Flutter Web',
+ style: TextStyle(
+ fontWeight: FontWeight.w500,
+ color: Theme.of(
+ context,
+ ).colorScheme.onSurface,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ Icons.menu_book_outlined,
+ size: 16,
+ color: Theme.of(
+ context,
+ ).colorScheme.onSurfaceVariant,
+ ),
+ const SizedBox(width: 6),
+ Text(
+ 'github.com/wasabeef/flutter_use',
+ style: TextStyle(
+ fontSize: 13,
+ color: Theme.of(
+ context,
+ ).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 40),
+ ]),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildRefinedSection(
+ BuildContext context,
+ String title,
+ String subtitle,
+ List cards,
+ ) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ title,
+ style: TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.w600,
+ color: Theme.of(context).colorScheme.onSurface,
+ letterSpacing: -0.5,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ subtitle,
+ style: TextStyle(
+ fontSize: 16,
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ fontWeight: FontWeight.w400,
+ ),
+ ),
+ const SizedBox(height: 24),
+ Wrap(spacing: 20, runSpacing: 20, children: cards),
+ ],
+ );
+ }
+
+ Widget _buildEnhancedDemoCard(
+ BuildContext context,
+ String title,
+ String description,
+ String subDescription,
+ IconData icon,
+ Color accentColor,
+ String route,
+ ) {
+ return SizedBox(
+ width: 320,
+ child: Card(
+ elevation: 4,
+ child: InkWell(
+ onTap: () => Navigator.pushNamed(context, route),
+ borderRadius: BorderRadius.circular(16),
+ child: Padding(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Row(
+ children: [
+ Icon(
+ icon,
+ size: 24,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ title,
+ style: const TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ const Icon(Icons.arrow_forward_ios, size: 16),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Text(description, style: const TextStyle(color: Colors.grey)),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/demo/pubspec.yaml b/demo/pubspec.yaml
new file mode 100644
index 0000000..d6a3227
--- /dev/null
+++ b/demo/pubspec.yaml
@@ -0,0 +1,90 @@
+name: demo
+description: "A new Flutter project."
+# 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
+
+# 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 is used as CFBundleVersion.
+# Read more about iOS versioning at
+# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
+# In Windows, build-name is used as the major, minor, and patch parts
+# of the product and file versions while build-number is used as the build suffix.
+version: 1.0.0+1
+
+environment:
+ sdk: ^3.8.1
+
+# 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
+ flutter_use:
+ path: ../packages/basic
+ flutter_hooks: ^0.21.0
+ cupertino_icons: ^1.0.8
+
+ collection: any
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+ # 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: ^5.0.0
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter packages.
+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/to/resolution-aware-images
+
+ # For details regarding adding assets from package dependencies, see
+ # https://flutter.dev/to/asset-from-package
+
+ # 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/to/font-from-package
diff --git a/demo/test/widget_test.dart b/demo/test/widget_test.dart
new file mode 100644
index 0000000..129fcbd
--- /dev/null
+++ b/demo/test/widget_test.dart
@@ -0,0 +1,21 @@
+// This is a basic Flutter widget test.
+//
+// To perform an interaction with a widget in your test, use the WidgetTester
+// utility in the flutter_test package. 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.
+
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:demo/main.dart';
+
+void main() {
+ testWidgets('FlutterUseDemo app test', (WidgetTester tester) async {
+ // Build our app and trigger a frame.
+ await tester.pumpWidget(const FlutterUseDemo());
+
+ // Verify that the app starts with the home page
+ expect(find.text('Flutter Use - Interactive Demos'), findsOneWidget);
+ expect(find.text('Explore React-like hooks for Flutter'), findsOneWidget);
+ });
+}
diff --git a/demo/web/favicon.png b/demo/web/favicon.png
new file mode 100644
index 0000000..8aaa46a
Binary files /dev/null and b/demo/web/favicon.png differ
diff --git a/demo/web/icons/Icon-192.png b/demo/web/icons/Icon-192.png
new file mode 100644
index 0000000..b749bfe
Binary files /dev/null and b/demo/web/icons/Icon-192.png differ
diff --git a/demo/web/icons/Icon-512.png b/demo/web/icons/Icon-512.png
new file mode 100644
index 0000000..88cfd48
Binary files /dev/null and b/demo/web/icons/Icon-512.png differ
diff --git a/demo/web/icons/Icon-maskable-192.png b/demo/web/icons/Icon-maskable-192.png
new file mode 100644
index 0000000..eb9b4d7
Binary files /dev/null and b/demo/web/icons/Icon-maskable-192.png differ
diff --git a/demo/web/icons/Icon-maskable-512.png b/demo/web/icons/Icon-maskable-512.png
new file mode 100644
index 0000000..d69c566
Binary files /dev/null and b/demo/web/icons/Icon-maskable-512.png differ
diff --git a/demo/web/index.html b/demo/web/index.html
new file mode 100644
index 0000000..ffc2107
--- /dev/null
+++ b/demo/web/index.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ demo
+
+
+
+
+
+
diff --git a/demo/web/manifest.json b/demo/web/manifest.json
new file mode 100644
index 0000000..238a284
--- /dev/null
+++ b/demo/web/manifest.json
@@ -0,0 +1,35 @@
+{
+ "name": "demo",
+ "short_name": "demo",
+ "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/docs/useClickAway.md b/docs/useClickAway.md
new file mode 100644
index 0000000..4b59f26
--- /dev/null
+++ b/docs/useClickAway.md
@@ -0,0 +1,51 @@
+# `useClickAway`
+
+Flutter state hook that detects clicks outside a target widget and calls a callback function.
+
+## Installation
+
+```yaml
+dependencies:
+ flutter_use:
+```
+
+## Usage
+
+```dart
+class Sample extends HookWidget {
+ @override
+ Widget build(BuildContext context) {
+ final showDropdown = useState(false);
+ final clickAway = useClickAway(() {
+ showDropdown.value = false;
+ });
+
+ return Column(
+ children: [
+ ElevatedButton(
+ onPressed: () => showDropdown.value = !showDropdown.value,
+ child: Text('Toggle Dropdown'),
+ ),
+ if (showDropdown.value)
+ Container(
+ key: clickAway.ref,
+ width: 200,
+ height: 100,
+ color: Colors.blue,
+ child: Center(
+ child: Text(
+ 'Click outside to close',
+ style: TextStyle(color: Colors.white),
+ ),
+ ),
+ ),
+ ]
+ );
+ }
+}
+```
+
+## Reference
+
+- **`ref`**_`: GlobalKey`_ - a global key to attach to the target widget;
+- **`onClickAway`**_`: VoidCallback`_ - callback function called when clicking outside the target;
\ No newline at end of file
diff --git a/docs/useCopyToClipboard.md b/docs/useCopyToClipboard.md
new file mode 100644
index 0000000..b9ea98c
--- /dev/null
+++ b/docs/useCopyToClipboard.md
@@ -0,0 +1,48 @@
+# `useCopyToClipboard`
+
+Flutter state hook that provides functionality to copy text to the clipboard.
+
+## Installation
+
+```yaml
+dependencies:
+ flutter_use:
+```
+
+## Usage
+
+```dart
+class Sample extends HookWidget {
+ @override
+ Widget build(BuildContext context) {
+ final copyToClipboard = useCopyToClipboard();
+
+ return Column(
+ children: [
+ if (copyToClipboard.copied != null)
+ Text("Copied: ${copyToClipboard.copied}"),
+ if (copyToClipboard.error != null)
+ Text("Error: ${copyToClipboard.error}"),
+ ElevatedButton(
+ onPressed: () {
+ copyToClipboard.copy("Hello, World!");
+ },
+ child: const Text('Copy Text'),
+ ),
+ ElevatedButton(
+ onPressed: () {
+ copyToClipboard.copy("flutter_use is awesome!");
+ },
+ child: const Text('Copy Another Text'),
+ ),
+ ]
+ );
+ }
+}
+```
+
+## Reference
+
+- **`copied`**_`: String?`_ - the last successfully copied text;
+- **`error`**_`: Object?`_ - error occurred during copying, if any;
+- **`copy(String)`** - copies the given text to clipboard;
\ No newline at end of file
diff --git a/docs/useScroll.md b/docs/useScroll.md
new file mode 100644
index 0000000..6d6efad
--- /dev/null
+++ b/docs/useScroll.md
@@ -0,0 +1,45 @@
+# `useScroll`
+
+Flutter state hook that tracks a widget's scroll position.
+
+## Installation
+
+```yaml
+dependencies:
+ flutter_use:
+```
+
+## Usage
+
+```dart
+class Sample extends HookWidget {
+ @override
+ Widget build(BuildContext context) {
+ final scrollState = useScroll();
+
+ return Column(
+ children: [
+ Text("X: ${scrollState.x.toStringAsFixed(2)}"),
+ Text("Y: ${scrollState.y.toStringAsFixed(2)}"),
+ Expanded(
+ child: ListView.builder(
+ controller: scrollState.controller,
+ itemCount: 100,
+ itemBuilder: (context, index) {
+ return ListTile(
+ title: Text('Item $index'),
+ );
+ },
+ ),
+ ),
+ ]
+ );
+ }
+}
+```
+
+## Reference
+
+- **`x`**_`: double`_ - horizontal scroll position;
+- **`y`**_`: double`_ - vertical scroll position;
+- **`controller`**_`: ScrollController`_ - scroll controller to attach to scrollable widget;
\ No newline at end of file
diff --git a/docs/useScrolling.md b/docs/useScrolling.md
new file mode 100644
index 0000000..159a13e
--- /dev/null
+++ b/docs/useScrolling.md
@@ -0,0 +1,50 @@
+# `useScrolling`
+
+Flutter state hook that tracks whether a widget is currently scrolling.
+
+## Installation
+
+```yaml
+dependencies:
+ flutter_use:
+```
+
+## Usage
+
+```dart
+class Sample extends HookWidget {
+ @override
+ Widget build(BuildContext context) {
+ final scrollingState = useScrolling();
+
+ return Column(
+ children: [
+ Container(
+ padding: EdgeInsets.all(16),
+ color: scrollingState.isScrolling ? Colors.red : Colors.green,
+ child: Text(
+ scrollingState.isScrolling ? "Scrolling..." : "Not scrolling",
+ style: TextStyle(color: Colors.white),
+ ),
+ ),
+ Expanded(
+ child: ListView.builder(
+ controller: scrollingState.controller,
+ itemCount: 100,
+ itemBuilder: (context, index) {
+ return ListTile(
+ title: Text('Item $index'),
+ );
+ },
+ ),
+ ),
+ ]
+ );
+ }
+}
+```
+
+## Reference
+
+- **`isScrolling`**_`: bool`_ - whether the widget is currently scrolling;
+- **`controller`**_`: ScrollController`_ - scroll controller to attach to scrollable widget;
\ No newline at end of file
diff --git a/docs/useThrottle.md b/docs/useThrottle.md
new file mode 100644
index 0000000..455ddda
--- /dev/null
+++ b/docs/useThrottle.md
@@ -0,0 +1,42 @@
+# `useThrottle`
+
+Flutter state hook that throttles a value to update at most once per specified duration.
+
+## Installation
+
+```yaml
+dependencies:
+ flutter_use:
+```
+
+## Usage
+
+```dart
+class Sample extends HookWidget {
+ @override
+ Widget build(BuildContext context) {
+ final input = useState("");
+ final throttledValue = useThrottle(input.value, Duration(milliseconds: 500));
+
+ return Column(
+ children: [
+ TextField(
+ onChanged: (value) => input.value = value,
+ decoration: InputDecoration(
+ labelText: 'Type something...',
+ ),
+ ),
+ Text("Input: ${input.value}"),
+ Text("Throttled: $throttledValue"),
+ Text("Updates at most once every 500ms"),
+ ]
+ );
+ }
+}
+```
+
+## Reference
+
+- **Returns**_`: T`_ - the throttled value that updates at most once per duration;
+- **`value`**_`: T`_ - the input value to throttle;
+- **`duration`**_`: Duration`_ - the minimum time between updates;
\ No newline at end of file
diff --git a/docs/useThrottleFn.md b/docs/useThrottleFn.md
new file mode 100644
index 0000000..d254396
--- /dev/null
+++ b/docs/useThrottleFn.md
@@ -0,0 +1,43 @@
+# `useThrottleFn`
+
+Flutter state hook that throttles a function to execute at most once per specified duration.
+
+## Installation
+
+```yaml
+dependencies:
+ flutter_use:
+```
+
+## Usage
+
+```dart
+class Sample extends HookWidget {
+ @override
+ Widget build(BuildContext context) {
+ final counter = useState(0);
+
+ final throttledIncrement = useThrottleFn(
+ () => counter.value++,
+ Duration(milliseconds: 1000),
+ );
+
+ return Column(
+ children: [
+ Text("Counter: ${counter.value}"),
+ ElevatedButton(
+ onPressed: throttledIncrement,
+ child: const Text('Increment (throttled)'),
+ ),
+ Text("Function executes at most once per second"),
+ ]
+ );
+ }
+}
+```
+
+## Reference
+
+- **Returns**_`: VoidCallback`_ - the throttled function;
+- **`fn`**_`: VoidCallback`_ - the function to throttle;
+- **`duration`**_`: Duration`_ - the minimum time between function executions;
\ No newline at end of file
diff --git a/lefthook.yml b/lefthook.yml
new file mode 100644
index 0000000..5d374e5
--- /dev/null
+++ b/lefthook.yml
@@ -0,0 +1,14 @@
+# lefthook.yml
+# https://github.com/evilmartians/lefthook
+
+pre-commit:
+ parallel: true
+ commands:
+ dart-format:
+ glob: "**/*.dart"
+ run: dart format {staged_files}
+ stage_fixed: true
+ prettier:
+ glob: "*.{json,yaml,yml,md}"
+ run: npx prettier --write {staged_files}
+ stage_fixed: true
diff --git a/melos.yaml b/melos.yaml
index 2d6b1f0..ca8d084 100644
--- a/melos.yaml
+++ b/melos.yaml
@@ -3,6 +3,7 @@ name: flutter_use
packages:
- packages/**
- packages/**/example/*
+ - demo
ide:
intellij: true
@@ -16,10 +17,51 @@ scripts:
analyze: melos exec -- dart analyze .
- format: dart format .
+ format:
+ run: dart format .
+ description: Format all Dart code
+
+ check:
+ run: dart format --set-exit-if-changed .
+ description: Check if code is properly formatted
test:
run: |
melos exec -c 8 -- flutter test
packageFilters:
scope: flutter_use
+ description: Run tests for flutter_use package
+
+ coverage:
+ run: |
+ melos exec -c 8 --scope="flutter_use" -- flutter test --coverage
+ description: Run tests with coverage for flutter_use package
+
+ test-coverage:
+ run: melos run coverage
+ description: Run tests with coverage for flutter_use package
+
+ report:
+ run: |
+ melos exec -c 1 --scope="flutter_use" -- genhtml coverage/lcov.info -o coverage/html
+ description: Generate HTML coverage report
+
+ clean:
+ run: melos exec -- flutter clean
+ description: Clean all packages
+
+ fix:
+ run: |
+ melos exec -- dart fix --apply
+ dart format .
+ description: Auto-fix lint issues and format code across all packages
+
+ demo-build:
+ run: |
+ melos exec --scope="demo" -- flutter build web --release --base-href "/flutter_use/"
+ description: Build demo app for GitHub Pages
+
+ demo-run:
+ run: |
+ melos exec --scope="demo" -- flutter run -d web-server --web-port 8080
+ description: Run demo app on web server
diff --git a/package.json b/package.json
index fe905e4..93950a5 100644
--- a/package.json
+++ b/package.json
@@ -2,18 +2,12 @@
"name": "flutter_use",
"private": false,
"author": "wasabeef",
- "devDependencies": {
- "husky": "8.0.3",
- "lint-staged": "13.2.3",
- "prettier": "^3.0.0"
- },
"scripts": {
- "prepare": "husky install"
+ "prepare": "lefthook install",
+ "format": "prettier --write ."
},
- "lint-staged": {
- "*.dart": "dart format",
- "*.@(json|yaml|yml)": [
- "prettier --write"
- ]
+ "devDependencies": {
+ "@evilmartians/lefthook": "^1.5.5",
+ "prettier": "^3.5.3"
}
}
diff --git a/packages/audio/lib/flutter_use_audio.dart b/packages/audio/lib/flutter_use_audio.dart
index cfff2c6..36d4b85 100644
--- a/packages/audio/lib/flutter_use_audio.dart
+++ b/packages/audio/lib/flutter_use_audio.dart
@@ -30,19 +30,19 @@ AudioPlayer useAudio({
],
);
- useEffect(() {
- return () {
- audio.stop();
- audio.dispose();
- };
- }, [
- userAgent,
- handleInterruptions,
- androidApplyAudioAttributes,
- handleAudioSessionActivation,
- audioLoadConfiguration,
- audioPipeline,
- ]);
+ useEffect(
+ () => () {
+ audio.stop();
+ audio.dispose();
+ },
+ [
+ userAgent,
+ handleInterruptions,
+ androidApplyAudioAttributes,
+ handleAudioSessionActivation,
+ audioLoadConfiguration,
+ audioPipeline,
+ ]);
return audio;
}
diff --git a/packages/basic/example/README.md b/packages/basic/example/README.md
deleted file mode 100644
index 20a29ca..0000000
--- a/packages/basic/example/README.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# Example
-
-A sample project using flutter_use.
-
-```yaml
-melos run pub:get
-melos run example
-```
diff --git a/packages/basic/example/android/.gitignore b/packages/basic/example/android/.gitignore
deleted file mode 100644
index 6f56801..0000000
--- a/packages/basic/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/packages/basic/example/android/app/build.gradle b/packages/basic/example/android/app/build.gradle
deleted file mode 100644
index 56bfa9b..0000000
--- a/packages/basic/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 30
-
- 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 16
- targetSdkVersion 30
- 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/packages/basic/example/android/app/src/debug/AndroidManifest.xml b/packages/basic/example/android/app/src/debug/AndroidManifest.xml
deleted file mode 100644
index c208884..0000000
--- a/packages/basic/example/android/app/src/debug/AndroidManifest.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/packages/basic/example/android/app/src/main/AndroidManifest.xml b/packages/basic/example/android/app/src/main/AndroidManifest.xml
deleted file mode 100644
index 5827c4f..0000000
--- a/packages/basic/example/android/app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/basic/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/packages/basic/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt
deleted file mode 100644
index e793a00..0000000
--- a/packages/basic/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/packages/basic/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/basic/example/android/app/src/main/res/drawable-v21/launch_background.xml
deleted file mode 100644
index f74085f..0000000
--- a/packages/basic/example/android/app/src/main/res/drawable-v21/launch_background.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/packages/basic/example/android/app/src/main/res/drawable/launch_background.xml b/packages/basic/example/android/app/src/main/res/drawable/launch_background.xml
deleted file mode 100644
index 304732f..0000000
--- a/packages/basic/example/android/app/src/main/res/drawable/launch_background.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/packages/basic/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/basic/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index db77bb4..0000000
Binary files a/packages/basic/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/packages/basic/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/basic/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index 17987b7..0000000
Binary files a/packages/basic/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/packages/basic/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/basic/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index 09d4391..0000000
Binary files a/packages/basic/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/packages/basic/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/basic/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index d5f1c8d..0000000
Binary files a/packages/basic/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/packages/basic/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/basic/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 4d6372e..0000000
Binary files a/packages/basic/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/packages/basic/example/android/app/src/main/res/values-night/styles.xml b/packages/basic/example/android/app/src/main/res/values-night/styles.xml
deleted file mode 100644
index 449a9f9..0000000
--- a/packages/basic/example/android/app/src/main/res/values-night/styles.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
diff --git a/packages/basic/example/android/app/src/main/res/values/styles.xml b/packages/basic/example/android/app/src/main/res/values/styles.xml
deleted file mode 100644
index d74aa35..0000000
--- a/packages/basic/example/android/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
diff --git a/packages/basic/example/android/app/src/profile/AndroidManifest.xml b/packages/basic/example/android/app/src/profile/AndroidManifest.xml
deleted file mode 100644
index c208884..0000000
--- a/packages/basic/example/android/app/src/profile/AndroidManifest.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/packages/basic/example/android/build.gradle b/packages/basic/example/android/build.gradle
deleted file mode 100644
index 966e23b..0000000
--- a/packages/basic/example/android/build.gradle
+++ /dev/null
@@ -1,29 +0,0 @@
-buildscript {
- ext.kotlin_version = '1.7.10'
- repositories {
- google()
- mavenCentral()
- }
-
- dependencies {
- classpath 'com.android.tools.build:gradle:7.2.2'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- }
-}
-
-allprojects {
- repositories {
- google()
- mavenCentral()
- }
-}
-
-rootProject.buildDir = '../build'
-subprojects {
- project.buildDir = "${rootProject.buildDir}/${project.name}"
- project.evaluationDependsOn(':app')
-}
-
-task clean(type: Delete) {
- delete rootProject.buildDir
-}
diff --git a/packages/basic/example/android/gradle.properties b/packages/basic/example/android/gradle.properties
deleted file mode 100644
index 94adc3a..0000000
--- a/packages/basic/example/android/gradle.properties
+++ /dev/null
@@ -1,3 +0,0 @@
-org.gradle.jvmargs=-Xmx1536M
-android.useAndroidX=true
-android.enableJetifier=true
diff --git a/packages/basic/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/basic/example/android/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index 53d5a7c..0000000
--- a/packages/basic/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-7.5.1-bin.zip
diff --git a/packages/basic/example/android/settings.gradle b/packages/basic/example/android/settings.gradle
deleted file mode 100644
index 44e62bc..0000000
--- a/packages/basic/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/packages/basic/example/ios/Flutter/AppFrameworkInfo.plist b/packages/basic/example/ios/Flutter/AppFrameworkInfo.plist
deleted file mode 100644
index 8d4492f..0000000
--- a/packages/basic/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
- 9.0
-
-
diff --git a/packages/basic/example/ios/Flutter/Debug.xcconfig b/packages/basic/example/ios/Flutter/Debug.xcconfig
deleted file mode 100644
index ec97fc6..0000000
--- a/packages/basic/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/packages/basic/example/ios/Flutter/Release.xcconfig b/packages/basic/example/ios/Flutter/Release.xcconfig
deleted file mode 100644
index c4855bf..0000000
--- a/packages/basic/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/packages/basic/example/ios/Podfile b/packages/basic/example/ios/Podfile
deleted file mode 100644
index 1e8c3c9..0000000
--- a/packages/basic/example/ios/Podfile
+++ /dev/null
@@ -1,41 +0,0 @@
-# Uncomment this line to define a global platform for your project
-# platform :ios, '9.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/packages/basic/example/ios/Runner.xcodeproj/project.pbxproj b/packages/basic/example/ios/Runner.xcodeproj/project.pbxproj
deleted file mode 100644
index c6759a6..0000000
--- a/packages/basic/example/ios/Runner.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,471 +0,0 @@
-// !$*UTF8*$!
-{
- archiveVersion = 1;
- classes = {
- };
- objectVersion = 46;
- 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 */; };
- 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 = ""; };
- 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
- 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 = ""; };
-/* End PBXFileReference section */
-
-/* Begin PBXFrameworksBuildPhase section */
- 97C146EB1CF9000F007C117D /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- 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 */,
- );
- 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 = "";
- };
-/* End PBXGroup section */
-
-/* Begin PBXNativeTarget section */
- 97C146ED1CF9000F007C117D /* Runner */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
- buildPhases = (
- 9740EEB61CF901F6004384FC /* Run Script */,
- 97C146EA1CF9000F007C117D /* Sources */,
- 97C146EB1CF9000F007C117D /* Frameworks */,
- 97C146EC1CF9000F007C117D /* Resources */,
- 9705A1C41CF9048500538489 /* Embed Frameworks */,
- 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
- );
- 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 = 1020;
- 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 */
- 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
- isa = PBXShellScriptBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- inputPaths = (
- );
- 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;
- 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";
- };
-/* 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 = 9.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 = 9.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 = 9.0;
- MTL_ENABLE_DEBUG_INFO = NO;
- SDKROOT = iphoneos;
- SUPPORTED_PLATFORMS = iphoneos;
- SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
- 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/packages/basic/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/basic/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 919434a..0000000
--- a/packages/basic/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/packages/basic/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/basic/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d9810..0000000
--- a/packages/basic/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/packages/basic/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/basic/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
deleted file mode 100644
index f9b0d7c..0000000
--- a/packages/basic/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- PreviewsEnabled
-
-
-
diff --git a/packages/basic/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/basic/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
deleted file mode 100644
index a28140c..0000000
--- a/packages/basic/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ /dev/null
@@ -1,91 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/basic/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/basic/example/ios/Runner.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 1d526a1..0000000
--- a/packages/basic/example/ios/Runner.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/packages/basic/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/basic/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d9810..0000000
--- a/packages/basic/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/packages/basic/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/basic/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
deleted file mode 100644
index f9b0d7c..0000000
--- a/packages/basic/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- PreviewsEnabled
-
-
-
diff --git a/packages/basic/example/ios/Runner/AppDelegate.swift b/packages/basic/example/ios/Runner/AppDelegate.swift
deleted file mode 100644
index 70693e4..0000000
--- a/packages/basic/example/ios/Runner/AppDelegate.swift
+++ /dev/null
@@ -1,13 +0,0 @@
-import UIKit
-import Flutter
-
-@UIApplicationMain
-@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/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index d36b1fa..0000000
--- a/packages/basic/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/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
deleted file mode 100644
index dc9ada4..0000000
Binary files a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ
diff --git a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
deleted file mode 100644
index 28c6bf0..0000000
Binary files a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ
diff --git a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
deleted file mode 100644
index 2ccbfd9..0000000
Binary files a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ
diff --git a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
deleted file mode 100644
index f091b6b..0000000
Binary files a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ
diff --git a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
deleted file mode 100644
index 4cde121..0000000
Binary files a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ
diff --git a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
deleted file mode 100644
index d0ef06e..0000000
Binary files a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ
diff --git a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
deleted file mode 100644
index dcdc230..0000000
Binary files a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ
diff --git a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
deleted file mode 100644
index 2ccbfd9..0000000
Binary files a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ
diff --git a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
deleted file mode 100644
index c8f9ed8..0000000
Binary files a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ
diff --git a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
deleted file mode 100644
index a6d6b86..0000000
Binary files a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ
diff --git a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
deleted file mode 100644
index a6d6b86..0000000
Binary files a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ
diff --git a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
deleted file mode 100644
index 75b2d16..0000000
Binary files a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ
diff --git a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
deleted file mode 100644
index c4df70d..0000000
Binary files a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ
diff --git a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
deleted file mode 100644
index 6a84f41..0000000
Binary files a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ
diff --git a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
deleted file mode 100644
index d0e1f58..0000000
Binary files a/packages/basic/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ
diff --git a/packages/basic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/basic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
deleted file mode 100644
index 0bedcf2..0000000
--- a/packages/basic/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/packages/basic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/basic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
deleted file mode 100644
index 9da19ea..0000000
Binary files a/packages/basic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ
diff --git a/packages/basic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/basic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
deleted file mode 100644
index 9da19ea..0000000
Binary files a/packages/basic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ
diff --git a/packages/basic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/basic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
deleted file mode 100644
index 9da19ea..0000000
Binary files a/packages/basic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ
diff --git a/packages/basic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/basic/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
deleted file mode 100644
index 89c2725..0000000
--- a/packages/basic/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/packages/basic/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/basic/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
deleted file mode 100644
index f2e259c..0000000
--- a/packages/basic/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/basic/example/ios/Runner/Base.lproj/Main.storyboard b/packages/basic/example/ios/Runner/Base.lproj/Main.storyboard
deleted file mode 100644
index f3c2851..0000000
--- a/packages/basic/example/ios/Runner/Base.lproj/Main.storyboard
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/basic/example/ios/Runner/Info.plist b/packages/basic/example/ios/Runner/Info.plist
deleted file mode 100644
index a060db6..0000000
--- a/packages/basic/example/ios/Runner/Info.plist
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- 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
-
-
-
diff --git a/packages/basic/example/ios/Runner/Runner-Bridging-Header.h b/packages/basic/example/ios/Runner/Runner-Bridging-Header.h
deleted file mode 100644
index 308a2a5..0000000
--- a/packages/basic/example/ios/Runner/Runner-Bridging-Header.h
+++ /dev/null
@@ -1 +0,0 @@
-#import "GeneratedPluginRegistrant.h"
diff --git a/packages/basic/example/lib/main.dart b/packages/basic/example/lib/main.dart
deleted file mode 100644
index 041f1c9..0000000
--- a/packages/basic/example/lib/main.dart
+++ /dev/null
@@ -1,83 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:flutter_use/flutter_use.dart';
-
-void main() {
- runApp(const MyApp());
-}
-
-class MyApp extends StatelessWidget {
- const MyApp({Key? key}) : super(key: key);
-
- @override
- Widget build(BuildContext context) {
- return MaterialApp(
- title: 'Flutter Demo',
- theme: ThemeData(
- primarySwatch: Colors.blue,
- ),
- home: const MyHomePage(),
- );
- }
-}
-
-class SampleError extends Error {
- SampleError(this.message);
- final String message;
-}
-
-class UseError extends Error {}
-
-class SampleException implements Exception {}
-
-class UseException implements Exception {}
-
-class MyHomePage extends HookWidget {
- const MyHomePage({Key? key}) : super(key: key);
-
- @override
- Widget build(BuildContext context) {
- debugPrint("build");
-
- final toggleState = useToggle(false);
-
- return Scaffold(
- body: SingleChildScrollView(
- padding: const EdgeInsets.symmetric(vertical: 32),
- child: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- const Text("-- Toggle --"),
- Text("${toggleState.value}"),
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- ElevatedButton(
- onPressed: () {
- toggleState.toggle();
- },
- child: const Text('toggle'),
- ),
- ElevatedButton(
- onPressed: () {
- toggleState.toggle(true);
- },
- child: const Text('true'),
- ),
- ElevatedButton(
- onPressed: () {
- toggleState.toggle(false);
- },
- child: const Text('false'),
- ),
- ],
- ),
- ],
- ),
- ),
- ),
- );
- }
-}
diff --git a/packages/basic/example/pubspec.lock b/packages/basic/example/pubspec.lock
deleted file mode 100644
index ea7a569..0000000
--- a/packages/basic/example/pubspec.lock
+++ /dev/null
@@ -1,388 +0,0 @@
-# Generated by pub
-# See https://dart.dev/tools/pub/glossary#lockfile
-packages:
- _fe_analyzer_shared:
- dependency: transitive
- description:
- name: _fe_analyzer_shared
- sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f
- url: "https://pub.dev"
- source: hosted
- version: "82.0.0"
- analyzer:
- dependency: transitive
- description:
- name: analyzer
- sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0"
- url: "https://pub.dev"
- source: hosted
- version: "7.4.5"
- args:
- dependency: transitive
- description:
- name: args
- sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
- url: "https://pub.dev"
- source: hosted
- version: "2.7.0"
- async:
- dependency: transitive
- description:
- name: async
- sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
- url: "https://pub.dev"
- source: hosted
- version: "2.13.0"
- boolean_selector:
- dependency: transitive
- description:
- name: boolean_selector
- sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
- url: "https://pub.dev"
- source: hosted
- version: "2.1.2"
- build:
- dependency: transitive
- description:
- name: build
- sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0
- url: "https://pub.dev"
- source: hosted
- version: "2.4.2"
- built_collection:
- dependency: transitive
- description:
- name: built_collection
- sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
- url: "https://pub.dev"
- source: hosted
- version: "5.1.1"
- built_value:
- dependency: transitive
- description:
- name: built_value
- sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27"
- url: "https://pub.dev"
- source: hosted
- version: "8.10.1"
- characters:
- dependency: transitive
- description:
- name: characters
- sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
- url: "https://pub.dev"
- source: hosted
- version: "1.4.0"
- clock:
- dependency: transitive
- description:
- name: clock
- sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
- url: "https://pub.dev"
- source: hosted
- version: "1.1.2"
- code_builder:
- dependency: transitive
- description:
- name: code_builder
- sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
- url: "https://pub.dev"
- source: hosted
- version: "4.10.1"
- collection:
- dependency: transitive
- description:
- name: collection
- sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
- url: "https://pub.dev"
- source: hosted
- version: "1.19.1"
- convert:
- dependency: transitive
- description:
- name: convert
- sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
- url: "https://pub.dev"
- source: hosted
- version: "3.1.2"
- crypto:
- dependency: transitive
- description:
- name: crypto
- sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
- url: "https://pub.dev"
- source: hosted
- version: "3.0.6"
- dart_style:
- dependency: transitive
- description:
- name: dart_style
- sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af"
- url: "https://pub.dev"
- source: hosted
- version: "3.1.0"
- fake_async:
- dependency: transitive
- description:
- name: fake_async
- sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
- url: "https://pub.dev"
- source: hosted
- version: "1.3.3"
- file:
- dependency: transitive
- description:
- name: file
- sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
- url: "https://pub.dev"
- source: hosted
- version: "7.0.1"
- fixnum:
- dependency: transitive
- description:
- name: fixnum
- sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
- url: "https://pub.dev"
- source: hosted
- version: "1.1.1"
- flutter:
- dependency: "direct main"
- description: flutter
- source: sdk
- version: "0.0.0"
- flutter_hooks:
- dependency: "direct main"
- description:
- name: flutter_hooks
- sha256: b772e710d16d7a20c0740c4f855095026b31c7eb5ba3ab67d2bd52021cd9461d
- url: "https://pub.dev"
- source: hosted
- version: "0.21.2"
- flutter_lints:
- dependency: "direct dev"
- description:
- name: flutter_lints
- sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
- url: "https://pub.dev"
- source: hosted
- version: "2.0.3"
- flutter_test:
- dependency: "direct dev"
- description: flutter
- source: sdk
- version: "0.0.0"
- flutter_use:
- dependency: "direct main"
- description:
- path: ".."
- relative: true
- source: path
- version: "0.0.5"
- glob:
- dependency: transitive
- description:
- name: glob
- sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
- url: "https://pub.dev"
- source: hosted
- version: "2.1.3"
- leak_tracker:
- dependency: transitive
- description:
- name: leak_tracker
- sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
- url: "https://pub.dev"
- source: hosted
- version: "10.0.9"
- leak_tracker_flutter_testing:
- dependency: transitive
- description:
- name: leak_tracker_flutter_testing
- sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
- url: "https://pub.dev"
- source: hosted
- version: "3.0.9"
- leak_tracker_testing:
- dependency: transitive
- description:
- name: leak_tracker_testing
- sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
- url: "https://pub.dev"
- source: hosted
- version: "3.0.1"
- lints:
- dependency: transitive
- description:
- name: lints
- sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
- url: "https://pub.dev"
- source: hosted
- version: "2.1.1"
- logging:
- dependency: transitive
- description:
- name: logging
- sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
- url: "https://pub.dev"
- source: hosted
- version: "1.3.0"
- matcher:
- dependency: transitive
- description:
- name: matcher
- sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
- url: "https://pub.dev"
- source: hosted
- version: "0.12.17"
- material_color_utilities:
- dependency: transitive
- description:
- name: material_color_utilities
- sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
- url: "https://pub.dev"
- source: hosted
- version: "0.11.1"
- meta:
- dependency: transitive
- description:
- name: meta
- sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
- url: "https://pub.dev"
- source: hosted
- version: "1.16.0"
- mockito:
- dependency: "direct dev"
- description:
- name: mockito
- sha256: "4546eac99e8967ea91bae633d2ca7698181d008e95fa4627330cf903d573277a"
- url: "https://pub.dev"
- source: hosted
- version: "5.4.6"
- package_config:
- dependency: transitive
- description:
- name: package_config
- sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
- url: "https://pub.dev"
- source: hosted
- version: "2.2.0"
- path:
- dependency: transitive
- description:
- name: path
- sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
- url: "https://pub.dev"
- source: hosted
- version: "1.9.1"
- pub_semver:
- dependency: transitive
- description:
- name: pub_semver
- sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
- url: "https://pub.dev"
- source: hosted
- version: "2.2.0"
- sky_engine:
- dependency: transitive
- description: flutter
- source: sdk
- version: "0.0.0"
- source_gen:
- dependency: transitive
- description:
- name: source_gen
- sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
- url: "https://pub.dev"
- source: hosted
- version: "2.0.0"
- source_span:
- dependency: transitive
- description:
- name: source_span
- sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
- url: "https://pub.dev"
- source: hosted
- version: "1.10.1"
- stack_trace:
- dependency: transitive
- description:
- name: stack_trace
- sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
- url: "https://pub.dev"
- source: hosted
- version: "1.12.1"
- stream_channel:
- dependency: transitive
- description:
- name: stream_channel
- sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
- url: "https://pub.dev"
- source: hosted
- version: "2.1.4"
- string_scanner:
- dependency: transitive
- description:
- name: string_scanner
- sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
- url: "https://pub.dev"
- source: hosted
- version: "1.4.1"
- term_glyph:
- dependency: transitive
- description:
- name: term_glyph
- sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
- url: "https://pub.dev"
- source: hosted
- version: "1.2.2"
- test_api:
- dependency: transitive
- description:
- name: test_api
- sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
- url: "https://pub.dev"
- source: hosted
- version: "0.7.4"
- typed_data:
- dependency: transitive
- description:
- name: typed_data
- sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
- url: "https://pub.dev"
- source: hosted
- version: "1.4.0"
- vector_math:
- dependency: transitive
- description:
- name: vector_math
- sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
- url: "https://pub.dev"
- source: hosted
- version: "2.1.4"
- vm_service:
- dependency: transitive
- description:
- name: vm_service
- sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
- url: "https://pub.dev"
- source: hosted
- version: "15.0.0"
- watcher:
- dependency: transitive
- description:
- name: watcher
- sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
- url: "https://pub.dev"
- source: hosted
- version: "1.1.1"
- yaml:
- dependency: transitive
- description:
- name: yaml
- sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
- url: "https://pub.dev"
- source: hosted
- version: "3.1.3"
-sdks:
- dart: ">=3.7.0 <4.0.0"
- flutter: ">=3.21.0-13.0.pre.4"
diff --git a/packages/basic/example/pubspec.yaml b/packages/basic/example/pubspec.yaml
deleted file mode 100644
index 06efdc0..0000000
--- a/packages/basic/example/pubspec.yaml
+++ /dev/null
@@ -1,25 +0,0 @@
-name: example_basic
-description: A new Flutter project.
-
-publish_to: "none" # Remove this line if you wish to publish to pub.dev
-version: 1.0.0+1
-
-environment:
- sdk: ">=2.17.0 <4.0.0"
-
-dependencies:
- flutter:
- sdk: flutter
-
- flutter_hooks: ^0.21.0
- flutter_use:
- path: ../
-
-dev_dependencies:
- flutter_test:
- sdk: flutter
- flutter_lints: ^2.0.2
- mockito: ^5.3.0
-
-flutter:
- uses-material-design: true
diff --git a/packages/basic/lib/flutter_use.dart b/packages/basic/lib/flutter_use.dart
index 998b89b..133fb3d 100644
--- a/packages/basic/lib/flutter_use.dart
+++ b/packages/basic/lib/flutter_use.dart
@@ -11,8 +11,14 @@ export 'src/use_update.dart';
// Side-effects
export 'src/use_future_retry.dart';
export 'src/use_debounce.dart';
+export 'src/use_throttle.dart';
+export 'src/use_throttle_fn.dart';
+export 'src/use_scroll.dart';
+export 'src/use_scrolling.dart';
+export 'src/use_click_away.dart';
export 'src/use_error.dart';
export 'src/use_exception.dart';
+export 'src/use_copy_to_clipboard.dart';
// Lifecycles
export 'src/use_effect_once.dart';
export 'src/use_lifecycles.dart';
diff --git a/packages/basic/lib/src/use_boolean.dart b/packages/basic/lib/src/use_boolean.dart
index 29795ce..8a25856 100644
--- a/packages/basic/lib/src/use_boolean.dart
+++ b/packages/basic/lib/src/use_boolean.dart
@@ -1,7 +1,29 @@
import 'use_toggle.dart';
-/// Flutter state hook that tracks value of a bool.
-/// useBoolean is an alias for useToggle.
-ToggleState useBoolean(bool initialValue) {
- return useToggle(initialValue);
-}
+/// Flutter state hook that manages a boolean value with toggle functionality.
+///
+/// This is an alias for [useToggle] that provides a more semantic name
+/// when working specifically with boolean values.
+///
+/// [initialValue] is the starting boolean value.
+///
+/// Returns a [ToggleState] object that provides access to the current value
+/// and a toggle method.
+///
+/// Example:
+/// ```dart
+/// final boolState = useBoolean(false);
+///
+/// print(boolState.value); // false
+///
+/// // Toggle the value
+/// boolState.toggle();
+/// print(boolState.value); // true
+///
+/// // Set to specific value
+/// boolState.toggle(false);
+/// print(boolState.value); // false
+/// ```
+///
+/// See also: [useToggle]
+ToggleState useBoolean(bool initialValue) => useToggle(initialValue);
diff --git a/packages/basic/lib/src/use_builds_count.dart b/packages/basic/lib/src/use_builds_count.dart
index 00593b3..18ed3f5 100644
--- a/packages/basic/lib/src/use_builds_count.dart
+++ b/packages/basic/lib/src/use_builds_count.dart
@@ -1,6 +1,4 @@
import 'package:flutter_hooks/flutter_hooks.dart';
/// Tracks component's builds count including the first build.
-int useBuildsCount() {
- return ++useRef(0).value;
-}
+int useBuildsCount() => ++useRef(0).value;
diff --git a/packages/basic/lib/src/use_click_away.dart b/packages/basic/lib/src/use_click_away.dart
new file mode 100644
index 0000000..b912fc8
--- /dev/null
+++ b/packages/basic/lib/src/use_click_away.dart
@@ -0,0 +1,104 @@
+import 'package:flutter/gestures.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+
+/// State object returned by [useClickAway].
+class ClickAwayState {
+ /// Creates a [ClickAwayState].
+ const ClickAwayState({
+ required this.ref,
+ });
+
+ /// A global key that should be attached to the target widget.
+ ///
+ /// Clicks outside this widget will trigger the callback.
+ final GlobalKey ref;
+}
+
+/// Detects clicks outside a target widget and calls a callback function.
+///
+/// This hook is useful for implementing behaviors like closing dropdowns,
+/// modals, or context menus when clicking outside them.
+///
+/// Returns a [ClickAwayState] that contains:
+/// - [ClickAwayState.ref]: A [GlobalKey] to attach to the target widget
+///
+/// Example:
+/// ```dart
+/// Widget build(BuildContext context) {
+/// final showDropdown = useState(false);
+/// final clickAway = useClickAway(() {
+/// showDropdown.value = false;
+/// });
+///
+/// return Column(
+/// children: [
+/// ElevatedButton(
+/// onPressed: () => showDropdown.value = !showDropdown.value,
+/// child: Text('Toggle Dropdown'),
+/// ),
+/// if (showDropdown.value)
+/// Container(
+/// key: clickAway.ref,
+/// width: 200,
+/// height: 100,
+/// color: Colors.blue,
+/// child: Center(
+/// child: Text('Click outside to close'),
+/// ),
+/// ),
+/// ],
+/// );
+/// }
+/// ```
+ClickAwayState useClickAway(VoidCallback onClickAway) {
+ final ref = useMemoized(GlobalKey.new, []);
+ final callbackRef = useRef(onClickAway);
+
+ // Update the callback reference whenever it changes
+ callbackRef.value = onClickAway;
+
+ useEffect(
+ () {
+ void handlePointerEvent(PointerEvent event) {
+ // Only handle pointer down events
+ if (event is! PointerDownEvent) {
+ return;
+ }
+
+ final context = ref.currentContext;
+ if (context == null) {
+ return;
+ }
+
+ final renderBox = context.findRenderObject() as RenderBox?;
+ if (renderBox == null) {
+ return;
+ }
+
+ final offset = renderBox.globalToLocal(event.position);
+ final size = renderBox.size;
+
+ // Check if the click is outside the target widget
+ if (offset.dx < 0 ||
+ offset.dy < 0 ||
+ offset.dx > size.width ||
+ offset.dy > size.height) {
+ callbackRef.value();
+ }
+ }
+
+ // Add global pointer listener
+ GestureBinding.instance.pointerRouter.addGlobalRoute(handlePointerEvent);
+
+ return () {
+ // Remove global pointer listener
+ GestureBinding.instance.pointerRouter
+ .removeGlobalRoute(handlePointerEvent);
+ };
+ },
+ [],
+ );
+
+ return ClickAwayState(ref: ref);
+}
diff --git a/packages/basic/lib/src/use_copy_to_clipboard.dart b/packages/basic/lib/src/use_copy_to_clipboard.dart
new file mode 100644
index 0000000..c67a3b3
--- /dev/null
+++ b/packages/basic/lib/src/use_copy_to_clipboard.dart
@@ -0,0 +1,75 @@
+import 'package:flutter/services.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+
+/// State object returned by [useCopyToClipboard].
+class CopyToClipboardState {
+ /// Creates a [CopyToClipboardState].
+ const CopyToClipboardState({
+ required this.copied,
+ required this.error,
+ required this.copy,
+ });
+
+ /// The last successfully copied text, or null if nothing was copied yet.
+ final String? copied;
+
+ /// The error that occurred during the last copy operation, if any.
+ final Object? error;
+
+ /// Copies the given text to the clipboard.
+ ///
+ /// Returns a [Future] that completes when the operation is done.
+ /// If successful, [copied] will be updated with the text.
+ /// If failed, [error] will be updated with the error.
+ final Future Function(String text) copy;
+}
+
+/// Provides a way to copy text to the clipboard.
+///
+/// Returns a [CopyToClipboardState] that contains:
+/// - [CopyToClipboardState.copy]: A function to copy text to clipboard
+/// - [CopyToClipboardState.copied]: The last successfully copied text
+/// - [CopyToClipboardState.error]: Any error that occurred during copy
+///
+/// Example:
+/// ```dart
+/// Widget build(BuildContext context) {
+/// final clipboard = useCopyToClipboard();
+///
+/// return Column(
+/// children: [
+/// if (clipboard.error != null)
+/// Text('Error: ${clipboard.error}'),
+/// if (clipboard.copied != null)
+/// Text('Copied: ${clipboard.copied}'),
+/// ElevatedButton(
+/// onPressed: () => clipboard.copy('Hello, World!'),
+/// child: Text('Copy to clipboard'),
+/// ),
+/// ],
+/// );
+/// }
+/// ```
+CopyToClipboardState useCopyToClipboard() {
+ final copied = useState(null);
+ final error = useState(null);
+
+ final copy = useCallback Function(String)>(
+ (text) async {
+ try {
+ await Clipboard.setData(ClipboardData(text: text));
+ copied.value = text;
+ error.value = null;
+ } on Exception catch (e) {
+ error.value = e;
+ }
+ },
+ [],
+ );
+
+ return CopyToClipboardState(
+ copied: copied.value,
+ error: error.value,
+ copy: copy,
+ );
+}
diff --git a/packages/basic/lib/src/use_counter.dart b/packages/basic/lib/src/use_counter.dart
index a909705..d346d9d 100644
--- a/packages/basic/lib/src/use_counter.dart
+++ b/packages/basic/lib/src/use_counter.dart
@@ -3,103 +3,156 @@ import 'dart:math' as math;
import 'package:flutter_hooks/flutter_hooks.dart';
/// Flutter state hook that tracks a numeric value.
+///
+/// Creates a counter with increment, decrement, set, and reset operations.
+/// The counter value can be constrained between optional [min] and [max] values.
+///
+/// [initialValue] is the starting value for the counter.
+/// [min] is the optional minimum value the counter can reach.
+/// [max] is the optional maximum value the counter can reach.
+///
+/// Returns a [CounterActions] object that provides methods to manipulate the counter.
+///
+/// Throws [ArgumentError] if [initialValue] is outside the [min]/[max] bounds.
+///
+/// Example:
+/// ```dart
+/// final counter = useCounter(0, min: 0, max: 10);
+///
+/// // Increment by 1
+/// counter.inc();
+///
+/// // Increment by specific amount
+/// counter.inc(5);
+///
+/// // Set to specific value
+/// counter.setter(7);
+///
+/// // Get current value
+/// print(counter.value); // 7
+/// ```
+///
/// useNumber is an alias for useCounter.
CounterActions useCounter(int initialValue, {int? min, int? max}) {
if (min != null && initialValue < min) {
throw ArgumentError(
- "The initialValue must be equal to or greater than min value.");
+ 'The initialValue must be equal to or greater than min value.',
+ );
}
if (max != null && initialValue > max) {
throw ArgumentError(
- "The initialValue must be equal to or less than max value.");
+ 'The initialValue must be equal to or less than max value.',
+ );
}
final state = useState(initialValue);
- final get = useCallback(() {
- return state.value;
- }, const []);
+ final get = useCallback(
+ () => state.value,
+ const [],
+ );
- final inc = useCallback(([value]) {
- if (max == null) {
- if (value == null) {
- state.value++;
+ final inc = useCallback(
+ ([value]) {
+ if (max == null) {
+ if (value == null) {
+ state.value++;
+ } else {
+ state.value += value;
+ }
} else {
- state.value += value;
+ if (value == null) {
+ state.value = math.min(state.value + 1, max);
+ } else {
+ state.value = math.min(state.value + value, max);
+ }
}
- } else {
- if (value == null) {
- state.value = math.min(state.value + 1, max);
+ },
+ const [],
+ );
+
+ final dec = useCallback(
+ ([value]) {
+ if (min == null) {
+ if (value == null) {
+ state.value--;
+ } else {
+ state.value -= value;
+ }
} else {
- state.value = math.min(state.value + value, max);
+ if (value == null) {
+ state.value = math.max(state.value - 1, min);
+ } else {
+ state.value = math.max(state.value - value, min);
+ }
}
- }
- }, const []);
+ },
+ const [],
+ );
- final dec = useCallback(([value]) {
- if (min == null) {
- if (value == null) {
- state.value--;
+ final set = useCallback(
+ (value) {
+ if (max == null) {
+ state.value = value;
} else {
- state.value -= value;
+ state.value = math.min(value, max);
}
- } else {
- if (value == null) {
- state.value = math.max(state.value - 1, min);
+ },
+ const [],
+ );
+
+ final reset = useCallback(
+ ([value]) {
+ if (value != null) {
+ initialValue = value;
+
+ if (min != null) {
+ initialValue = math.max(value, min);
+ }
+
+ if (max != null) {
+ initialValue = math.min(initialValue, max);
+ }
+
+ state.value = initialValue;
} else {
- state.value = math.max(state.value - value, min);
- }
- }
- }, const []);
-
- final set = useCallback((value) {
- if (max == null) {
- state.value = value;
- } else {
- state.value = math.min(value, max);
- }
- }, const []);
-
- final reset = useCallback(([value]) {
- if (value != null) {
- initialValue = value;
-
- if (min != null) {
- initialValue = math.max(value, min);
+ state.value = initialValue;
}
+ },
+ const [],
+ );
- if (max != null) {
- initialValue = math.min(initialValue, max);
- }
+ final minValue = useCallback(
+ () => min,
+ const [],
+ );
- state.value = initialValue;
- } else {
- state.value = initialValue;
- }
- }, const []);
-
- final minValue = useCallback(() {
- return min;
- }, const []);
-
- final maxValue = useCallback(() {
- return max;
- }, const []);
-
- final action = useRef(CounterActions(
- get,
- inc,
- dec,
- set,
- reset,
- minValue,
- maxValue,
- ));
+ final maxValue = useCallback(
+ () => max,
+ const [],
+ );
+
+ final action = useRef(
+ CounterActions(
+ get,
+ inc,
+ dec,
+ set,
+ reset,
+ minValue,
+ maxValue,
+ ),
+ );
return action.value;
}
+/// Actions for manipulating a counter value.
+///
+/// This class provides methods to increment, decrement, set, and reset
+/// a counter value while respecting optional min/max constraints.
class CounterActions {
+ /// Creates a [CounterActions] instance with the provided functions.
CounterActions(
this.getter,
this.inc,
@@ -110,14 +163,44 @@ class CounterActions {
this._max,
);
+ /// Increments the counter value.
+ ///
+ /// If no [value] is provided, increments by 1.
+ /// If [value] is provided, increments by that amount.
+ /// Respects the maximum constraint if set.
final void Function([int?]) inc;
+
+ /// Decrements the counter value.
+ ///
+ /// If no [value] is provided, decrements by 1.
+ /// If [value] is provided, decrements by that amount.
+ /// Respects the minimum constraint if set.
final void Function([int?]) dec;
+
+ /// Function to get the current counter value.
final int Function() getter;
+
+ /// The current value of the counter.
int get value => getter();
+
+ /// Sets the counter to a specific value.
+ ///
+ /// Respects the maximum constraint if set.
final void Function(int) setter;
+
+ /// Resets the counter to its initial value.
+ ///
+ /// If [value] is provided, resets to that value and updates the initial value.
+ /// The new initial value will be clamped to min/max constraints if they exist.
final void Function([int?]) reset;
+
final int? Function() _min;
+
+ /// The minimum value constraint, or null if no constraint is set.
int? get min => _min();
+
final int? Function() _max;
+
+ /// The maximum value constraint, or null if no constraint is set.
int? get max => _max();
}
diff --git a/packages/basic/lib/src/use_custom_compare_effect.dart b/packages/basic/lib/src/use_custom_compare_effect.dart
index a5cdf9b..406df38 100644
--- a/packages/basic/lib/src/use_custom_compare_effect.dart
+++ b/packages/basic/lib/src/use_custom_compare_effect.dart
@@ -15,5 +15,27 @@ void useCustomCompareEffect(
useEffect(effect, ref.value);
}
+/// A function that compares two lists of dependencies for equality.
+///
+/// This function receives the previous and next dependency lists and should
+/// return true if they are considered equal, false otherwise. This allows
+/// for custom comparison logic beyond simple reference equality.
+///
+/// Example:
+/// ```dart
+/// // Deep equality comparison for lists
+/// bool deepEquals(List? prev, List? next) {
+/// if (prev == null && next == null) return true;
+/// if (prev == null || next == null) return false;
+/// if (prev.length != next.length) return false;
+///
+/// for (int i = 0; i < prev.length; i++) {
+/// if (!deepEqual(prev[i], next[i])) return false;
+/// }
+/// return true;
+/// }
+/// ```
typedef EqualFunction = bool Function(
- List? prevKeys, List? nextKeys);
+ List? prevKeys,
+ List? nextKeys,
+);
diff --git a/packages/basic/lib/src/use_debounce.dart b/packages/basic/lib/src/use_debounce.dart
index cbb3704..6bba49b 100644
--- a/packages/basic/lib/src/use_debounce.dart
+++ b/packages/basic/lib/src/use_debounce.dart
@@ -2,11 +2,35 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_use/flutter_use.dart';
-/// Flutter hook that delays invoking a function until after wait milliseconds
-/// have elapsed since the last time the debounced function was invoked.
-/// The third argument is the array of values that the debounce depends on,
-/// in the same manner as useEffect. The debounce timeout will start when one
-/// of the values changes.
+/// Flutter hook that debounces a function call.
+///
+/// Delays the execution of a function until after the specified delay
+/// has elapsed since the last time the function was scheduled to be called.
+///
+/// [fn] is the function to be debounced.
+/// [delay] is the duration to wait before executing the function.
+/// [keys] is an optional list of dependencies. The debounce timer resets
+/// whenever any of these dependencies change, similar to useEffect.
+///
+/// Example:
+/// ```dart
+/// final searchController = TextEditingController();
+///
+/// useDebounce(() {
+/// // This will only execute 500ms after the user stops typing
+/// performSearch(searchController.text);
+/// }, Duration(milliseconds: 500), [searchController.text]);
+///
+/// // Usage with a search field
+/// TextField(
+/// controller: searchController,
+/// onChanged: (value) {
+/// // The search will be debounced automatically
+/// },
+/// )
+/// ```
+///
+/// See also: [useTimeoutFn] for the underlying timeout implementation.
void useDebounce(VoidCallback fn, Duration delay, [List? keys]) {
final timeout = useTimeoutFn(fn, delay);
useEffect(() => timeout.reset, keys);
diff --git a/packages/basic/lib/src/use_default.dart b/packages/basic/lib/src/use_default.dart
index 73ba602..c4cbe52 100644
--- a/packages/basic/lib/src/use_default.dart
+++ b/packages/basic/lib/src/use_default.dart
@@ -1,30 +1,63 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
-/// Flutter state hook that returns the default value when state is null.
+/// Flutter state hook that manages a value with automatic fallback to a default.
+///
+/// When the state value is set to null, it automatically falls back to the [defaultValue].
+///
+/// [defaultValue] is the value to use when the state is set to null.
+/// [initialValue] is the starting value for the state.
+///
+/// Returns a [DefaultState] object that provides access to the current value
+/// and a setter that handles null values automatically.
+///
+/// Example:
+/// ```dart
+/// final defaultState = useDefault('Hello', 'World');
+///
+/// print(defaultState.value); // 'World'
+///
+/// defaultState.value = 'Flutter';
+/// print(defaultState.value); // 'Flutter'
+///
+/// defaultState.value = null; // Falls back to default
+/// print(defaultState.value); // 'Hello'
+/// ```
DefaultState useDefault(T defaultValue, T initialValue) {
final value = useState(initialValue);
- final getter = useCallback(() {
- return value.value;
- }, const []);
+ final getter = useCallback(
+ () => value.value,
+ const [],
+ );
- final setter = useCallback((newValue) {
- value.value = newValue ??= defaultValue;
- }, const []);
+ final setter = useCallback(
+ (newValue) {
+ value.value = newValue ??= defaultValue;
+ },
+ const [],
+ );
final state = useRef(DefaultState(getter, setter));
return state.value;
}
+/// State manager that provides automatic fallback to a default value.
+///
+/// This class encapsulates a value that automatically falls back to a default
+/// when set to null. It provides both getter and setter access to the underlying value.
@immutable
class DefaultState {
+ /// Creates a [DefaultState] with the provided getter and setter functions.
const DefaultState(this._getter, this._setter);
final T Function() _getter;
final void Function(T?) _setter;
+ /// The current value. Never null due to automatic fallback behavior.
T get value => _getter();
+
+ /// Sets the value. If [newValue] is null, the state falls back to the default value.
set value(T? newValue) => _setter(newValue);
}
diff --git a/packages/basic/lib/src/use_effect_once.dart b/packages/basic/lib/src/use_effect_once.dart
index 036db1f..428c48b 100644
--- a/packages/basic/lib/src/use_effect_once.dart
+++ b/packages/basic/lib/src/use_effect_once.dart
@@ -2,6 +2,4 @@ import 'package:flutter_hooks/flutter_hooks.dart';
/// A modified [useEffect](ref link) hook that only runs once.
/// [ref link](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html)
-void useEffectOnce(Dispose? Function() effect) {
- return useEffect(effect, const []);
-}
+void useEffectOnce(Dispose? Function() effect) => useEffect(effect, const []);
diff --git a/packages/basic/lib/src/use_error.dart b/packages/basic/lib/src/use_error.dart
index 5124757..d727f65 100644
--- a/packages/basic/lib/src/use_error.dart
+++ b/packages/basic/lib/src/use_error.dart
@@ -1,27 +1,67 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
-/// Returns an error dispatcher.
+/// Flutter state hook for managing error states.
+///
+/// Provides a mechanism to store and dispatch errors.
+///
+/// Returns an [ErrorState] object that can dispatch errors and retrieve
+/// the current error value.
+///
+/// Example:
+/// ```dart
+/// final errorState = useError();
+///
+/// // Dispatch an error
+/// try {
+/// // Some operation that might fail
+/// throw ArgumentError('Invalid input');
+/// } catch (e) {
+/// if (e is Error) {
+/// errorState.dispatch(e);
+/// }
+/// }
+///
+/// // Check for errors
+/// if (errorState.value != null) {
+/// print('Error occurred: ${errorState.value}');
+/// }
+/// ```
ErrorState useError() {
final error = useState(null);
- final dispatcher = useCallback((e) {
- error.value = e;
- }, const []);
+ final dispatcher = useCallback(
+ (e) {
+ error.value = e;
+ },
+ const [],
+ );
- final getter = useCallback(() {
- return error.value;
- }, const []);
+ final getter = useCallback(
+ () => error.value,
+ const [],
+ );
final state = useRef(ErrorState(dispatcher, getter));
return state.value;
}
+/// State manager for handling errors.
+///
+/// This class provides methods to dispatch errors and retrieve the current error state.
+/// It maintains the latest error that was dispatched and provides access to it.
@immutable
class ErrorState {
+ /// Creates an [ErrorState] with the provided dispatcher and getter functions.
const ErrorState(this._dispatcher, this._getter);
+
final Error? Function() _getter;
final void Function(Error e) _dispatcher;
+ /// Dispatches an error to be stored in the state.
+ ///
+ /// [e] is the error to store. This will replace any previously stored error.
void dispatch(Error e) => _dispatcher(e);
+
+ /// The current error value, or null if no error has been dispatched.
Error? get value => _getter();
}
diff --git a/packages/basic/lib/src/use_exception.dart b/packages/basic/lib/src/use_exception.dart
index 13b9f78..f8aa095 100644
--- a/packages/basic/lib/src/use_exception.dart
+++ b/packages/basic/lib/src/use_exception.dart
@@ -1,27 +1,67 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
-/// Returns an exception dispatcher.
+/// Flutter state hook for managing exception states.
+///
+/// Provides a mechanism to store and dispatch exceptions.
+///
+/// Returns an [ExceptionState] object that can dispatch exceptions and retrieve
+/// the current exception value.
+///
+/// Example:
+/// ```dart
+/// final exceptionState = useException();
+///
+/// // Dispatch an exception
+/// try {
+/// // Some operation that might fail
+/// throw Exception('Network error');
+/// } catch (e) {
+/// if (e is Exception) {
+/// exceptionState.dispatch(e);
+/// }
+/// }
+///
+/// // Check for exceptions
+/// if (exceptionState.value != null) {
+/// print('Exception occurred: ${exceptionState.value}');
+/// }
+/// ```
ExceptionState useException() {
final exception = useState(null);
- final dispatcher = useCallback((e) {
- exception.value = e;
- }, const []);
+ final dispatcher = useCallback(
+ (e) {
+ exception.value = e;
+ },
+ const [],
+ );
- final getter = useCallback(() {
- return exception.value;
- }, const []);
+ final getter = useCallback(
+ () => exception.value,
+ const [],
+ );
final state = useRef(ExceptionState(dispatcher, getter));
return state.value;
}
+/// State manager for handling exceptions.
+///
+/// This class provides methods to dispatch exceptions and retrieve the current exception state.
+/// It maintains the latest exception that was dispatched and provides access to it.
@immutable
class ExceptionState {
+ /// Creates an [ExceptionState] with the provided dispatcher and getter functions.
const ExceptionState(this._dispatcher, this._getter);
+
final Exception? Function() _getter;
final void Function(Exception e) _dispatcher;
+ /// Dispatches an exception to be stored in the state.
+ ///
+ /// [e] is the exception to store. This will replace any previously stored exception.
void dispatch(Exception e) => _dispatcher(e);
+
+ /// The current exception value, or null if no exception has been dispatched.
Exception? get value => _getter();
}
diff --git a/packages/basic/lib/src/use_future_retry.dart b/packages/basic/lib/src/use_future_retry.dart
index 7977331..046f565 100644
--- a/packages/basic/lib/src/use_future_retry.dart
+++ b/packages/basic/lib/src/use_future_retry.dart
@@ -1,8 +1,35 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
-/// Uses useFuture with an additional retry method to easily retry/refresh
-/// the future function.
+/// Flutter state hook that manages a Future with retry functionality.
+///
+/// Extends the standard `useFuture` hook by adding a retry mechanism
+/// that allows re-execution of the future.
+///
+/// [future] is the Future to execute. Can be null.
+/// [initialData] is the initial data to use before the future completes.
+/// [preserveState] determines whether to keep the previous data while retrying.
+///
+/// Returns a [FutureState] object that provides access to the current
+/// [AsyncSnapshot] and a retry method.
+///
+/// Example:
+/// ```dart
+/// final futureState = useFutureRetry(
+/// fetchUserData(userId),
+/// initialData: null,
+/// preserveState: true,
+/// );
+///
+/// // Access the current state
+/// if (futureState.snapshot.hasData) {
+/// print('Data: ${futureState.snapshot.data}');
+/// } else if (futureState.snapshot.hasError) {
+/// print('Error: ${futureState.snapshot.error}');
+/// // Retry on error
+/// futureState.retry();
+/// }
+/// ```
FutureState useFutureRetry(
Future? future, {
T? initialData,
@@ -18,25 +45,46 @@ FutureState useFutureRetry(
);
snapshotRef.value = snapshot;
- final snapshotCallback = useCallback>(() {
- return snapshotRef.value;
- }, const []);
+ final snapshotCallback = useCallback>(
+ () => snapshotRef.value,
+ const [],
+ );
- final retry = useCallback(() {
- attempt.value++;
- }, [future, initialData, preserveState]);
+ final retry = useCallback(
+ () {
+ attempt.value++;
+ },
+ [future, initialData, preserveState],
+ );
final state = useRef(FutureState(snapshotCallback, retry));
return state.value;
}
+/// Callback type for getting the current AsyncSnapshot.
typedef SnapshotCallback = AsyncSnapshot Function();
+/// State manager for a Future with retry functionality.
+///
+/// This class provides access to the current [AsyncSnapshot] state of a Future
+/// and a method to retry the Future execution.
@immutable
class FutureState {
+ /// Creates a [FutureState] with the provided snapshot callback and retry function.
const FutureState(this._snapshot, this.retry);
+
final SnapshotCallback _snapshot;
+
+ /// The current AsyncSnapshot representing the state of the Future.
+ ///
+ /// This snapshot contains information about whether the Future is loading,
+ /// has completed with data, or has completed with an error.
AsyncSnapshot get snapshot => _snapshot();
+
+ /// Retries the Future execution.
+ ///
+ /// Calling this method will cause the Future to be re-executed,
+ /// which is useful for handling failures or refreshing data.
final VoidCallback retry;
}
diff --git a/packages/basic/lib/src/use_interval.dart b/packages/basic/lib/src/use_interval.dart
index 5779888..cea9ab1 100644
--- a/packages/basic/lib/src/use_interval.dart
+++ b/packages/basic/lib/src/use_interval.dart
@@ -18,12 +18,16 @@ void useInterval(
});
// ignore: body_might_complete_normally_nullable
- useEffect(() {
- if (delay != null) {
- final timer = Timer.periodic(delay, (time) {
- savedCallback.value();
- });
- return () => timer.cancel();
- }
- }, [delay]);
+ useEffect(
+ () {
+ if (delay != null) {
+ final timer = Timer.periodic(delay, (time) {
+ savedCallback.value();
+ });
+ return timer.cancel;
+ }
+ return null;
+ },
+ [delay],
+ );
}
diff --git a/packages/basic/lib/src/use_lifecycles.dart b/packages/basic/lib/src/use_lifecycles.dart
index 737dbc7..5f84e86 100644
--- a/packages/basic/lib/src/use_lifecycles.dart
+++ b/packages/basic/lib/src/use_lifecycles.dart
@@ -1,15 +1,17 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
-/// Flutter lifecycle hook that call mount and unmount callbacks, when component
-/// is mounted and un-mounted, respectively.
-/// If you want to use hook that app lifecycles, recommended use to
-/// flutter_hooks v0.18.1+ [useAppLifecycleState](ref link1) or [useOnAppLifecycleStateChange](ref link2)
+/// Flutter lifecycle hook that calls mount and unmount callbacks when component
+/// is mounted and unmounted, respectively.
+/// For app lifecycle hooks, see flutter_hooks v0.18.1+ [useAppLifecycleState](ref link1) or [useOnAppLifecycleStateChange](ref link2)
/// [ref link1](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useAppLifecycleState.html)
/// [ref link2](https://pub.dartlang.org/documentation/flutter_hooks/latest/flutter_hooks/useOnAppLifecycleStateChange.html)
void useLifecycles({VoidCallback? mount, VoidCallback? unmount}) {
- useEffect(() {
- mount?.call();
- return () => unmount?.call();
- }, const []);
+ useEffect(
+ () {
+ mount?.call();
+ return () => unmount?.call();
+ },
+ const [],
+ );
}
diff --git a/packages/basic/lib/src/use_list.dart b/packages/basic/lib/src/use_list.dart
index 36769b6..9c059db 100644
--- a/packages/basic/lib/src/use_list.dart
+++ b/packages/basic/lib/src/use_list.dart
@@ -3,223 +3,326 @@ import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
-/// Tracks an array and provides methods to modify it. To cause component
-/// re-build you have to use these methods instead of direct interaction
-/// with array - it won't cause re-build.
-/// We can ensure that actions object and actions itself will not mutate or
-/// change between builds, so there is no need to add it to useEffect
-/// dependencies and safe to pass them down to children.
+/// Flutter state hook that manages a list with reactive updates.
+///
+/// Provides methods to modify a list. The component re-builds when the list
+/// changes through these methods. Direct modification of the list does not
+/// trigger re-builds.
+///
+/// The actions object and its methods remain stable across builds.
+///
+/// [initialList] is the starting list value.
+///
+/// Returns a [ListAction] object that provides access to the current list
+/// and all modification methods.
+///
+/// Example:
+/// ```dart
+/// final listActions = useList([1, 2, 3]);
+///
+/// // Access the current list
+/// print(listActions.list); // [1, 2, 3]
+///
+/// // Add elements
+/// listActions.add(4);
+/// listActions.addAll([5, 6]);
+///
+/// // Remove elements
+/// listActions.remove(2);
+/// listActions.removeAt(0);
+///
+/// // Modify elements
+/// listActions.first(10);
+/// listActions.last(20);
+///
+/// // Reset to initial value
+/// listActions.reset();
+/// ```
ListAction useList(List initialList) {
final list = useState(initialList);
- final value = useCallback Function()>(() {
- return list.value;
- }, const []);
-
- final first = useCallback((E value) {
- final newList = [...list.value];
- newList.first = value;
- list.value = newList;
- }, const []);
-
- final last = useCallback((E value) {
- final newList = [...list.value];
- newList.last = value;
- list.value = newList;
- }, const []);
-
- final length = useCallback(() {
- return list.value.length;
- }, const []);
-
- final add = useCallback((E value) {
- final newList = [...list.value];
- newList.add(value);
- list.value = newList;
- }, const []);
-
- final addAll =
- useCallback iterable)>((Iterable iterable) {
- final newList = [...list.value];
- newList.addAll(iterable);
- list.value = newList;
- }, const []);
-
- final sort = useCallback((
- [int Function(E, E)? compare]) {
- final newList = [...list.value];
- newList.sort(compare);
- list.value = newList;
- }, const []);
-
- final shuffle =
- useCallback(([Random? random]) {
- final newList = [...list.value];
- newList.shuffle(random);
- list.value = newList;
- }, const []);
-
- final indexOf = useCallback((E element,
- [int start = 0]) {
- return list.value.indexOf(element, start);
- }, const []);
+ final value = useCallback Function()>(
+ () => list.value,
+ const [],
+ );
+
+ final first = useCallback(
+ (value) {
+ final newList = [...list.value];
+ newList.first = value;
+ list.value = newList;
+ },
+ const [],
+ );
+
+ final last = useCallback(
+ (value) {
+ final newList = [...list.value];
+ newList.last = value;
+ list.value = newList;
+ },
+ const [],
+ );
+
+ final length = useCallback(
+ () => list.value.length,
+ const [],
+ );
+
+ final add = useCallback(
+ (value) {
+ final newList = [...list.value];
+ newList.add(value);
+ list.value = newList;
+ },
+ const [],
+ );
+
+ final addAll = useCallback iterable)>(
+ (iterable) {
+ final newList = [...list.value];
+ newList.addAll(iterable);
+ list.value = newList;
+ },
+ const [],
+ );
+
+ final sort = useCallback(
+ ([
+ int Function(E, E)? compare,
+ ]) {
+ final newList = [...list.value];
+ newList.sort(compare);
+ list.value = newList;
+ },
+ const [],
+ );
+
+ final shuffle = useCallback(
+ ([random]) {
+ final newList = [...list.value];
+ newList.shuffle(random);
+ list.value = newList;
+ },
+ const [],
+ );
+
+ final indexOf = useCallback(
+ (
+ element, [
+ start = 0,
+ ]) =>
+ list.value.indexOf(element, start),
+ const [],
+ );
final indexWhere =
useCallback(
- (bool Function(E element) test, [int start = 0]) {
- return list.value.indexWhere(test, start);
- }, const []);
+ (bool Function(E element) test, [start = 0]) =>
+ list.value.indexWhere(test, start),
+ const [],
+ );
final lastIndexWhere =
useCallback(
- (bool Function(E element) test, [int? start]) {
- return list.value.lastIndexWhere(test, start);
- }, const []);
+ (bool Function(E element) test, [start]) =>
+ list.value.lastIndexWhere(test, start),
+ const [],
+ );
final lastIndexOf = useCallback(
- (E element, [int? start]) {
- return list.value.lastIndexOf(element, start);
- }, const []);
-
- final clear = useCallback(() {
- final newList = [...list.value];
- newList.clear();
- list.value = newList;
- }, const []);
-
- final insert =
- useCallback((int index, E element) {
- final newList = [...list.value];
- newList.insert(index, element);
- list.value = newList;
- }, const []);
+ (element, [start]) => list.value.lastIndexOf(element, start),
+ const [],
+ );
+
+ final clear = useCallback(
+ () {
+ final newList = [...list.value];
+ newList.clear();
+ list.value = newList;
+ },
+ const [],
+ );
+
+ final insert = useCallback(
+ (index, element) {
+ final newList = [...list.value];
+ newList.insert(index, element);
+ list.value = newList;
+ },
+ const [],
+ );
final insertAll = useCallback iterable)>(
- (int index, Iterable iterable) {
- final newList = [...list.value];
- newList.insertAll(index, iterable);
- list.value = newList;
- }, const []);
+ (index, iterable) {
+ final newList = [...list.value];
+ newList.insertAll(index, iterable);
+ list.value = newList;
+ },
+ const [],
+ );
final setAll = useCallback iterable)>(
- (int index, Iterable iterable) {
- final newList = [...list.value];
- newList.setAll(index, iterable);
- list.value = newList;
- }, const []);
-
- final remove = useCallback((Object? value) {
- final newList = [...list.value];
- final removed = newList.remove(value);
- list.value = newList;
- return removed;
- }, const []);
-
- final removeAt = useCallback((int index) {
- final newList = [...list.value];
- final removed = newList.removeAt(index);
- list.value = newList;
- return removed;
- }, const []);
-
- final removeLast = useCallback(() {
- final newList = [...list.value];
- final removed = newList.removeLast();
- list.value = newList;
- return removed;
- }, const []);
+ (index, iterable) {
+ final newList = [...list.value];
+ newList.setAll(index, iterable);
+ list.value = newList;
+ },
+ const [],
+ );
+
+ final remove = useCallback(
+ (value) {
+ final newList = [...list.value];
+ final removed = newList.remove(value);
+ list.value = newList;
+ return removed;
+ },
+ const [],
+ );
+
+ final removeAt = useCallback(
+ (index) {
+ final newList = [...list.value];
+ final removed = newList.removeAt(index);
+ list.value = newList;
+ return removed;
+ },
+ const [],
+ );
+
+ final removeLast = useCallback(
+ () {
+ final newList = [...list.value];
+ final removed = newList.removeLast();
+ list.value = newList;
+ return removed;
+ },
+ const [],
+ );
final removeWhere = useCallback(
- (bool Function(E element) test) {
- final newList = [...list.value];
- newList.removeWhere(test);
- list.value = newList;
- }, const []);
+ (bool Function(E element) test) {
+ final newList = [...list.value];
+ newList.removeWhere(test);
+ list.value = newList;
+ },
+ const [],
+ );
final sublist = useCallback Function(int start, [int? end])>(
- (int start, [int? end]) {
- return list.value.sublist(start, end);
- }, const []);
+ (start, [end]) => list.value.sublist(start, end),
+ const [],
+ );
final getRange = useCallback Function(int start, int end)>(
- (int start, int end) {
- return list.value.getRange(start, end);
- }, const []);
+ (start, end) => list.value.getRange(start, end),
+ const [],
+ );
final setRange = useCallback<
- void Function(int start, int end, Iterable iterable,
- [int skipCount])>((int start, int end, Iterable iterable,
- [int skipCount = 0]) {
- final newList = [...list.value];
- newList.setRange(start, end, iterable, skipCount);
- list.value = newList;
- }, const []);
-
- final removeRange =
- useCallback((int start, int end) {
- final newList = [...list.value];
- newList.removeRange(start, end);
- list.value = newList;
- }, const []);
+ void Function(
+ int start,
+ int end,
+ Iterable iterable, [
+ int skipCount,
+ ])>(
+ (
+ start,
+ end,
+ iterable, [
+ skipCount = 0,
+ ]) {
+ final newList = [...list.value];
+ newList.setRange(start, end, iterable, skipCount);
+ list.value = newList;
+ },
+ const [],
+ );
+
+ final removeRange = useCallback(
+ (start, end) {
+ final newList = [...list.value];
+ newList.removeRange(start, end);
+ list.value = newList;
+ },
+ const [],
+ );
final fillRange =
useCallback(
- (int start, int end, [E? fillValue]) {
- final newList = [...list.value];
- newList.fillRange(start, end, fillValue);
- list.value = newList;
- }, const []);
+ (start, end, [fillValue]) {
+ final newList = [...list.value];
+ newList.fillRange(start, end, fillValue);
+ list.value = newList;
+ },
+ const [],
+ );
final replaceRange =
useCallback iterable)>(
- (int start, int end, Iterable iterable) {
- final newList = [...list.value];
- newList.replaceRange(start, end, iterable);
- list.value = newList;
- }, const []);
-
- final asMap = useCallback Function()>(() {
- return list.value.asMap();
- }, const []);
-
- final reset = useCallback(() {
- list.value = initialList;
- }, const []);
-
- final state = useRef(ListAction(
- value,
- first,
- last,
- length,
- add,
- addAll,
- sort,
- shuffle,
- indexOf,
- indexWhere,
- lastIndexWhere,
- lastIndexOf,
- clear,
- insert,
- insertAll,
- setAll,
- remove,
- removeAt,
- removeLast,
- removeWhere,
- sublist,
- getRange,
- setRange,
- removeRange,
- fillRange,
- replaceRange,
- reset,
- asMap,
- ));
+ (start, end, iterable) {
+ final newList = [...list.value];
+ newList.replaceRange(start, end, iterable);
+ list.value = newList;
+ },
+ const [],
+ );
+
+ final asMap = useCallback Function()>(
+ () => list.value.asMap(),
+ const [],
+ );
+
+ final reset = useCallback(
+ () {
+ list.value = initialList;
+ },
+ const [],
+ );
+
+ final state = useRef(
+ ListAction(
+ value,
+ first,
+ last,
+ length,
+ add,
+ addAll,
+ sort,
+ shuffle,
+ indexOf,
+ indexWhere,
+ lastIndexWhere,
+ lastIndexOf,
+ clear,
+ insert,
+ insertAll,
+ setAll,
+ remove,
+ removeAt,
+ removeLast,
+ removeWhere,
+ sublist,
+ getRange,
+ setRange,
+ removeRange,
+ fillRange,
+ replaceRange,
+ reset,
+ asMap,
+ ),
+ );
return state.value;
}
+/// Provides reactive list manipulation methods.
+///
+/// This class contains all the methods needed to manipulate a list while
+/// ensuring reactive updates. It mirrors most of Dart's List API but with
+/// reactive behavior that triggers widget rebuilds.
class ListAction {
+ /// Creates a [ListAction] with all the provided list manipulation functions.
ListAction(
this._list,
this.first,
@@ -250,36 +353,92 @@ class ListAction {
this.reset,
this.asMap,
);
+
+ /// Sets the first element of the list.
final void Function(E value) first;
+
+ /// Sets the last element of the list.
final void Function(E value) last;
+
+ /// Returns the length of the list.
final int Function() length;
+
+ /// Adds an element to the end of the list.
final void Function(E value) add;
+
+ /// Adds all elements of an iterable to the end of the list.
final void Function(Iterable iterable) addAll;
+
+ /// Sorts the list according to the compare function.
final void Function([int Function(E, E)? compare]) sort;
+
+ /// Shuffles the elements of the list randomly.
final void Function([Random? random]) shuffle;
+
+ /// Returns the first index of the element in the list.
final int Function(E element, [int start]) indexOf;
+
+ /// Returns the first index where the test function returns true.
final int Function(bool Function(E) test, [int start]) indexWhere;
+
+ /// Returns the last index where the test function returns true.
final int Function(bool Function(E) test, [int? start]) lastIndexWhere;
+
+ /// Returns the last index of the element in the list.
final int Function(E element, [int? start]) lastIndexOf;
+
+ /// Removes all elements from the list.
final void Function() clear;
+
+ /// Inserts an element at the specified index.
final void Function(int index, E element) insert;
+
+ /// Inserts all elements of an iterable at the specified index.
final void Function(int index, Iterable iterable) insertAll;
+
+ /// Overwrites elements with the elements of an iterable.
final void Function(int index, Iterable iterable) setAll;
+
+ /// Removes the first occurrence of the value from the list.
final bool Function(Object? value) remove;
+
+ /// Removes the element at the specified index.
final E Function(int index) removeAt;
+
+ /// Removes and returns the last element of the list.
final E Function() removeLast;
+
+ /// Removes all elements that satisfy the test function.
final void Function(bool Function(E element) test) removeWhere;
+
+ /// Returns a new list containing elements from start to end.
final List Function(int start, [int? end]) sublist;
+
+ /// Returns an iterable for the specified range.
final Iterable Function(int start, int end) getRange;
+
+ /// Copies elements from an iterable into a range of the list.
final void Function(int start, int end, Iterable iterable, [int skipCount])
setRange;
+
+ /// Removes elements in the specified range.
final void Function(int start, int end) removeRange;
+
+ /// Sets elements in a range to a fill value.
final void Function(int start, int end, [E? fillValue]) fillRange;
+
+ /// Replaces elements in a range with elements from an iterable.
final void Function(int start, int end, Iterable replacements)
replaceRange;
+
+ /// Resets the list to its initial value.
final VoidCallback reset;
+
+ /// Returns a map where keys are indices and values are list elements.
final Map Function() asMap;
+
final List Function() _list;
+ /// The current list value.
List get list => _list();
}
diff --git a/packages/basic/lib/src/use_map.dart b/packages/basic/lib/src/use_map.dart
index 8eb19c4..b95c547 100644
--- a/packages/basic/lib/src/use_map.dart
+++ b/packages/basic/lib/src/use_map.dart
@@ -1,45 +1,96 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
-/// Flutter state hook that tracks a value of a Map.
+/// Flutter state hook that manages a Map with reactive updates.
+///
+/// Provides methods to modify a map. The component re-builds when the map
+/// changes through these methods. Direct modification of the map does not
+/// trigger re-builds.
+///
+/// [initialMap] is the starting map value.
+///
+/// Returns a [MapAction] object that provides access to the current map
+/// and all modification methods.
+///
+/// Example:
+/// ```dart
+/// final mapActions = useMap({'a': 1, 'b': 2});
+///
+/// // Access the current map
+/// print(mapActions.map); // {'a': 1, 'b': 2}
+///
+/// // Add or update entries
+/// mapActions.add('c', 3);
+/// mapActions.addAll({'d': 4, 'e': 5});
+///
+/// // Remove entries
+/// mapActions.remove('b');
+///
+/// // Replace the entire map
+/// mapActions.replace({'x': 10, 'y': 20});
+///
+/// // Reset to initial value
+/// mapActions.reset();
+/// ```
MapAction useMap(Map initialMap) {
final map = useState(initialMap);
- final value = useCallback Function()>(() {
- return map.value;
- }, const []);
+ final value = useCallback Function()>(
+ () => map.value,
+ const [],
+ );
- final add = useCallback((key, entry) {
- map.value = {
- ...map.value,
- ...{key: entry}
- };
- }, const []);
+ final add = useCallback(
+ (key, entry) {
+ map.value = {
+ ...map.value,
+ ...{key: entry},
+ };
+ },
+ const [],
+ );
- final addAll = useCallback)>((value) {
- map.value = {...map.value, ...value};
- }, const []);
+ final addAll = useCallback)>(
+ (value) {
+ map.value = {...map.value, ...value};
+ },
+ const [],
+ );
- final replace = useCallback)>((newMap) {
- map.value = newMap;
- }, const []);
+ final replace = useCallback)>(
+ (newMap) {
+ map.value = newMap;
+ },
+ const [],
+ );
- final remove = useCallback((key) {
- final removedMap = {...map.value};
- removedMap.remove(key);
- map.value = removedMap;
- }, const []);
+ final remove = useCallback(
+ (key) {
+ final removedMap = {...map.value};
+ removedMap.remove(key);
+ map.value = removedMap;
+ },
+ const [],
+ );
- final reset = useCallback(() {
- map.value = initialMap;
- }, const []);
+ final reset = useCallback(
+ () {
+ map.value = initialMap;
+ },
+ const [],
+ );
final state =
useRef(MapAction(value, add, addAll, replace, remove, reset));
return state.value;
}
+/// Provides reactive map manipulation methods.
+///
+/// This class contains all the methods needed to manipulate a map while
+/// ensuring reactive updates that trigger widget rebuilds.
class MapAction {
+ /// Creates a [MapAction] with all the provided map manipulation functions.
const MapAction(
this._map,
this.add,
@@ -49,13 +100,26 @@ class MapAction {
this.reset,
);
+ /// Adds or updates a single entry in the map.
final void Function(K key, V entry) add;
+
+ /// Adds all entries from another map to this map.
final void Function(Map) addAll;
+
+ /// Replaces the entire map with a new map.
final void Function(Map) replace;
+
+ /// Removes an entry with the specified key from the map.
final void Function(K key) remove;
+
+ /// Resets the map to its initial value.
final VoidCallback reset;
+
final Map Function() _map;
+
+ /// Gets the value associated with the given key, or null if not found.
V? get(K key) => map[key];
+ /// The current map value.
Map get map => _map();
}
diff --git a/packages/basic/lib/src/use_mount.dart b/packages/basic/lib/src/use_mount.dart
index ec0c30b..ab8794a 100644
--- a/packages/basic/lib/src/use_mount.dart
+++ b/packages/basic/lib/src/use_mount.dart
@@ -1,11 +1,30 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_use/flutter_use.dart';
-/// Flutter lifecycle hook that calls a function after the component is mounted.
-/// Use useLifecycles if you need both a mount and unmount function.
-void useMount(VoidCallback fn) {
- // ignore: body_might_complete_normally_nullable
- return useEffectOnce(() {
- fn();
- });
-}
+/// Flutter lifecycle hook that executes a function when the component is mounted.
+///
+/// This hook runs the provided function once when the component is first mounted
+/// (similar to React's useEffect with an empty dependency array). It's useful
+/// for initialization logic that should only run once.
+///
+/// [fn] is the function to execute on mount.
+///
+/// Example:
+/// ```dart
+/// useMount(() {
+/// print('Component mounted!');
+/// // Initialize data, start timers, etc.
+/// fetchInitialData();
+/// });
+/// ```
+///
+/// Note: If you need both mount and unmount functions, use [useLifecycles] instead.
+///
+/// See also:
+/// - [useUnmount] for unmount-only logic
+/// - [useLifecycles] for both mount and unmount logic
+/// - [useEffectOnce] for the underlying implementation
+void useMount(VoidCallback fn) => useEffectOnce(() {
+ fn();
+ return null;
+ });
diff --git a/packages/basic/lib/src/use_number.dart b/packages/basic/lib/src/use_number.dart
index 2ce27f7..8c6afa7 100644
--- a/packages/basic/lib/src/use_number.dart
+++ b/packages/basic/lib/src/use_number.dart
@@ -1,7 +1,36 @@
import 'use_counter.dart';
-/// Flutter state hook that tracks a numeric value.
-/// useNumber is an alias for useCounter.
-CounterActions useNumber(int initialValue, {int? min, int? max}) {
- return useCounter(initialValue, min: min, max: max);
-}
+/// Flutter state hook that manages a numeric value with increment/decrement operations.
+///
+/// This is an alias for [useCounter] that provides a more semantic name
+/// when working specifically with numeric values and mathematical operations.
+///
+/// [initialValue] is the starting numeric value.
+/// [min] is the optional minimum value the number can reach.
+/// [max] is the optional maximum value the number can reach.
+///
+/// Returns a [CounterActions] object that provides methods to manipulate the number.
+///
+/// Throws [ArgumentError] if [initialValue] is outside the [min]/[max] bounds.
+///
+/// Example:
+/// ```dart
+/// final number = useNumber(5, min: 0, max: 10);
+///
+/// print(number.value); // 5
+///
+/// // Increment/decrement
+/// number.inc(); // 6
+/// number.dec(); // 5
+/// number.inc(3); // 8
+///
+/// // Set value
+/// number.setter(2); // 2
+///
+/// // Reset
+/// number.reset(); // back to 5
+/// ```
+///
+/// See also: [useCounter]
+CounterActions useNumber(int initialValue, {int? min, int? max}) =>
+ useCounter(initialValue, min: min, max: max);
diff --git a/packages/basic/lib/src/use_previous_distinct.dart b/packages/basic/lib/src/use_previous_distinct.dart
index eb76005..873ac9e 100644
--- a/packages/basic/lib/src/use_previous_distinct.dart
+++ b/packages/basic/lib/src/use_previous_distinct.dart
@@ -2,10 +2,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'use_first_mount_state.dart';
-/// Just like usePrevious but it will only update once the value actually
-/// changes. This is important when other hooks are involved and you aren't
-/// just interested in the previous props version, but want to know the
-/// previous distinct value
+/// Tracks the previous distinct value of a variable, updating only when the
+/// value actually changes according to the comparison function.
T? usePreviousDistinct(T value, [Predicate? compare]) {
compare ??= (prev, next) => prev == next;
final prevRef = useRef(null);
@@ -20,4 +18,19 @@ T? usePreviousDistinct(T value, [Predicate? compare]) {
return prevRef.value;
}
+/// A predicate function that compares two values for equality.
+///
+/// This function receives the previous and next values and should return
+/// true if they are considered equal, false if they are different.
+/// Used by [usePreviousDistinct] to determine when to update the previous value.
+///
+/// Example:
+/// ```dart
+/// // Custom comparison for objects
+/// bool userEquals(User prev, User next) {
+/// return prev.id == next.id && prev.name == next.name;
+/// }
+///
+/// final prevUser = usePreviousDistinct(currentUser, userEquals);
+/// ```
typedef Predicate = bool Function(T prev, T next);
diff --git a/packages/basic/lib/src/use_scroll.dart b/packages/basic/lib/src/use_scroll.dart
new file mode 100644
index 0000000..0ec7481
--- /dev/null
+++ b/packages/basic/lib/src/use_scroll.dart
@@ -0,0 +1,82 @@
+import 'package:flutter/widgets.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+
+/// State object returned by [useScroll].
+class ScrollState {
+ /// Creates a [ScrollState].
+ const ScrollState({
+ required this.x,
+ required this.y,
+ required this.controller,
+ });
+
+ /// The horizontal scroll offset.
+ final double x;
+
+ /// The vertical scroll offset.
+ final double y;
+
+ /// The scroll controller that can be attached to scrollable widgets.
+ final ScrollController controller;
+}
+
+/// Tracks scroll position of a scrollable widget.
+///
+/// Returns a [ScrollState] that contains:
+/// - [ScrollState.x]: The horizontal scroll offset (always 0 for single-axis scrollables)
+/// - [ScrollState.y]: The vertical scroll offset
+/// - [ScrollState.controller]: A [ScrollController] to attach to scrollable widgets
+///
+/// Example:
+/// ```dart
+/// Widget build(BuildContext context) {
+/// final scroll = useScroll();
+///
+/// return Column(
+/// children: [
+/// Text('Scroll position: ${scroll.y.toStringAsFixed(2)}'),
+/// Expanded(
+/// child: ListView.builder(
+/// controller: scroll.controller,
+/// itemCount: 100,
+/// itemBuilder: (context, index) => ListTile(
+/// title: Text('Item $index'),
+/// ),
+/// ),
+/// ),
+/// ],
+/// );
+/// }
+/// ```
+ScrollState useScroll() {
+ final controller = useMemoized(ScrollController.new, []);
+ final x = useState(0);
+ final y = useState(0);
+
+ useEffect(
+ () {
+ void listener() {
+ if (controller.hasClients) {
+ y.value = controller.offset;
+ // For single-axis scrollables, x is always 0
+ x.value = 0;
+ }
+ }
+
+ controller.addListener(listener);
+ return () => controller.removeListener(listener);
+ },
+ [controller],
+ );
+
+ useEffect(
+ () => controller.dispose,
+ [],
+ );
+
+ return ScrollState(
+ x: x.value,
+ y: y.value,
+ controller: controller,
+ );
+}
diff --git a/packages/basic/lib/src/use_scrolling.dart b/packages/basic/lib/src/use_scrolling.dart
new file mode 100644
index 0000000..4637274
--- /dev/null
+++ b/packages/basic/lib/src/use_scrolling.dart
@@ -0,0 +1,105 @@
+import 'dart:async';
+
+import 'package:flutter/widgets.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+
+/// State object returned by [useScrolling].
+class ScrollingState {
+ /// Creates a [ScrollingState].
+ const ScrollingState({
+ required this.isScrolling,
+ required this.controller,
+ });
+
+ /// Whether the scrollable widget is currently being scrolled.
+ final bool isScrolling;
+
+ /// The scroll controller that can be attached to scrollable widgets.
+ final ScrollController controller;
+}
+
+/// Tracks whether a scrollable widget is currently being scrolled.
+///
+/// Returns a [ScrollingState] that contains:
+/// - [ScrollingState.isScrolling]: Whether the widget is currently scrolling
+/// - [ScrollingState.controller]: A [ScrollController] to attach to scrollable widgets
+///
+/// The scrolling state is determined by detecting scroll activity and setting
+/// a timeout period. If no scroll events occur within the timeout, scrolling
+/// is considered to have stopped.
+///
+/// Example:
+/// ```dart
+/// Widget build(BuildContext context) {
+/// final scrolling = useScrolling();
+///
+/// return Column(
+/// children: [
+/// Container(
+/// color: scrolling.isScrolling ? Colors.red : Colors.green,
+/// height: 50,
+/// child: Center(
+/// child: Text(
+/// scrolling.isScrolling ? 'Scrolling...' : 'Not scrolling',
+/// ),
+/// ),
+/// ),
+/// Expanded(
+/// child: ListView.builder(
+/// controller: scrolling.controller,
+/// itemCount: 100,
+/// itemBuilder: (context, index) => ListTile(
+/// title: Text('Item $index'),
+/// ),
+/// ),
+/// ),
+/// ],
+/// );
+/// }
+/// ```
+ScrollingState useScrolling([
+ Duration timeout = const Duration(milliseconds: 150),
+]) {
+ final controller = useMemoized(ScrollController.new, []);
+ final isScrolling = useState(false);
+ final timer = useRef(null);
+
+ useEffect(
+ () {
+ void listener() {
+ if (controller.hasClients) {
+ // Set scrolling to true
+ isScrolling.value = true;
+
+ // Cancel any existing timer
+ timer.value?.cancel();
+
+ // Set a timer to detect when scrolling stops
+ timer.value = Timer(timeout, () {
+ isScrolling.value = false;
+ });
+ }
+ }
+
+ controller.addListener(listener);
+ return () {
+ controller.removeListener(listener);
+ timer.value?.cancel();
+ };
+ },
+ [controller, timeout],
+ );
+
+ useEffect(
+ () => () {
+ timer.value?.cancel();
+ controller.dispose();
+ },
+ [],
+ );
+
+ return ScrollingState(
+ isScrolling: isScrolling.value,
+ controller: controller,
+ );
+}
diff --git a/packages/basic/lib/src/use_set.dart b/packages/basic/lib/src/use_set.dart
index 492f476..6046580 100644
--- a/packages/basic/lib/src/use_set.dart
+++ b/packages/basic/lib/src/use_set.dart
@@ -1,53 +1,111 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
-/// Flutter state hook that tracks a Set.
+/// Flutter state hook that manages a Set with reactive updates.
+///
+/// Provides methods to modify a set. The component re-builds when the set
+/// changes through these methods. Direct modification of the set does not
+/// trigger re-builds.
+///
+/// [initialSet] is the starting set value.
+///
+/// Returns a [SetAction] object that provides access to the current set
+/// and all modification methods.
+///
+/// Example:
+/// ```dart
+/// final setActions = useSet({'a', 'b', 'c'});
+///
+/// // Access the current set
+/// print(setActions.set); // {'a', 'b', 'c'}
+///
+/// // Add elements
+/// setActions.add('d');
+/// setActions.addAll({'e', 'f'});
+///
+/// // Toggle elements (add if absent, remove if present)
+/// setActions.toggle('a'); // removes 'a'
+/// setActions.toggle('z'); // adds 'z'
+///
+/// // Remove elements
+/// setActions.remove('b');
+///
+/// // Replace the entire set
+/// setActions.replace({'x', 'y', 'z'});
+///
+/// // Reset to initial value
+/// setActions.reset();
+/// ```
SetAction useSet(Set initialSet) {
final set = useState(initialSet);
- final value = useCallback Function()>(() {
- return set.value;
- }, const []);
-
- final add = useCallback((element) {
- set.value = {
- ...set.value,
- ...{element}
- };
- }, const []);
-
- final addAll = useCallback)>((value) {
- set.value = {...set.value, ...value};
- }, const []);
-
- final replace = useCallback)>((newMap) {
- set.value = newMap;
- }, const []);
-
- final remove = useCallback((element) {
- final removedSet = {...set.value};
- removedSet.remove(element);
- set.value = removedSet;
- }, const []);
-
- final reset = useCallback(() {
- set.value = initialSet;
- }, const []);
-
- final toggle = useCallback((element) {
- final toggleSet = {...set.value};
- toggleSet.contains(element)
- ? toggleSet.remove(element)
- : toggleSet.add(element);
- set.value = toggleSet;
- }, const []);
+ final value = useCallback Function()>(
+ () => set.value,
+ const [],
+ );
+
+ final add = useCallback(
+ (element) {
+ set.value = {
+ ...set.value,
+ ...{element},
+ };
+ },
+ const [],
+ );
+
+ final addAll = useCallback)>(
+ (value) {
+ set.value = {...set.value, ...value};
+ },
+ const [],
+ );
+
+ final replace = useCallback)>(
+ (newMap) {
+ set.value = newMap;
+ },
+ const [],
+ );
+
+ final remove = useCallback(
+ (element) {
+ final removedSet = {...set.value};
+ removedSet.remove(element);
+ set.value = removedSet;
+ },
+ const [],
+ );
+
+ final reset = useCallback(
+ () {
+ set.value = initialSet;
+ },
+ const [],
+ );
+
+ final toggle = useCallback(
+ (element) {
+ final toggleSet = {...set.value};
+ toggleSet.contains(element)
+ ? toggleSet.remove(element)
+ : toggleSet.add(element);
+ set.value = toggleSet;
+ },
+ const [],
+ );
final state =
useRef(SetAction(value, add, addAll, replace, toggle, remove, reset));
return state.value;
}
+/// Provides reactive set manipulation methods.
+///
+/// This class contains all the methods needed to manipulate a set while
+/// ensuring reactive updates that trigger widget rebuilds.
class SetAction {
+ /// Creates a [SetAction] with all the provided set manipulation functions.
const SetAction(
this._set,
this.add,
@@ -58,13 +116,26 @@ class SetAction {
this.reset,
);
+ /// Adds an element to the set.
final void Function(E element) add;
+
+ /// Adds all elements from another set to this set.
final void Function(Set) addAll;
+
+ /// Replaces the entire set with a new set.
final void Function(Set