manual_suspense 1.0.0 copy "manual_suspense: ^1.0.0" to clipboard
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.

pub package License: MIT

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 FutureBuilder use 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.

1
likes
160
points
8
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A programmatically controlled suspense widget inspired by React Suspense. Manage loading states declaratively with fine-grained manual control.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on manual_suspense