Essentials

Observing a Shard

Monitor and debug Shard state changes, errors, and lifecycle events.

Overview

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:

  • Debugging: Log all state transitions during development
  • Analytics: Track user interactions and state changes
  • Error Monitoring: Capture and report errors to crash reporting services
  • Testing: Verify state changes in tests

ShardObserver

ShardObserver is an abstract class that you can extend to receive notifications about Shard events.

LoggingObserver

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.
Because 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.

Creating an Observer

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');
  }
}

Setting the Global Observer

Set the observer via Shard.observer:

void main() {
  // Set global observer (usually in main.dart)
  Shard.observer = AnalyticsObserver();
  
  runApp(MyApp());
}

Instance-Level Overrides

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);
}
Always call super.onChange() and super.onError() when overriding these methods to ensure the global observer receives notifications.

Error Reporting with addError

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 Steps

Next: ShardLocator for registering and resolving singletons. See also: Future Shard, Examples.