Use the Event class to interact with Flutter's stateful widgets (TextField, ListView, etc.)...
Events are single-use notifications used to trigger side effects in widgets. They're designed for controlling native Flutter widgets like TextField and ListView that manage their own state through controllers.
Use events for:
Do NOT use events for:
Add the event method to your BuildContext extension:
extension BuildContextExtension on BuildContext {
AppState get state => getState<AppState>();
R select<R>(R Function(AppState state) selector) =>
getSelect<AppState, R>(selector);
// Add this for events:
R? event<R>(Evt<R> Function(AppState state) selector) =>
getEvent<AppState, R>(selector);
}
For simple triggers that don't carry data:
// Create an unspent event (will return true once)
var clearTextEvt = Evt();
// Create a spent event (will return false)
var clearTextEvt = Evt.spent();
For events that carry a value:
// Create an unspent event with a value (will return value once, then null)
var changeTextEvt = Evt<String>("New text");
var scrollToIndexEvt = Evt<int>(42);
// Create a spent event (will return null)
var changeTextEvt = Evt<String>.spent();
Initialize all events as spent in your initial state:
class AppState {
final Evt clearTextEvt;
final Evt<String> changeTextEvt;
final Evt<int> scrollToIndexEvt;
AppState({
required this.clearTextEvt,
required this.changeTextEvt,
required this.scrollToIndexEvt,
});
static AppState initialState() => AppState(
clearTextEvt: Evt.spent(),
changeTextEvt: Evt<String>.spent(),
scrollToIndexEvt: Evt<int>.spent(),
);
AppState copy({
Evt? clearTextEvt,
Evt<String>? changeTextEvt,
Evt<int>? scrollToIndexEvt,
}) => AppState(
clearTextEvt: clearTextEvt ?? this.clearTextEvt,
changeTextEvt: changeTextEvt ?? this.changeTextEvt,
scrollToIndexEvt: scrollToIndexEvt ?? this.scrollToIndexEvt,
);
}
Actions create unspent events and place them in state:
// Boolean event - triggers clearing the text field
class ClearTextAction extends AppAction {
AppState reduce() => state.copy(clearTextEvt: Evt());
}
// Typed event - changes the text field to a new value
class ChangeTextAction extends AppAction {
final String newText;
ChangeTextAction(this.newText);
AppState reduce() => state.copy(changeTextEvt: Evt<String>(newText));
}
// Typed event from async operation
class FetchAndSetTextAction extends AppAction {
Future<AppState> reduce() async {
String text = await api.fetchText();
return state.copy(changeTextEvt: Evt<String>(text));
}
}
// Scroll to a specific index in a ListView
class ScrollToItemAction extends AppAction {
final int index;
ScrollToItemAction(this.index);
AppState reduce() => state.copy(scrollToIndexEvt: Evt<int>(index));
}
Use context.event() in the widget's build method. The event is consumed (marked as spent) immediately when read.
class MyTextField extends StatefulWidget {
@override
State<MyTextField> createState() => _MyTextFieldState();
}
class _MyTextFieldState extends State<MyTextField> {
final controller = TextEditingController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// Consume the clear event - returns true once, then false
bool shouldClear = context.event((s) => s.clearTextEvt);
if (shouldClear) {
controller.clear();
}
// Consume the change event - returns the value once, then null
String? newText = context.event((s) => s.changeTextEvt);
if (newText != null) {
controller.text = newText;
}
return TextField(controller: controller);
}
}
class MyListView extends StatefulWidget {
@override
State<MyListView> createState() => _MyListViewState();
}
class _MyListViewState extends State<MyListView> {
final scrollController = ScrollController();
final itemHeight = 50.0;
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final items = context.select((s) => s.items);
// Consume the scroll event
int? scrollToIndex = context.event((s) => s.scrollToIndexEvt);
if (scrollToIndex != null) {
// Schedule the scroll after the frame is built
WidgetsBinding.instance.addPostFrameCallback((_) {
scrollController.animateTo(
scrollToIndex * itemHeight,
duration: Duration(milliseconds: 300),
curve: Curves.easeOut,
);
});
}
return ListView.builder(
controller: scrollController,
itemCount: items.length,
itemBuilder: (context, index) => SizedBox(
height: itemHeight,
child: Text(items[index]),
),
);
}
}
Evt.spent() in initial stateEvt() or Evt<T>(value) and puts it in statecontext.event() returns the value and marks the event as spentnull (typed) or false (boolean)If multiple widgets need the same trigger, create separate events:
class AppState {
final Evt clearSearchEvt; // For search field
final Evt clearCommentsEvt; // For comments field
// ...
}
Events are mutable and designed for one-time use. Never persist them to local storage.
Events have special equality methods that prevent unnecessary widget rebuilds when used correctly with the selector pattern.
Use these methods to check an event's status without consuming it:
// Check if an event has been consumed
bool consumed = myEvent.isSpent;
// Check if an event is ready to be consumed
bool ready = myEvent.isNotSpent;
// Get the underlying state without consuming
var eventState = myEvent.state;
Transform an event's value:
// Map an event to a different type
Evt<String> nameEvt = Evt<int>(42).map((value) => 'Item $value');
When you need to consume from multiple possible event sources:
// Create an event that consumes from first non-spent source
var combined = Event.from([event1, event2, event3]);
// Or use the static method
var value = Event.consumeFrom([event1, event2, event3]);
URLs from the documentation: