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.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:manual_suspense/manual_suspense.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Manual Suspense Example',
      theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  List<String> _data = [];
  int _loadCount = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Manual Suspense Demo'), elevation: 2),
      body: RefreshIndicator(
        onRefresh: _loadData,
        child: Column(
          children: [
            Padding(
              padding: EdgeInsets.all(16),
              child: Text(
                'Pull down to refresh or tap the button below',
                style: Theme.of(context).textTheme.titleSmall,
                textAlign: TextAlign.center,
              ),
            ),
            Expanded(
              child: ManualSuspense(
                suspenseKey: 'fetch-data',
                fallback: Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      CircularProgressIndicator(),
                      SizedBox(height: 16),
                      Text(
                        'Loading data...',
                        style: TextStyle(fontSize: 16, color: Colors.grey[600]),
                      ),
                    ],
                  ),
                ),
                child: _data.isEmpty
                    ? Center(
                        child: Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Icon(
                              Icons.inbox_outlined,
                              size: 64,
                              color: Colors.grey[400],
                            ),
                            SizedBox(height: 16),
                            Text(
                              'No data loaded yet',
                              style: TextStyle(
                                fontSize: 18,
                                color: Colors.grey[600],
                              ),
                            ),
                            SizedBox(height: 8),
                            Text(
                              'Tap the button to load data',
                              style: TextStyle(
                                fontSize: 14,
                                color: Colors.grey[500],
                              ),
                            ),
                          ],
                        ),
                      )
                    : ListView.builder(
                        padding: EdgeInsets.all(8),
                        itemCount: _data.length,
                        itemBuilder: (context, index) => Card(
                          child: ListTile(
                            leading: CircleAvatar(child: Text('${index + 1}')),
                            title: Text(_data[index]),
                            subtitle: Text('Loaded $_loadCount time(s)'),
                            trailing: Icon(Icons.chevron_right),
                          ),
                        ),
                      ),
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: _loadData,
        icon: Icon(Icons.refresh),
        label: Text('Load Data'),
      ),
    );
  }

  Future<void> _loadData() async {
    // Start the suspense state
    SuspenseController.start('fetch-data');

    try {
      // Simulate API call with delay
      await Future.delayed(Duration(seconds: 2));

      // Generate mock data
      setState(() {
        _loadCount++;
        _data = List.generate(20, (i) => 'Item ${i + 1} - Load #$_loadCount');
      });

      // Complete the suspense state
      SuspenseController.complete('fetch-data');

      // Show success message
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Data loaded successfully!'),
            duration: Duration(seconds: 2),
            behavior: SnackBarBehavior.floating,
          ),
        );
      }
    } catch (e) {
      // Complete suspense even on error
      SuspenseController.complete('fetch-data');

      // Show error message
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Error: $e'),
            backgroundColor: Colors.red,
            duration: Duration(seconds: 3),
            behavior: SnackBarBehavior.floating,
          ),
        );
      }
    }
  }

  @override
  void dispose() {
    // Optional: cleanup when leaving the page
    SuspenseController.remove('fetch-data');
    super.dispose();
  }
}
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