with one click
add-effect-queue
// Add EffectQueue to state for triggering multiple sequential one-time UI effects
// Add EffectQueue to state for triggering multiple sequential one-time UI effects
Add custom error handling with catchError to a mix() call for logging, error conversion, or suppression
Add internet connectivity checking before executing a Cubit method with optional retry
Add debounce to delay method execution until after a period of inactivity for search or validation
Add Effect fields to state class for one-time UI notifications like snackbars, navigation, or form clearing
Add error state handling to widgets using context.isFailed() and context.getException()
Add freshness caching to prevent redundant method executions within a time period
| name | add-effect-queue |
| description | Add EffectQueue to state for triggering multiple sequential one-time UI effects |
This skill adds an EffectQueue field to a state class for sequenced one-time UI effects.
Adds EffectQueue<T> to state for triggering multiple side effects in order:
This pattern eliminates the need for BlocListener. The Cubit declares what should happen
(the effects), while the widget determines how to execute them (the UI logic).
Use EffectQueue when:
Use simple Effect<T> when:
Create a sealed class hierarchy for your effects:
sealed class UiEffect {}
class ShowToast extends UiEffect {
final String message;
ShowToast(this.message);
}
class ShowDialog extends UiEffect {
final String title;
final String content;
ShowDialog(this.title, this.content);
}
class Navigate extends UiEffect {
final String route;
Navigate(this.route);
}
class ClearForm extends UiEffect {}
Add an EffectQueue<UiEffect> field. Always initialize as spent:
import 'package:bloc_superpowers/bloc_superpowers.dart';
class AppState {
final User? user;
final EffectQueue<UiEffect> effectQueue;
AppState({
this.user,
EffectQueue<UiEffect>? effectQueue,
}) : effectQueue = effectQueue ?? EffectQueue.spent();
AppState copyWith({
User? user,
EffectQueue<UiEffect>? effectQueue,
}) {
return AppState(
user: user ?? this.user,
effectQueue: effectQueue ?? this.effectQueue,
);
}
}
Create an EffectQueue with a list of effects and a callback for remaining effects:
class AppCubit extends Cubit<AppState> {
AppCubit() : super(AppState());
void onPurchaseComplete() {
emit(state.copyWith(
effectQueue: EffectQueue<UiEffect>(
[
ShowToast('Purchase successful!'),
ShowDialog('Thank You', 'Your order has been placed.'),
Navigate('/orders'),
],
// Callback to emit remaining effects
(remaining) => emit(state.copyWith(effectQueue: remaining)),
),
));
}
}
Use context.effectQueue() in the build method:
class AppScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
context.effectQueue<AppCubit, UiEffect>(
// Select the queue
(cubit) => cubit.state.effectQueue,
// Handle each effect
(context, effect) => switch (effect) {
ShowToast(:final message) =>
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
),
ShowDialog(:final title, :final content) =>
showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text(title),
content: Text(content),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
),
Navigate(:final route) =>
Navigator.of(context).pushNamed(route),
ClearForm() =>
_formKey.currentState?.reset(),
},
);
return Scaffold(
body: MyContent(),
);
}
}
Effects execute one at a time, with a rebuild between each:
context.effectQueue<AppCubit, UiEffect>(
(cubit) => cubit.state.effectQueue,
onePerFrame: true, // Default
(context, effect) => ...,
);
All effects execute in a single frame:
context.effectQueue<AppCubit, UiEffect>(
(cubit) => cubit.state.effectQueue,
onePerFrame: false, // Execute all immediately
(context, effect) => ...,
);
sealed class OnboardingEffect {}
class ShowWelcome extends OnboardingEffect {}
class RequestPermissions extends OnboardingEffect {}
class ShowTutorial extends OnboardingEffect {}
class NavigateToHome extends OnboardingEffect {}
void startOnboarding() {
emit(state.copyWith(
effectQueue: EffectQueue<OnboardingEffect>(
[
ShowWelcome(),
RequestPermissions(),
ShowTutorial(),
NavigateToHome(),
],
(remaining) => emit(state.copyWith(effectQueue: remaining)),
),
));
}
sealed class FormEffect {}
class ShowSaving extends FormEffect {}
class ShowSuccess extends FormEffect {
final String message;
ShowSuccess(this.message);
}
class ClearForm extends FormEffect {}
class NavigateBack extends FormEffect {}
void submitForm(FormData data) => mix(
key: this,
() async {
await api.submit(data);
emit(state.copyWith(
effectQueue: EffectQueue<FormEffect>(
[
ShowSuccess('Form submitted!'),
ClearForm(),
NavigateBack(),
],
(remaining) => emit(state.copyWith(effectQueue: remaining)),
),
));
},
);
sealed class ErrorEffect {}
class ShowError extends ErrorEffect {
final String message;
ShowError(this.message);
}
class OfferRetry extends ErrorEffect {
final VoidCallback onRetry;
OfferRetry(this.onRetry);
}
class LogError extends ErrorEffect {
final Object error;
LogError(this.error);
}
void onError(Object error) {
emit(state.copyWith(
effectQueue: EffectQueue<ErrorEffect>(
[
LogError(error),
ShowError('Something went wrong'),
OfferRetry(() => loadData()),
],
(remaining) => emit(state.copyWith(effectQueue: remaining)),
),
));
}
// Effects
sealed class CheckoutEffect {}
class ShowProcessing extends CheckoutEffect {}
class ShowSuccess extends CheckoutEffect {
final String orderId;
ShowSuccess(this.orderId);
}
class SendConfirmationEmail extends CheckoutEffect {
final String email;
SendConfirmationEmail(this.email);
}
class NavigateToOrder extends CheckoutEffect {
final String orderId;
NavigateToOrder(this.orderId);
}
// State
class CheckoutState {
final Cart cart;
final EffectQueue<CheckoutEffect> effectQueue;
CheckoutState({
required this.cart,
EffectQueue<CheckoutEffect>? effectQueue,
}) : effectQueue = effectQueue ?? EffectQueue.spent();
CheckoutState copyWith({
Cart? cart,
EffectQueue<CheckoutEffect>? effectQueue,
}) => CheckoutState(
cart: cart ?? this.cart,
effectQueue: effectQueue ?? this.effectQueue,
);
}
// Cubit
class CheckoutCubit extends Cubit<CheckoutState> {
CheckoutCubit(Cart cart) : super(CheckoutState(cart: cart));
void placeOrder(String email) => mix(
key: this,
() async {
final order = await api.placeOrder(state.cart);
emit(state.copyWith(
cart: Cart.empty(),
effectQueue: EffectQueue<CheckoutEffect>(
[
ShowSuccess(order.id),
SendConfirmationEmail(email),
NavigateToOrder(order.id),
],
(remaining) => emit(state.copyWith(effectQueue: remaining)),
),
));
},
);
}
// Widget
class CheckoutScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
context.effectQueue<CheckoutCubit, CheckoutEffect>(
(cubit) => cubit.state.effectQueue,
onePerFrame: true,
(context, effect) => switch (effect) {
ShowProcessing() =>
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => const AlertDialog(
content: CircularProgressIndicator(),
),
),
ShowSuccess(:final orderId) =>
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Order $orderId placed!')),
),
SendConfirmationEmail(:final email) =>
emailService.sendConfirmation(email),
NavigateToOrder(:final orderId) =>
Navigator.pushReplacementNamed(
context,
'/order/$orderId',
),
},
);
return CheckoutForm();
}
}
Ask the user: