flutter_api_state 0.0.4
flutter_api_state: ^0.0.4 copied to clipboard
A Flutter package that simplifies async UI state handling (loading, success, error, empty) with built-in retry and pull-to-refresh support.
flutter_api_state #
ApiStateBuilder — a declarative Flutter widget that replaces repetitive
FutureBuilder boilerplate. It handles loading, error, empty, and
success states in one place, with built-in retry, pull-to-refresh,
and an optional network check.
Why #
Most screens that talk to an API end up writing the same FutureBuilder block:
check connectionState, check hasError, check for an empty list, then render
the success UI. ApiStateBuilder collapses all four branches into one widget —
and adds the things you usually bolt on later (retry, refresh, offline check).
Install #
dependencies:
flutter_api_state: ^0.0.3
import 'package:flutter_api_state/flutter_api_state.dart';
Quick start #
ApiStateBuilder<List<User>>(
future: () => fetchUsers(),
loading: const Center(child: CircularProgressIndicator()),
error: (context, error, stack) => Center(child: Text('$error')),
empty: const Center(child: Text('No users')),
success: (context, users) => UserList(users),
)
futuretakes a factory (() => fetchUsers()), not aFuturedirectly. That's what lets the widget re-invoke it for retry, pull-to-refresh, and network re-check.
API #
| Parameter | Type | Required | Default | What it does |
|---|---|---|---|---|
future |
Future<T> Function() |
✅ | — | The async work to run |
success |
Widget Function(BuildContext, T) |
✅ | — | UI for the resolved (non-empty) data |
loading |
Widget? |
— | Center(CircularProgressIndicator()) |
Shown while pending |
error |
Widget Function(BuildContext, Object, StackTrace?)? |
— | Center(Text('$error')) |
Shown on thrown exception |
empty |
Widget? |
— | Center(Text('No data')) |
Shown when data is empty |
enableRetry |
bool |
— | false |
Adds a Retry button under errors |
retryButtonBuilder |
Widget Function(BuildContext, VoidCallback)? |
— | ElevatedButton('Retry') |
Custom retry button |
enablePullToRefresh |
bool |
— | false |
Wraps content in a RefreshIndicator |
refreshIndicatorColor |
Color? |
— | theme default | Colour for the refresh spinner |
enableNetworkCheck |
bool |
— | false |
Run a connectivity check before future |
noNetwork |
Widget? |
— | Center(Text('No internet connection')) |
Shown when offline |
hasNetwork |
Future<bool> Function()? |
— | DNS lookup (mobile/desktop) / true (web) |
Custom connectivity check |
Empty detection #
The empty branch fires automatically when the resolved data is:
null- an empty
List - an empty
Map - an empty
Set - an empty
String
Generic support #
ApiStateBuilder<User>(...)
ApiStateBuilder<List<User>>(...)
ApiStateBuilder<Map<String, dynamic>>(...)
ApiStateBuilder<String>(...)
ApiStateBuilder<bool>(...)
Retry button #
ApiStateBuilder<List<User>>(
future: () => fetchUsers(),
enableRetry: true,
retryButtonBuilder: (ctx, onRetry) => OutlinedButton(
onPressed: onRetry,
child: const Text('Try again'),
),
error: (ctx, e, _) => Text('$e'),
success: (ctx, users) => UserList(users),
)
Conditions: the button only appears when all three are true — the future
threw, enableRetry: true, and (optionally) retryButtonBuilder is set.
Tapping it re-invokes future().
Pull-to-refresh #
ApiStateBuilder<List<User>>(
future: () => fetchUsers(),
enablePullToRefresh: true,
refreshIndicatorColor: Colors.red,
success: (ctx, users) => ListView.builder(/* ... */),
)
Works whether your success widget is already a scrollable
(ListView / GridView / CustomScrollView) or a plain widget like Center
— the package detects the type and wraps only when needed.
Network check #
Skip the request entirely when the device is offline:
ApiStateBuilder<List<Post>>(
future: () => fetchPosts(),
enableNetworkCheck: true,
noNetwork: const Center(child: Text('You are offline. Please reconnect.')),
enableRetry: true, // shows a Retry button under noNetwork too
success: (ctx, posts) => PostList(posts),
)
The default check resolves DNS for one.one.one.one with a 3 s timeout
(mobile/desktop). On web the default is a no-op (true) because the browser
already handles its own offline behavior.
Plug in connectivity_plus or your own logic:
ApiStateBuilder<List<Post>>(
future: () => fetchPosts(),
enableNetworkCheck: true,
hasNetwork: () async {
final result = await Connectivity().checkConnectivity();
return result != ConnectivityResult.none;
},
noNetwork: const OfflineBanner(),
success: (ctx, posts) => PostList(posts),
)
Note on captive portals: the default DNS check correctly reports offline on hotel/airport Wi-Fi where you're "connected" but blocked. If you want "any network present" semantics instead, plug in
connectivity_plusas shown above.
Full example #
The example/ folder contains a runnable app that fetches posts
from jsonplaceholder.typicode.com with all features turned on (loading,
success, error, empty, retry, pull-to-refresh):
ApiStateBuilder<List<dynamic>>(
future: () => fetchPosts(),
loading: const Center(child: CircularProgressIndicator()),
enablePullToRefresh: true,
refreshIndicatorColor: Colors.red,
enableRetry: true,
retryButtonBuilder: (ctx, onRetry) => OutlinedButton(
onPressed: onRetry,
child: const Text('Try again'),
),
success: (context, posts) => ListView.builder(
itemCount: posts.length,
itemBuilder: (context, i) => Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: ListTile(
title: Text(posts[i]['title'],
style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(posts[i]['body']),
),
),
),
error: (context, error, stack) => Center(child: Text('$error')),
empty: const Center(child: Text('No posts')),
)
Run it locally:
cd example
flutter pub get
flutter run
Cookbook #
1. API list screen with everything turned on #
ApiStateBuilder<List<Post>>(
future: () => api.fetchPosts(),
loading: const Center(child: CircularProgressIndicator()),
empty: const Center(child: Text('No posts yet')),
error: (_, e, __) => Center(child: Text('Failed: $e')),
success: (_, posts) => ListView(
children: [for (final p in posts) PostTile(p)],
),
enableRetry: true,
enablePullToRefresh: true,
enableNetworkCheck: true,
)
2. User profile API (single object) #
ApiStateBuilder<User>(
future: () => api.fetchUser(id),
loading: const CircularProgressIndicator(),
success: (_, user) => ProfileView(user),
)
3. Custom empty state #
ApiStateBuilder<List<Task>>(
future: () => api.fetchTasks(),
empty: const _NoTasksPlaceholder(),
success: (_, tasks) => TaskList(tasks),
)
4. Custom error widget with stack trace #
ApiStateBuilder<Report>(
future: () => api.fetchReport(),
error: (_, e, stack) => ErrorView(error: e, stack: stack),
success: (_, report) => ReportView(report),
)
5. Skeleton-shimmer loading #
ApiStateBuilder<List<Item>>(
future: () => api.fetchItems(),
loading: const ShimmerListSkeleton(),
success: (_, items) => ItemGrid(items),
)
Package structure #
lib/
flutter_api_state.dart // public barrel
src/
api_state_builder_widget.dart // the widget
empty_detector.dart // null / empty list / map / set / string
network_checker.dart // conditional export
network_checker_io.dart // dart:io DNS lookup
network_checker_web.dart // web stub
state_helpers.dart // typedefs
example/
lib/main.dart // runnable demo
Changelog #
See CHANGELOG.md.
License #
MIT — see LICENSE.