manual_suspense 1.0.0
manual_suspense: ^1.0.0 copied to clipboard
A programmatically controlled suspense widget inspired by React Suspense. Manage loading states declaratively with fine-grained manual control.
Manual Suspense #
A programmatically controlled suspense widget inspired by React Suspense. Manage loading states declaratively with fine-grained manual control over task completion.
Features #
- ๐ฏ Manual Control: Programmatically start and complete suspense tasks
- ๐ Declarative UI: Automatically switches between loading and content states
- ๐งน Auto Cleanup: Optional automatic resource cleanup on widget disposal
- ๐ Key-based System: Manage multiple independent loading states
- ๐ฆ Zero Dependencies: Built entirely with Flutter framework
- โก Lightweight: Minimal overhead, maximum performance
Installation #
Add this to your pubspec.yaml:
dependencies:
manual_suspense: ^1.0.0
Then run:
flutter pub get
Quick Start #
import 'package:manual_suspense/manual_suspense.dart';
// 1. Display the widget
ManualSuspense(
suspenseKey: 'fetch-users',
fallback: CircularProgressIndicator(),
child: UsersList(),
)
// 2. Control it programmatically
void loadUsers() async {
SuspenseController.start('fetch-users');
try {
final users = await api.fetchUsers();
setState(() => _users = users);
SuspenseController.complete('fetch-users');
} catch (e) {
// Handle error
SuspenseController.complete('fetch-users');
}
}
Usage Examples #
Basic Usage #
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ManualSuspense(
suspenseKey: 'load-data',
fallback: Center(
child: CircularProgressIndicator(),
),
child: DataView(),
),
floatingActionButton: FloatingActionButton(
onPressed: _refreshData,
child: Icon(Icons.refresh),
),
);
}
Future<void> _refreshData() async {
SuspenseController.start('load-data');
await Future.delayed(Duration(seconds: 2));
SuspenseController.complete('load-data');
}
}
With Auto Cleanup #
ManualSuspense(
suspenseKey: 'profile-${userId}',
autoRemoveOnDispose: true, // Cleanup when widget is removed
fallback: ProfileSkeleton(),
child: ProfileCard(user: user),
)
Multiple Independent Tasks #
Column(
children: [
ManualSuspense(
suspenseKey: 'users',
fallback: Text('Loading users...'),
child: UsersList(),
),
ManualSuspense(
suspenseKey: 'posts',
fallback: Text('Loading posts...'),
child: PostsList(),
),
],
)
Pull to Refresh #
class DataPage extends StatefulWidget {
@override
State<DataPage> createState() => _DataPageState();
}
class _DataPageState extends State<DataPage> {
List<String> _items = [];
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: _handleRefresh,
child: ManualSuspense(
suspenseKey: 'data-list',
fallback: Center(child: CircularProgressIndicator()),
child: ListView.builder(
itemCount: _items.length,
itemBuilder: (context, index) => ListTile(
title: Text(_items[index]),
),
),
),
);
}
Future<void> _handleRefresh() async {
SuspenseController.start('data-list');
try {
final data = await fetchData();
setState(() => _items = data);
SuspenseController.complete('data-list');
} catch (e) {
SuspenseController.complete('data-list');
// Show error
}
}
}
Form Submission #
class LoginForm extends StatefulWidget {
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(/* ... */),
TextFormField(/* ... */),
ManualSuspense(
suspenseKey: 'login-submit',
fallback: CircularProgressIndicator(),
child: ElevatedButton(
onPressed: _handleSubmit,
child: Text('Login'),
),
),
],
),
);
}
Future<void> _handleSubmit() async {
if (!_formKey.currentState!.validate()) return;
SuspenseController.start('login-submit');
try {
await authService.login(email, password);
SuspenseController.complete('login-submit');
// Navigate to home
} catch (e) {
SuspenseController.complete('login-submit');
// Show error
}
}
}
Error Handling Pattern #
Future<void> loadData() async {
SuspenseController.start('my-data');
try {
final data = await api.fetchData();
setState(() => _data = data);
SuspenseController.complete('my-data');
} on NetworkException catch (e) {
SuspenseController.complete('my-data');
showErrorDialog('Network error: ${e.message}');
} catch (e) {
SuspenseController.complete('my-data');
showErrorDialog('Unexpected error occurred');
}
}
API Reference #
SuspenseController #
Static methods for controlling suspense tasks:
| Method | Description |
|---|---|
start(String key) |
Start a new suspense task |
complete(String key) |
Complete a suspense task |
remove(String key) |
Remove task and cleanup resources |
has(String key) |
Check if task exists |
isRunning(String key) |
Check if task is currently running |
clearAll() |
Remove all tasks and cleanup |
ManualSuspense #
Widget properties:
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
suspenseKey |
String |
โ | - | Unique identifier for the task |
fallback |
Widget |
โ | - | Widget to show while loading |
child |
Widget |
โ | - | Widget to show when completed |
autoRemoveOnDispose |
bool |
โ | false |
Auto cleanup on disposal |
When to Use #
โ Perfect for:
- Manual data fetching with custom loading states
- Multi-step async workflows
- Form submissions with loading indicators
- Pull-to-refresh implementations
- Complex async operations requiring fine-grained control
- Custom loading sequences
- Retry mechanisms
โ Not recommended for:
- Simple
FutureBuilderuse cases - Automatic API call management (consider state management solutions like Riverpod, Bloc)
- When you don't need manual control over loading states
Comparison with Alternatives #
| Feature | manual_suspense | FutureBuilder | State Management |
|---|---|---|---|
| Manual control | โ | โ | โ |
| Declarative UI | โ | โ | โ |
| Multiple tasks | โ | โ | โ |
| Zero dependencies | โ | โ | โ |
| Learning curve | Low | Low | Medium-High |
Contributing #
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
License #
This project is licensed under the MIT License - see the LICENSE file for details.
Support #
If you like this package, please give it a โญ๏ธ on GitHub!
For issues and feature requests, please visit the issue tracker.