Async Switcher

A smart Flutter widget that automatically switches UI based on async state. Framework-independent and works with any state management solution.

pub package

Features

  • Automatic State Switching: Loading → Empty → Error → Data
  • Future & Stream Support: Built-in constructors for Future<T> and Stream<T>
  • Animated Transitions: Smooth transitions between states (configurable)
  • Retry Functionality: Built-in retry callback for error states
  • Auto Empty Detection: Automatically detects empty collections
  • Customizable Widgets: Custom widgets for each state
  • Framework Independent: Works with any state management (Bloc, Riverpod, Provider, etc.)
  • Default Widgets: Beautiful default widgets included out of the box

Installation

Add this to your package's pubspec.yaml file:

dependencies:
  async_switcher: ^1.0.0

Quick Start

Basic Usage

import 'package:async_switcher/async_switcher.dart';

StateWrapper<List<Product>>(
  state: productState,
  builder: (context, products) => ProductList(products: products),
  loadingMessage: 'Loading products...',
  emptyMessage: 'No products found',
  errorMessage: 'Failed to load products',
  onRetry: () => loadProducts(),
)

With Future

StateWrapper.fromFuture<List<Product>>(
  future: fetchProducts(),
  builder: (context, products) => ProductList(products: products),
  loadingMessage: 'Loading...',
  emptyMessage: 'No products',
  onRetry: () => loadProducts(),
)

With Stream

StateWrapper.fromStream<List<Product>>(
  stream: productsStream(),
  builder: (context, products) => ProductList(products: products),
  loadingMessage: 'Loading...',
  onRetry: () => resubscribe(),
)

API Reference

StateWrapper

The main widget that switches UI based on AsyncValue<T> state.

Constructor Parameters

Parameter Type Description
state AsyncValue<T> The current async state
builder Widget Function(BuildContext, T) Builder for data state (required)
loading Widget? Custom loading widget
empty Widget? Custom empty widget
error Widget? Custom error widget
onRetry VoidCallback? Retry callback for error state
animate bool Enable animated transitions (default: true)
transitionDuration Duration Transition duration (default: 300ms)
transitionCurve Curve Transition curve (default: Curves.easeInOut)
loadingMessage String? Message for default loading widget
emptyMessage String? Message for default empty widget
errorMessage String? Message for default error widget
detectEmpty bool Auto-detect empty collections (default: true)
isEmptyPredicate bool Function(T)? Custom empty detection predicate

AsyncValue

Sealed class representing async states:

  • AsyncLoading<T>() - Loading state
  • AsyncData<T>(T data) - Success state with data
  • AsyncEmpty<T>() - Empty state (no data)
  • AsyncError<T>(String message, {Object? error, StackTrace? stackTrace}) - Error state

Helper Methods

// Check state type
state.isLoading  // bool
state.hasData    // bool
state.isEmpty    // bool
state.isError    // bool

// Get data if available
final data = state.dataOrNull; // T?

// Map over data
final newState = state.map((data) => data.length);

Customization Examples

Custom Loading Widget

StateWrapper<List<Product>>(
  state: productState,
  builder: (context, products) => ProductList(products: products),
  loading: ShimmerLoader(), // Your custom shimmer
)

Custom Empty Widget

StateWrapper<List<Product>>(
  state: productState,
  builder: (context, products) => ProductList(products: products),
  empty: Center(
    child: Column(
      children: [
        Icon(Icons.shopping_bag_outlined, size: 64),
        Text('Your cart is empty'),
      ],
    ),
  ),
)

Custom Error Widget

StateWrapper<List<Product>>(
  state: productState,
  builder: (context, products) => ProductList(products: products),
  error: CustomErrorView(
    message: 'Failed to load',
    onRetry: () => loadProducts(),
  ),
)

Disable Animations

StateWrapper<List<Product>>(
  state: productState,
  builder: (context, products) => ProductList(products: products),
  animate: false,
)

Custom Empty Detection

StateWrapper<Map<String, dynamic>>(
  state: mapState,
  builder: (context, map) => MapView(map: map),
  isEmptyPredicate: (map) => map.isEmpty, // Custom check
)

State Management Examples

With Riverpod

final productsProvider = FutureProvider<List<Product>>((ref) async {
  return await fetchProducts();
});

// In widget
StateWrapper<List<Product>>(
  state: ref.watch(productsProvider).when(
    data: (data) => AsyncData(data),
    loading: () => AsyncLoading(),
    error: (err, stack) => AsyncError(err.toString()),
  ),
  builder: (context, products) => ProductList(products: products),
)

With Bloc

// In your bloc
Stream<List<Product>> mapEventToState(ProductEvent event) async* {
  yield ProductLoading();
  try {
    final products = await repository.fetchProducts();
    yield ProductLoaded(products);
  } catch (e) {
    yield ProductError(e.toString());
  }
}

// In widget
StateWrapper<List<Product>>(
  state: state.when(
    loading: () => AsyncLoading(),
    loaded: (products) => AsyncData(products),
    error: (message) => AsyncError(message),
  ),
  builder: (context, products) => ProductList(products: products),
)

With Provider/ChangeNotifier

class ProductNotifier extends ChangeNotifier {
  AsyncValue<List<Product>> state = AsyncLoading();

  Future<void> loadProducts() async {
    state = AsyncLoading();
    notifyListeners();

    try {
      final products = await repository.fetchProducts();
      state = AsyncData(products);
    } catch (e) {
      state = AsyncError(e.toString());
    }
    notifyListeners();
  }
}

// In widget
StateWrapper<List<Product>>(
  state: context.watch<ProductNotifier>().state,
  builder: (context, products) => ProductList(products: products),
  onRetry: () => context.read<ProductNotifier>().loadProducts(),
)

Use Cases

  • API Call Screens - Handle loading/error/data states automatically
  • List Screens - Perfect for product lists, user lists, etc.
  • Grid Views - Image galleries, product grids
  • Search Results - Empty states when no results found
  • Pagination - Loading more items
  • Firebase Streams - Real-time data from Firestore
  • Forms - Async validation states

Architecture

The package uses a sealed class hierarchy for type-safe state management:

AsyncValue<T>
├── AsyncLoading<T>
├── AsyncData<T>
├── AsyncEmpty<T>
└── AsyncError<T>

This ensures exhaustive pattern matching and prevents invalid states.

Additional Resources

Testing

The package includes comprehensive tests. Run them with:

flutter test

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

Inspired by Riverpod's AsyncValue but designed to be framework-independent and more flexible.

Package Health

This package aims for high pub.flutter-io.cn scores with:

  • Comprehensive documentation
  • Full test coverage
  • Example application
  • Type-safe API
  • Framework independence

Libraries

async_switcher
A smart Flutter widget that automatically switches UI based on async state