builder_plus 1.1.8
builder_plus: ^1.1.8 copied to clipboard
A Flutter package for simplified async operations with built-in loading, error, and empty states management.
Repository on GitHub | Report an Issue
builder_plus Package #
A Flutter package to simplify the management of asynchronous operations with loading, error, and empty data states.
Features #
- FutureWorker: Simplified wrapper for FutureBuilder
- StreamWorker: Simplified wrapper for StreamBuilder
- State Widgets: OptionLoading, OptionEmpty, OptionError
- Error Handling: Automatic error management with customizable error builders and retry option
- Production Mode: Hides empty states in production
Installation #
dependencies:
builder_plus: <latest_version>
Example #
Here is a complete example using all main features of the package:
import 'package:flutter/material.dart';
import 'package:builder_plus/builder_plus.dart';
import 'package:builder_plus/model/option_loading.dart';
import 'package:builder_plus/model/option_empty.dart';
import 'package:builder_plus/model/option_error.dart';
// A custom class to illustrate typing
typedef Id = int;
class Example {
final Id id;
final String name;
Example(this.id, this.name);
@override
String toString() => 'Example(id: $id, name: $name)';
}
// Simulate an asynchronous operation on an Example
Future<Example?> processExample(Example example, {bool throwError = false, bool empty = false}) async {
await Future.delayed(const Duration(seconds: 2));
if (throwError) throw Exception('Network error');
if (empty) return null;
return Example(example.id, example.name.toUpperCase());
}
// Simulate a stream of operations on an Example
Stream<Example?> streamExample(Example example, {bool throwError = false, bool empty = false}) async* {
yield null;
await Future.delayed(const Duration(seconds: 1));
if (throwError) throw Exception('Stream error');
if (empty) yield null;
else yield Example(example.id, example.name.toUpperCase());
await Future.delayed(const Duration(seconds: 1));
yield Example(example.id, ' [1m${example.name.toUpperCase()} (final)');
}
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'builder_plus Example',
theme: ThemeData(primarySwatch: Colors.blue),
home: const ExampleScreen(),
);
}
}
class ExampleScreen extends StatefulWidget {
const ExampleScreen({super.key});
@override
State<ExampleScreen> createState() => _ExampleScreenState();
}
class _ExampleScreenState extends State<ExampleScreen> {
bool throwFutureError = false;
bool throwStreamError = false;
bool showEmpty = false;
@override
Widget build(BuildContext context) {
final example = Example(1, 'demo');
return Scaffold(
appBar: AppBar(title: const Text('FutureWorker & StreamWorker Example')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Checkbox(
value: throwFutureError,
onChanged: (v) => setState(() => throwFutureError = v ?? false),
),
const Text('Future Error'),
const SizedBox(width: 16),
Checkbox(
value: throwStreamError,
onChanged: (v) => setState(() => throwStreamError = v ?? false),
),
const Text('Stream Error'),
const SizedBox(width: 16),
Checkbox(
value: showEmpty,
onChanged: (v) => setState(() => showEmpty = v ?? false),
),
const Text('Empty State'),
],
),
const SizedBox(height: 24),
const Text('FutureWorker', style: TextStyle(fontWeight: FontWeight.bold)),
FutureWorker<Example?>(
future: processExample(example, throwError: throwFutureError, empty: showEmpty),
builder: (context, result) => Text(
'After processing: \n [1m [0m${result.toString()}',
style: const TextStyle(fontSize: 18),
),
loadingBuilder: (context) => const OptionLoading(size: 32),
emptyBuilder: (context) => const OptionEmpty(
text: 'No data received',
icon: Icons.hourglass_empty,
),
errorBuilder: (context, error) => OptionError(
error: error,
onRetry: () => setState(() {}),
retryButtonText: 'Retry',
),
),
const SizedBox(height: 32),
const Text('StreamWorker', style: TextStyle(fontWeight: FontWeight.bold)),
StreamWorker<Example?>(
stream: streamExample(example, throwError: throwStreamError, empty: showEmpty),
builder: (context, result) => Text(
'Stream: \n${result.toString()}',
style: const TextStyle(fontSize: 18),
),
loadingBuilder: (context) => const OptionLoading(size: 32, color: Colors.orange),
emptyBuilder: (context) => const OptionEmpty(
text: 'Empty stream',
icon: Icons.stream,
iconColor: Colors.orange,
),
errorBuilder: (context, error) => OptionError(
error: error,
onRetry: () => setState(() {}),
retryButtonText: 'Restart stream',
),
),
],
),
),
);
}
}
Error Handling #
Both FutureWorker and StreamWorker provide flexible error handling:
- Custom Error Builder: Use
errorBuilderto provide a custom widget when an error occurs. The builder receives the error object (Object?) as the second parameter. - Default Error Widget: If no
errorBuilderis provided, the widgets automatically displayOptionErrorwith the error message. - OptionError Widget: The
OptionErrorwidget accepts any error object type and automatically converts it to a displayable string.
Example with custom error handling:
FutureWorker<User>(
future: userService.getUser(id),
builder: (context, user) => UserCard(user: user),
errorBuilder: (context, error) => Column(
children: [
Icon(Icons.error, color: Colors.red),
Text('Failed to load user: ${error?.toString() ?? "Unknown error"}'),
ElevatedButton(
onPressed: () => setState(() {}),
child: Text('Retry'),
),
],
),
)
Parameters #
FutureWorker / StreamWorker #
| Parameter | Type | Description |
|---|---|---|
| `future | stream` | `Future |
builder |
Widget Function(BuildContext, T) |
Builder for the data |
isProd |
bool |
Production mode (hides empty states) |
initialData |
T? |
Initial data |
emptyBuilder |
Widget Function(BuildContext)? |
Builder for empty state |
errorBuilder |
Widget Function(BuildContext, Object?)? |
Builder for error state (receives the error object) |
loadingBuilder |
Widget Function(BuildContext)? |
Builder for loading |
OptionLoading #
| Parameter | Type | Default | Description |
|---|---|---|---|
size |
double |
24.0 |
Indicator size |
color |
Color? |
null |
Color (uses theme if null) |
OptionEmpty #
| Parameter | Type | Default | Description |
|---|---|---|---|
text |
String |
- | Text to display |
icon |
IconData? |
null |
Optional icon |
textStyle |
TextStyle? |
null |
Text style |
iconSize |
double |
48.0 |
Icon size |
iconColor |
Color? |
null |
Icon color |
spacing |
double |
16.0 |
Spacing |
OptionError #
| Parameter | Type | Default | Description |
|---|---|---|---|
error |
Object? |
- | Error object or message (automatically converted to string) |
onRetry |
VoidCallback? |
null |
Retry callback |
textStyle |
TextStyle? |
null |
Text style |
iconSize |
double |
48.0 |
Icon size |
iconColor |
Color? |
null |
Icon color |
spacing |
double |
16.0 |
Spacing |
showRetryButton |
bool |
true |
Show retry button |
retryButtonText |
String |
'Retry' | Retry button text |
Note: The error parameter accepts any object type. If it's a String, it will be displayed directly. If it's an Exception or other object, it will be converted to a string automatically. If null, it will display "An unknown error occurred".