Set up observers for debugging and monitoring. Covers implementing actionObservers for dispatch logging, stateObserver for state change tracking, combining observers with globalWrapError, and using observers for analytics.
AsyncRedux provides several observer types for monitoring actions, state changes, errors, and widget rebuilds. These observers are configured when creating the Store.
| Observer Type | Purpose |
|---|---|
ActionObserver | Monitor action dispatch (start and end) |
StateObserver | Monitor state changes after actions |
ErrorObserver | Monitor and handle action errors |
ModelObserver | Monitor widget rebuilds (for StoreConnector) |
var store = Store<AppState>(
initialState: AppState.initialState(),
actionObservers: [ConsoleActionObserver()],
stateObservers: [MyStateObserver()],
errorObserver: MyErrorObserver(),
modelObserver: DefaultModelObserver(),
);
The ActionObserver monitors when actions are dispatched and when they complete. It triggers twice per action: at the start (INI) and at the end (END).
abstract class ActionObserver<St> {
void observe(
ReduxAction<St> action,
int dispatchCount, {
required bool ini,
});
}
action: The dispatched action instancedispatchCount: Sequential number of this dispatchini: true when action starts (INI phase), false when it ends (END phase)INI Phase: Action dispatch begins. The reducer hasn't modified state yet. Sync reducers may complete during this phase; async reducers start their async process.
END Phase: The reducer has finished and returned the new state. State modifications are now observable.
Important: Receiving an END observation does not guarantee all effects have finished. Async operations that were not awaited may continue running and dispatch additional actions later.
AsyncRedux provides ConsoleActionObserver for development debugging:
var store = Store<AppState>(
initialState: AppState.initialState(),
actionObservers: kReleaseMode ? null : [ConsoleActionObserver()],
);
This prints actions in yellow to the console. Override toString() in your actions to display additional information:
class LoadUserAction extends AppAction {
final String username;
LoadUserAction(this.username);
@override
Future<AppState?> reduce() async {
// ...
}
@override
String toString() => 'LoadUserAction(username: $username)';
}
var store = Store<AppState>(
initialState: AppState.initialState(),
actionObservers: [Log.printer(formatter: Log.verySimpleFormatter)],
);
class MyActionObserver implements ActionObserver<AppState> {
@override
void observe(
ReduxAction<AppState> action,
int dispatchCount, {
required bool ini,
}) {
final phase = ini ? 'START' : 'END';
print('[$phase] Action #$dispatchCount: ${action.runtimeType}');
}
}
The StateObserver is notified of all state changes, allowing you to track, log, or record state history.
abstract class StateObserver<St> {
void observe(
ReduxAction<St> action,
St prevState,
St newState,
Object? error,
int dispatchCount,
);
}
action: The action that triggered the changeprevState: State before the reducer executednewState: State returned by the reducererror: Null if successful; contains the thrown error otherwisedispatchCount: Sequential dispatch numberCompare states using identical() to detect actual changes:
bool stateChanged = !identical(prevState, newState);
class StateLogger implements StateObserver<AppState> {
@override
void observe(
ReduxAction<AppState> action,
AppState prevState,
AppState newState,
Object? error,
int dispatchCount,
) {
final changed = !identical(prevState, newState);
print('Action #$dispatchCount: ${action.runtimeType}');
print(' State changed: $changed');
if (error != null) {
print(' Error: $error');
}
}
}
A common use case is recording state history for undo/redo functionality:
class UndoRedoObserver implements StateObserver<AppState> {
final List<AppState> _history = [];
int _currentIndex = -1;
final int maxHistorySize;
UndoRedoObserver({this.maxHistorySize = 50});
bool get canUndo => _currentIndex > 0;
bool get canRedo => _currentIndex < _history.length - 1;
@override
void observe(
ReduxAction<AppState> action,
AppState prevState,
AppState newState,
Object? error,
int dispatchCount,
) {
// Skip undo/redo actions to avoid recording navigation
if (action is UndoAction || action is RedoAction) return;
// Skip if state didn't change
if (identical(prevState, newState)) return;
// Remove "future" states if we're navigating
if (_currentIndex < _history.length - 1) {
_history.removeRange(_currentIndex + 1, _history.length);
}
// Add new state
_history.add(newState);
_currentIndex = _history.length - 1;
// Enforce max history size
if (_history.length > maxHistorySize) {
_history.removeAt(0);
_currentIndex--;
}
}
AppState? getPreviousState() {
if (!canUndo) return null;
_currentIndex--;
return _history[_currentIndex];
}
AppState? getNextState() {
if (!canRedo) return null;
_currentIndex++;
return _history[_currentIndex];
}
}
The ErrorObserver monitors all errors thrown by actions and can suppress or allow them to propagate.
The error handling order is:
wrapError() (action-level)GlobalWrapError (app-level)ErrorObserver (monitoring/logging)class MyErrorObserver<St> implements ErrorObserver<St> {
@override
bool observe(
Object error,
StackTrace stackTrace,
ReduxAction<St> action,
Store store,
) {
// Log the error
print('Error in ${action.runtimeType}: $error');
print(stackTrace);
// Send to crash reporting service
crashReporter.recordError(error, stackTrace, reason: action.runtimeType.toString());
// Return true to rethrow the error, false to suppress it
return true;
}
}
var store = Store<AppState>(
initialState: AppState.initialState(),
errorObserver: MyErrorObserver<AppState>(),
);
Use GlobalWrapError to transform errors before they reach the ErrorObserver:
var store = Store<AppState>(
initialState: AppState.initialState(),
globalWrapError: MyGlobalWrapError(),
errorObserver: MyErrorObserver<AppState>(),
);
class MyGlobalWrapError extends GlobalWrapError {
@override
Object? wrap(Object error, StackTrace stackTrace, ReduxAction<dynamic> action) {
// Transform platform errors to user-friendly messages
if (error is PlatformException) {
return UserException('Check your internet connection').addCause(error);
}
return error;
}
}
The ModelObserver monitors widget rebuilds when using StoreConnector. This is useful for debugging rebuild behavior and ensuring efficient state updates.
var store = Store<AppState>(
initialState: AppState.initialState(),
modelObserver: DefaultModelObserver(),
);
DefaultModelObserver prints rebuild information:
Model D:1 R:1 = Rebuild:true, Connector:MyWidgetConnector, Model:MyViewModel{B}.
Model D:2 R:2 = Rebuild:false, Connector:MyWidgetConnector, Model:MyViewModel{B}.
Model D:3 R:3 = Rebuild:true, Connector:MyWidgetConnector, Model:MyViewModel{C}.
D: Dispatch countR: Rebuild countRebuild: Whether the widget actually rebuiltConnector: The StoreConnector typeModel: The ViewModel with current statePass debug: this to StoreConnector to enable connector type printing:
class MyWidgetConnector extends StatelessWidget with StoreConnector<AppState, MyViewModel> {
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, MyViewModel>(
debug: this, // Enable for ModelObserver output
converter: (store) => MyViewModel.fromStore(store),
builder: (context, vm) => MyWidget(vm),
);
}
}
Override ViewModel.toString() for custom diagnostic information.
Create a metrics observer that delegates to action-specific tracking methods:
abstract class AppAction extends ReduxAction<AppState> {
/// Override in specific actions to track metrics
void trackEvent(MetricsService metrics) {}
}
class MetricsObserver implements StateObserver<AppState> {
final MetricsService metrics;
MetricsObserver(this.metrics);
@override
void observe(
ReduxAction<AppState> action,
AppState prevState,
AppState newState,
Object? error,
int dispatchCount,
) {
if (action is AppAction) {
action.trackEvent(metrics);
}
}
}
Then override trackEvent in specific actions:
class PurchaseAction extends AppAction {
final Product product;
PurchaseAction(this.product);
@override
Future<AppState?> reduce() async {
await purchaseService.buy(product);
return state.copy(purchases: state.purchases.add(product));
}
@override
void trackEvent(MetricsService metrics) {
metrics.trackPurchase(productId: product.id, price: product.price);
}
}
Track all dispatched actions for analytics:
class AnalyticsObserver implements ActionObserver<AppState> {
final AnalyticsService analytics;
AnalyticsObserver(this.analytics);
@override
void observe(
ReduxAction<AppState> action,
int dispatchCount, {
required bool ini,
}) {
// Only track at start (ini) to avoid double-counting
if (ini) {
analytics.trackEvent(
'action_dispatched',
parameters: {'action_type': action.runtimeType.toString()},
);
}
}
}
// observers.dart
class ConsoleStateObserver implements StateObserver<AppState> {
@override
void observe(
ReduxAction<AppState> action,
AppState prevState,
AppState newState,
Object? error,
int dispatchCount,
) {
final changed = !identical(prevState, newState);
print('[$dispatchCount] ${action.runtimeType} - Changed: $changed');
if (error != null) print(' Error: $error');
}
}
class CrashReportingErrorObserver implements ErrorObserver<AppState> {
@override
bool observe(Object error, StackTrace stackTrace, ReduxAction<AppState> action, Store store) {
// Don't report UserExceptions (they're expected)
if (error is! UserException) {
FirebaseCrashlytics.instance.recordError(error, stackTrace);
}
return true; // Rethrow the error
}
}
// main.dart
void main() {
final store = Store<AppState>(
initialState: AppState.initialState(),
// Only enable console observers in debug mode
actionObservers: kDebugMode ? [ConsoleActionObserver()] : null,
stateObservers: kDebugMode ? [ConsoleStateObserver()] : null,
// Always enable error observer
errorObserver: CrashReportingErrorObserver(),
// Transform errors globally
globalWrapError: MyGlobalWrapError(),
);
runApp(StoreProvider<AppState>(
store: store,
child: MyApp(),
));
}
You can use multiple observers of the same type:
var store = Store<AppState>(
initialState: AppState.initialState(),
actionObservers: [
ConsoleActionObserver(),
AnalyticsObserver(analyticsService),
PerformanceObserver(),
],
stateObservers: [
StateLogger(),
UndoRedoObserver(),
MetricsObserver(metricsService),
],
);
All observers will be notified in the order they are listed.
URLs from the documentation: