Set up observers for debugging and monitoring...
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 |
GlobalErrorObserver |
Monitor, transform, and handle action errors |
ModelObserver |
Monitor widget rebuilds (for StoreConnector) |
var store = Store<AppState>(
initialState: AppState.initialState(),
actionObservers: [ConsoleActionObserver()],
stateObservers: [MyStateObserver()],
globalErrorObserver: (store) => MyGlobalErrorObserver(),
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 GlobalErrorObserver monitors all errors thrown by actions. It can log them, transform them (for example, into UserExceptions), or suppress them.
The error handling order is:
wrapError() (action-level)GlobalErrorObserver (app-level transformation and monitoring/logging)After that, UserExceptions go to the error queue (shown by UserExceptionDialog) and are not thrown, while other errors are thrown.
Extend GlobalErrorObserver and override observe(). Inside it you can access error (after wrapError), originalError (before wrapError), stackTrace, action, and store. Do not use store to dispatch actions.
class MyGlobalErrorObserver extends GlobalErrorObserver<AppState> {
@override
Object? observe() {
// Transform platform errors to user-friendly messages
if (error is PlatformException) {
return UserException('Check your internet connection').addCause(error);
}
// Log unexpected errors and send them to a crash reporting service.
// Note `action` is null when the error came from the Persistor.
if (error is! UserException) {
var source = action?.runtimeType.toString() ?? 'persistence';
print('Error in $source: $error');
print(stackTrace);
crashReporter.recordError(error, stackTrace, reason: source);
}
// Return the error to keep it, or null to suppress it
return error;
}
}
var store = Store<AppState>(
initialState: AppState.initialState(),
globalErrorObserver: (store) => MyGlobalErrorObserver(),
);
AsyncRedux also provides GlobalErrorObserverDummy (does nothing), GlobalErrorObserverForDevelopment (also shows non-UserException errors in the dialog), and SwallowGlobalErrorObserver (swallows all errors, not recommended).
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 extends GlobalErrorObserver<AppState> {
@override
Object? observe() {
// Transform errors globally
if (error is PlatformException) {
return UserException('Check your internet connection').addCause(error);
}
// Don't report UserExceptions (they're expected)
if (error is! UserException) {
FirebaseCrashlytics.instance.recordError(error, stackTrace);
}
return error; // Keep 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 (also transforms errors globally)
globalErrorObserver: (store) => CrashReportingErrorObserver(),
);
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: