async_switcher 1.0.0
async_switcher: ^1.0.0 copied to clipboard
A smart Flutter widget that automatically switches UI based on async state (loading → empty → error → data). Framework-independent, supports Future & Stream.
example/lib/main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:async_switcher/async_switcher.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Async Switcher Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Async Switcher Examples'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildExampleCard(
context,
title: 'Example 1: Basic Usage',
subtitle: 'Manual state management with StateWrapper',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const BasicExample()),
),
),
const SizedBox(height: 16),
_buildExampleCard(
context,
title: 'Example 2: Future Integration',
subtitle: 'Using StateWrapper.fromFuture()',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const FutureExample()),
),
),
const SizedBox(height: 16),
_buildExampleCard(
context,
title: 'Example 3: Stream Integration',
subtitle: 'Using StateWrapper.fromStream()',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const StreamExample()),
),
),
const SizedBox(height: 16),
_buildExampleCard(
context,
title: 'Example 4: Custom Widgets',
subtitle: 'Custom loading, empty, and error widgets',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const CustomWidgetsExample()),
),
),
const SizedBox(height: 16),
_buildExampleCard(
context,
title: 'Example 5: Product List',
subtitle: 'Real-world example with API simulation',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const ProductListExample()),
),
),
],
),
);
}
Widget _buildExampleCard(
BuildContext context, {
required String title,
required String subtitle,
required VoidCallback onTap,
}) {
return Card(
elevation: 2,
child: ListTile(
title: Text(title),
subtitle: Text(subtitle),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: onTap,
),
);
}
}
// Example 1: Basic Usage
class BasicExample extends StatefulWidget {
const BasicExample({super.key});
@override
State<BasicExample> createState() => _BasicExampleState();
}
class _BasicExampleState extends State<BasicExample> {
AsyncValue<List<String>> _state = const AsyncLoading();
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
setState(() => _state = const AsyncLoading());
await Future.delayed(const Duration(seconds: 1));
setState(() => _state = AsyncData(['Item 1', 'Item 2', 'Item 3']));
}
Future<void> _loadError() async {
setState(() => _state = const AsyncLoading());
await Future.delayed(const Duration(seconds: 1));
setState(() => _state = const AsyncError('Failed to load data'));
}
Future<void> _loadEmpty() async {
setState(() => _state = const AsyncLoading());
await Future.delayed(const Duration(seconds: 1));
setState(() => _state = const AsyncEmpty());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Basic Usage')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: _loadData,
child: const Text('Load Data'),
),
ElevatedButton(
onPressed: _loadError,
child: const Text('Load Error'),
),
ElevatedButton(
onPressed: _loadEmpty,
child: const Text('Load Empty'),
),
],
),
),
Expanded(
child: StateWrapper<List<String>>(
state: _state,
builder: (context, items) => ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ListTile(
title: Text(items[index]),
),
),
onRetry: _loadData,
),
),
],
),
);
}
}
// Example 2: Future Integration
class FutureExample extends StatelessWidget {
const FutureExample({super.key});
Future<List<Product>> _fetchProducts() async {
await Future.delayed(const Duration(seconds: 2));
return [
Product(name: 'Product 1', price: 29.99, id: 1),
Product(name: 'Product 2', price: 39.99, id: 2),
Product(name: 'Product 3', price: 49.99, id: 3),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Future Integration')),
body: StateWrapper.fromFuture<List<Product>>(
future: _fetchProducts(),
builder: (context, products) => ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
title: Text(product.name),
subtitle: Text('\$${product.price.toStringAsFixed(2)}'),
leading: CircleAvatar(child: Text('${product.id}')),
),
);
},
),
loadingMessage: 'Loading products...',
emptyMessage: 'No products available',
),
);
}
}
// Example 3: Stream Integration
class StreamExample extends StatelessWidget {
const StreamExample({super.key});
Stream<List<String>> _createStream() async* {
yield* Stream.periodic(const Duration(seconds: 1), (count) => count)
.asyncMap((count) async {
await Future.delayed(const Duration(seconds: 1));
return List.generate(count + 1, (i) => 'Stream Item ${i + 1}');
}).take(5);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Stream Integration')),
body: StateWrapper.fromStream<List<String>>(
stream: _createStream(),
builder: (context, items) => ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ListTile(
title: Text(items[index]),
leading: CircleAvatar(child: Text('${index + 1}')),
),
),
loadingMessage: 'Waiting for stream data...',
),
);
}
}
// Example 4: Custom Widgets
class CustomWidgetsExample extends StatefulWidget {
const CustomWidgetsExample({super.key});
@override
State<CustomWidgetsExample> createState() => _CustomWidgetsExampleState();
}
class _CustomWidgetsExampleState extends State<CustomWidgetsExample> {
AsyncValue<List<String>> _state = const AsyncLoading();
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
setState(() => _state = const AsyncLoading());
await Future.delayed(const Duration(seconds: 2));
setState(() => _state = AsyncData(['Custom Item 1', 'Custom Item 2']));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Custom Widgets')),
body: StateWrapper<List<String>>(
state: _state,
builder: (context, items) => ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ListTile(
title: Text(items[index]),
),
),
loading: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading with custom widget...'),
],
),
),
empty: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox, size: 64, color: Colors.grey),
const SizedBox(height: 16),
Text('Nothing to show', style: TextStyle(fontSize: 18)),
],
),
),
error: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text('Oops! Something went wrong',
style: TextStyle(fontSize: 18)),
],
),
),
onRetry: _loadData,
),
floatingActionButton: FloatingActionButton(
onPressed: _loadData,
child: const Icon(Icons.refresh),
),
);
}
}
// Example 5: Product List (Real-world example)
class ProductListExample extends StatefulWidget {
const ProductListExample({super.key});
@override
State<ProductListExample> createState() => _ProductListExampleState();
}
class _ProductListExampleState extends State<ProductListExample> {
Future<List<Product>> _fetchProducts() async {
await Future.delayed(const Duration(seconds: 2));
// Simulate API call that might fail
if (DateTime.now().millisecond % 3 == 0) {
throw Exception('Network error: Failed to fetch products');
}
// Simulate empty result
if (DateTime.now().millisecond % 2 == 0) {
return [];
}
return [
Product(name: 'Laptop', price: 999.99, id: 1),
Product(name: 'Mouse', price: 29.99, id: 2),
Product(name: 'Keyboard', price: 79.99, id: 3),
Product(name: 'Monitor', price: 299.99, id: 4),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Product List'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => setState(() {}),
),
],
),
body: StateWrapper.fromFuture<List<Product>>(
future: _fetchProducts(),
builder: (context, products) => GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.75,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return Card(
elevation: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Container(
color: Colors.blue.shade100,
child: Center(
child: Text(
product.name[0],
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
),
),
),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
'\$${product.price.toStringAsFixed(2)}',
style: TextStyle(
color: Colors.blue.shade700,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
);
},
),
loadingMessage: 'Fetching products...',
emptyMessage: 'No products available',
onRetry: () => setState(() {}),
),
);
}
}
class Product {
final String name;
final double price;
final int id;
Product({required this.name, required this.price, required this.id});
}