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:
下载完整 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