Add context.effectQueue() to widgets to consume and process queued effects in sequence
This skill adds context.effectQueue() to widgets to consume and process queued effects in order.
Consumes EffectQueue<T> in widgets to:
The state must have an EffectQueue<T> field with defined effect types:
sealed class UiEffect {}
class ShowToast extends UiEffect { final String message; ShowToast(this.message); }
class Navigate extends UiEffect { final String route; Navigate(this.route); }
class AppState {
final EffectQueue<UiEffect> effectQueue;
AppState({EffectQueue<UiEffect>? effectQueue})
: effectQueue = effectQueue ?? EffectQueue.spent();
}
Use context.effectQueue() in the build method:
import 'package:bloc_superpowers/bloc_superpowers.dart';
class MyScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
context.effectQueue<MyCubit, UiEffect>(
// 1. Select the queue from state
(cubit) => cubit.state.effectQueue,
// 2. Handle each effect type
(context, effect) => switch (effect) {
ShowToast(:final message) =>
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
),
Navigate(:final route) =>
Navigator.pushNamed(context, route),
},
);
return Scaffold(
body: MyContent(),
);
}
}
context.effectQueue<CubitType, EffectType>(
// Selector: extract queue from state
(cubit) => cubit.state.effectQueue,
// Handler: process each effect
(context, effect) => /* handle effect */,
// Optional: execution mode
onePerFrame: true, // Default: true
);
Effects process one at a time, with a rebuild between each:
context.effectQueue<MyCubit, UiEffect>(
(c) => c.state.effectQueue,
onePerFrame: true, // Default - sequential with rebuilds
(context, effect) => handleEffect(effect),
);
Good for:
All effects process in a single frame:
context.effectQueue<MyCubit, UiEffect>(
(c) => c.state.effectQueue,
onePerFrame: false, // All effects execute immediately
(context, effect) => handleEffect(effect),
);
Good for:
Use Dart 3 pattern matching to handle different effect types:
(context, effect) => switch (effect) {
// Destructure properties
ShowToast(:final message) =>
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
),
// Multiple properties
ShowDialog(:final title, :final content) =>
showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text(title),
content: Text(content),
),
),
// No properties
ClearForm() =>
_formKey.currentState?.reset(),
// Navigate with dynamic route
Navigate(:final route) =>
Navigator.pushNamed(context, route),
// Complex handling
ShowConfirmation(:final message, :final onConfirm, :final onCancel) =>
showDialog(
context: context,
builder: (_) => AlertDialog(
content: Text(message),
actions: [
TextButton(onPressed: onCancel, child: Text('Cancel')),
TextButton(onPressed: onConfirm, child: Text('Confirm')),
],
),
),
},
sealed class OnboardingEffect {}
class WelcomeDialog extends OnboardingEffect {}
class PermissionRequest extends OnboardingEffect { final String permission; ... }
class TutorialOverlay extends OnboardingEffect {}
class NavigateToHome extends OnboardingEffect {}
// In widget
context.effectQueue<OnboardingCubit, OnboardingEffect>(
(c) => c.state.effectQueue,
onePerFrame: true, // Show one step at a time
(context, effect) => switch (effect) {
WelcomeDialog() => showWelcomeDialog(context),
PermissionRequest(:final permission) => requestPermission(permission),
TutorialOverlay() => showTutorial(context),
NavigateToHome() => Navigator.pushReplacementNamed(context, '/home'),
},
);
sealed class FormEffect {}
class ShowSaving extends FormEffect {}
class HideSaving extends FormEffect {}
class ShowSuccess extends FormEffect { final String message; ... }
class ClearForm extends FormEffect {}
class NavigateBack extends FormEffect {}
// In widget
context.effectQueue<FormCubit, FormEffect>(
(c) => c.state.effectQueue,
(context, effect) => switch (effect) {
ShowSaving() => _showLoadingOverlay(),
HideSaving() => _hideLoadingOverlay(),
ShowSuccess(:final message) =>
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), backgroundColor: Colors.green),
),
ClearForm() => _formKey.currentState?.reset(),
NavigateBack() => Navigator.pop(context),
},
);
sealed class ErrorEffect {}
class LogError extends ErrorEffect { final Object error; ... }
class ShowErrorMessage extends ErrorEffect { final String message; ... }
class OfferRetry extends ErrorEffect { final VoidCallback onRetry; ... }
// In widget
context.effectQueue<DataCubit, ErrorEffect>(
(c) => c.state.effectQueue,
(context, effect) => switch (effect) {
LogError(:final error) => analytics.logError(error),
ShowErrorMessage(:final message) =>
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), backgroundColor: Colors.red),
),
OfferRetry(:final onRetry) =>
showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('Error'),
content: Text('Would you like to retry?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
onRetry();
},
child: Text('Retry'),
),
],
),
),
},
);
// Effects
sealed class CheckoutEffect {}
class ShowProgress extends CheckoutEffect {
final String message;
ShowProgress(this.message);
}
class HideProgress extends CheckoutEffect {}
class ShowSuccess extends CheckoutEffect {
final String orderId;
ShowSuccess(this.orderId);
}
class SendReceipt extends CheckoutEffect {
final String email;
SendReceipt(this.email);
}
class NavigateToOrder extends CheckoutEffect {
final String orderId;
NavigateToOrder(this.orderId);
}
// Widget
class CheckoutScreen extends StatefulWidget {
@override
State<CheckoutScreen> createState() => _CheckoutScreenState();
}
class _CheckoutScreenState extends State<CheckoutScreen> {
OverlayEntry? _progressOverlay;
void _showProgress(String message) {
_progressOverlay = OverlayEntry(
builder: (_) => Material(
color: Colors.black54,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text(message, style: TextStyle(color: Colors.white)),
],
),
),
),
);
Overlay.of(context).insert(_progressOverlay!);
}
void _hideProgress() {
_progressOverlay?.remove();
_progressOverlay = null;
}
@override
Widget build(BuildContext context) {
context.effectQueue<CheckoutCubit, CheckoutEffect>(
(c) => c.state.effectQueue,
onePerFrame: true,
(context, effect) => switch (effect) {
ShowProgress(:final message) => _showProgress(message),
HideProgress() => _hideProgress(),
ShowSuccess(:final orderId) =>
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Order $orderId placed successfully!'),
backgroundColor: Colors.green,
),
),
SendReceipt(:final email) =>
emailService.sendReceipt(email),
NavigateToOrder(:final orderId) =>
Navigator.pushReplacementNamed(context, '/order/$orderId'),
},
);
return Scaffold(
appBar: AppBar(title: Text('Checkout')),
body: CheckoutForm(),
);
}
}
Ask the user:
下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer