一键导入
add-effect
// Add Effect fields to state class for one-time UI notifications like snackbars, navigation, or form clearing
// Add Effect fields to state class for one-time UI notifications like snackbars, navigation, or form clearing
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 EffectQueue to state for triggering multiple sequential one-time UI effects
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 |
| description | Add Effect fields to state class for one-time UI notifications like snackbars, navigation, or form clearing |
This skill adds an Effect field to a state class for one-time UI notifications.
Adds Effect<T> fields to state for triggering one-time side effects like:
Effects are automatically consumed after being read, ensuring they trigger only once.
Ask the user what one-time action they need:
Add an Effect<T> field to the state class. Always initialize as spent:
import 'package:bloc_superpowers/bloc_superpowers.dart';
class UserState {
final User? user;
final Effect<String> messageEffect; // For showing messages
final Effect<bool> clearFormEffect; // For clearing forms
UserState({
this.user,
Effect<String>? messageEffect,
Effect<bool>? clearFormEffect,
}) : messageEffect = messageEffect ?? Effect.spent(),
clearFormEffect = clearFormEffect ?? Effect.spent();
UserState copyWith({
User? user,
Effect<String>? messageEffect,
Effect<bool>? clearFormEffect,
}) {
return UserState(
user: user ?? this.user,
messageEffect: messageEffect ?? this.messageEffect,
clearFormEffect: clearFormEffect ?? this.clearFormEffect,
);
}
}
Create new effects using Effect(value):
class UserCubit extends Cubit<UserState> {
UserCubit() : super(UserState());
void saveUser(User user) => mix(
key: this,
() async {
await api.saveUser(user);
emit(state.copyWith(
user: user,
messageEffect: Effect('User saved successfully!'),
));
},
);
void clearForm() {
emit(state.copyWith(
clearFormEffect: Effect(true),
));
}
}
Use context.effect() in the build method:
class UserScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Consume message effect
final message = context.effect((UserCubit c) => c.state.messageEffect);
if (message != null) {
// Show snackbar after build completes
WidgetsBinding.instance.addPostFrameCallback((_) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
});
}
// Consume clear form effect
final shouldClear = context.effect((UserCubit c) => c.state.clearFormEffect);
if (shouldClear == true) {
_formKey.currentState?.reset();
}
return Scaffold(
body: UserForm(key: _formKey),
);
}
}
For simple triggers without data:
// State
final Effect clearEffect;
// Initialize
clearEffect = clearEffect ?? Effect.spent();
// Emit
emit(state.copyWith(clearEffect: Effect()));
// Consume - returns true if new, false if spent
final shouldClear = context.effect((MyCubit c) => c.state.clearEffect);
if (shouldClear) {
// Do something
}
For effects that carry data:
// State
final Effect<String> messageEffect;
final Effect<String> navigateEffect;
final Effect<int> scrollToEffect;
// Emit
emit(state.copyWith(messageEffect: Effect('Hello!')));
emit(state.copyWith(navigateEffect: Effect('/profile')));
emit(state.copyWith(scrollToEffect: Effect(42)));
// Consume - returns value if new, null if spent
final message = context.effect((MyCubit c) => c.state.messageEffect);
if (message != null) showMessage(message);
final route = context.effect((MyCubit c) => c.state.navigateEffect);
if (route != null) Navigator.pushNamed(context, route);
final index = context.effect((MyCubit c) => c.state.scrollToEffect);
if (index != null) scrollController.jumpTo(index.toDouble());
// State
final Effect<String> snackbarEffect;
// Cubit
void showSuccess() {
emit(state.copyWith(snackbarEffect: Effect('Operation successful!')));
}
// Widget
final message = context.effect((MyCubit c) => c.state.snackbarEffect);
if (message != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
});
}
// State
final Effect<String> navigateEffect;
// Cubit
void onLoginSuccess() {
emit(state.copyWith(navigateEffect: Effect('/home')));
}
// Widget
final route = context.effect((MyCubit c) => c.state.navigateEffect);
if (route != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pushReplacementNamed(context, route);
});
}
// State
final Effect<bool> clearTextEffect;
// Cubit
void onMessageSent() {
emit(state.copyWith(clearTextEffect: Effect(true)));
}
// Widget
final shouldClear = context.effect((ChatCubit c) => c.state.clearTextEffect);
if (shouldClear == true) {
_textController.clear();
}
// State
final Effect<String> setTextEffect;
// Cubit
void loadDraft(String text) {
emit(state.copyWith(setTextEffect: Effect(text)));
}
// Widget
final text = context.effect((FormCubit c) => c.state.setTextEffect);
if (text != null) {
_textController.text = text;
}
Always initialize as spent:
messageEffect = messageEffect ?? Effect.spent();
One consumer per effect: Only one widget should consume each effect
Effects are consumed once: After reading, the effect becomes "spent"
Use addPostFrameCallback for dialogs/navigation:
if (effect != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
// Show dialog, navigate, etc.
});
}
// Create a new effect
Effect() // Untyped
Effect(value) // Typed
// Create a spent effect
Effect.spent() // Untyped
Effect<T>.spent() // Typed
// Check state without consuming
effect.isSpent // true if already consumed
effect.isNotSpent // true if available
effect.state // Get value without consuming
// Consume (marks as spent)
effect.consume() // Returns value (or true/null)
Effects use custom equality to ensure proper rebuild behavior:
This is why emitting Effect('hello') twice triggers two separate rebuilds—each new Effect
instance is unique until consumed.
// State
class ChatState {
final List<Message> messages;
final Effect<bool> clearInputEffect;
final Effect<String> errorEffect;
final Effect<int> scrollToMessageEffect;
ChatState({
this.messages = const [],
Effect<bool>? clearInputEffect,
Effect<String>? errorEffect,
Effect<int>? scrollToMessageEffect,
}) : clearInputEffect = clearInputEffect ?? Effect.spent(),
errorEffect = errorEffect ?? Effect.spent(),
scrollToMessageEffect = scrollToMessageEffect ?? Effect.spent();
ChatState copyWith({
List<Message>? messages,
Effect<bool>? clearInputEffect,
Effect<String>? errorEffect,
Effect<int>? scrollToMessageEffect,
}) => ChatState(
messages: messages ?? this.messages,
clearInputEffect: clearInputEffect ?? this.clearInputEffect,
errorEffect: errorEffect ?? this.errorEffect,
scrollToMessageEffect: scrollToMessageEffect ?? this.scrollToMessageEffect,
);
}
// Cubit
class ChatCubit extends Cubit<ChatState> {
ChatCubit() : super(ChatState());
void sendMessage(String text) => mix(
key: this,
() async {
final message = await api.sendMessage(text);
emit(state.copyWith(
messages: [...state.messages, message],
clearInputEffect: Effect(true),
scrollToMessageEffect: Effect(state.messages.length),
));
},
);
}
// Widget
class ChatScreen extends StatelessWidget {
final _controller = TextEditingController();
final _scrollController = ScrollController();
@override
Widget build(BuildContext context) {
// Clear input after sending
final shouldClear = context.effect((ChatCubit c) => c.state.clearInputEffect);
if (shouldClear == true) {
_controller.clear();
}
// Scroll to new message
final scrollTo = context.effect((ChatCubit c) => c.state.scrollToMessageEffect);
if (scrollTo != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
});
}
// Show error
final error = context.effect((ChatCubit c) => c.state.errorEffect);
if (error != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(error), backgroundColor: Colors.red),
);
});
}
return Scaffold(
body: Column(
children: [
Expanded(child: MessageList(controller: _scrollController)),
MessageInput(controller: _controller),
],
),
);
}
}
Ask the user: