Shard provides an observer pattern that allows you to monitor state changes, errors, and lifecycle events across all Shard instances in your application. This is useful for:
ShardObserver is an abstract class that you can extend to receive notifications about Shard events.
LoggingObserver is a ready-made ShardObserver that logs state changes and errors for you — no subclass required. It's the easiest way to get observability, so reach for it first. By default it's inert in release builds, so you can leave it wired up year-round:
void main() {
// Ready-made observer: logs changes/errors to DevTools (dart:developer).
// Inert in release builds by default (enabled defaults to kDebugMode).
Shard.observer = LoggingObserver();
runApp(MyApp());
}
Every option is configurable through named parameters:
Shard.observer = LoggingObserver(
logChanges: true,
logErrors: true,
includeStackTrace: false,
shouldLog: (shard) => shard is! NoisyShard, // filter specific shards
printer: (message) => Sentry.captureMessage(message), // custom sink
);
Options:
enabled — when null (the default), it falls back to kDebugMode, so the observer is active during development and completely inert in release builds (no formatting cost, no PII leak). Pass true to force it on everywhere, or false to silence it everywhere.logChanges (default true) — log onChange events.logErrors (default true) — log onError events.includeStackTrace (default false) — append the stack trace to error log lines.shouldLog — optional predicate bool Function(Shard shard); return false to skip a given shard.printer — optional custom sink void Function(String message). When null, it uses dart:developer.log(name: 'shard'), which shows up in the Flutter DevTools Logging tab.enabled defaults to kDebugMode, you can wire up LoggingObserver() once in main() and leave it there — it produces no output and incurs no cost in release builds.For full control — analytics, crash reporting, custom routing — implement your own ShardObserver:
class AnalyticsObserver extends ShardObserver {
@override
void onChange<T>(Shard<T> shard, T previousState, T currentState) {
print('${shard.runtimeType}: $previousState → $currentState');
}
@override
void onError<T>(Shard<T> shard, Object error, StackTrace? stackTrace) {
print('${shard.runtimeType} error: $error');
}
}
Set the observer via Shard.observer:
void main() {
// Set global observer (usually in main.dart)
Shard.observer = AnalyticsObserver();
runApp(MyApp());
}
You can also override observer methods directly in your Shard subclass for instance-specific handling:
class CounterShard extends Shard<int> {
CounterShard() : super(0);
@override
void onChange(int previousState, int currentState) {
// Local handling
print('Counter changed: $previousState → $currentState');
// IMPORTANT: Call super to notify global observer
super.onChange(previousState, currentState);
}
@override
void onError(Object error, StackTrace? stackTrace) {
// Local error handling
print('Counter error: $error');
// IMPORTANT: Call super to notify global observer
super.onError(error, stackTrace);
}
@override
void dispose() {
// Cleanup resources if needed
print('Counter disposed');
super.dispose();
}
void increment() => emit(state + 1);
}
super.onChange() and super.onError() when overriding these methods to ensure the global observer receives notifications.The addError method allows you to report errors without blocking the execution flow. This is useful for non-fatal errors that should be logged but shouldn't stop the operation:
class DataShard extends Shard<DataState> {
DataShard() : super(DataState.initial());
Future<void> fetchData() async {
emit(state.copyWith(isLoading: true));
try {
final data = await api.fetchData();
emit(state.copyWith(isLoading: false, data: data));
} catch (e, st) {
// Report error to observer (non-blocking)
addError(e, st);
// Continue with fallback behavior
emit(state.copyWith(isLoading: false, error: e.toString()));
}
}
void riskyOperation() {
// Report error but continue execution
addError(Exception('Something went wrong'), StackTrace.current);
// This still executes
emit(state.copyWith(warningShown: true));
}
}
Next: ShardLocator for registering and resolving singletons. See also: Future Shard, Examples.