Essentials

Core Concepts

Learn the fundamental concepts of Shard state management.

Shard

Shard<T> is a generic shard that holds typed state. It extends ChangeNotifier and provides state management with built-in lifecycle management and mixin support.

Creating a Shard

class CounterShard extends Shard<int> {
  CounterShard() : super(0);
  
  void increment() {
    emit(state + 1);
  }
  
  void decrement() {
    emit(state - 1);
  }
}

State Updates

Use emit() to update state. This will notify all listeners:

void updateValue(int newValue) {
  emit(newValue);
}

State Equality Check

By default, emit() performs an equality check before updating state. If the new state equals the current state, no notification is triggered. This prevents unnecessary widget rebuilds and improves performance.

class CounterShard extends Shard<int> {
  CounterShard() : super(0);

  void increment() => emit(state + 1);  // Notifies listeners
  void emitSame() => emit(state);       // Skipped - no rebuild
}

Custom Equality

Override stateEquals() to provide custom equality logic for complex state objects:

class UserShard extends Shard<UserState> {
  UserShard() : super(UserState.empty());

  @override
  bool stateEquals(UserState a, UserState b) {
    // Only compare specific fields
    return a.id == b.id && a.name == b.name;
  }
}

Deep Equality for Collections

Dart's == on List, Set, and Map is identity-based, so rebuilding a collection with the same contents would still notify listeners. The top-level deepEquals function compares by contents instead—recursively for nested List/Set/Map/Iterable, falling back to == for scalars. The DeepEqualityMixin wires deepEquals into stateEquals, so emit() skips no-op rebuilds when collection state has identical contents.

// Structural equality for collections (List/Set/Map/Iterable, recursive):
deepEquals([1, 2, 3], [1, 2, 3]); // true
deepEquals({'a': 1}, {'a': 2});   // false
// Make emit() dedupe collection state by contents instead of identity:
class TagsShard extends Shard<List<String>>
    with DeepEqualityMixin<List<String>> {
  TagsShard() : super(const []);
  void setTags(List<String> tags) => emit(tags); // identical contents -> no rebuild
}

You can also call deepEquals directly inside a buildWhen/listenWhen predicate or a ShardSelector.

Force Emit

Use emitForce() to bypass equality check and always notify listeners:

void forceRefresh() {
  emitForce(state);  // Always notifies, even if state is same
}
emitForce() is useful when your state object is mutable and has been modified in place, or when you need to force a UI refresh.

Lifecycle

Override onInit() to perform initialization when the shard is created, and dispose() to clean up resources when the shard is disposed:

class TimerShard extends Shard<int> {
  Timer? _timer;
  
  TimerShard() : super(0);
  
  @override
  void onInit() {
    super.onInit();
    _timer = Timer.periodic(Duration(seconds: 1), (timer) {
      emit(state + 1);
    });
  }
  
  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }
}

Observer Methods

Shard provides built-in observer methods that you can override to handle state changes and errors:

class CounterShard extends Shard<int> {
  CounterShard() : super(0);

  @override
  void onChange(int previousState, int currentState) {
    print('State: $previousState$currentState');
    super.onChange(previousState, currentState);
  }

  @override
  void onError(Object error, StackTrace? stackTrace) {
    print('Error: $error');
    super.onError(error, stackTrace);
  }

  void increment() => emit(state + 1);
}

Use addError to report errors without stopping execution:

void riskyOperation() {
  try {
    // risky code
  } catch (e, st) {
    addError(e, st);  // Reports error, doesn't block
  }
  emit(state + 1);  // Still executes
}

Learn more about Observing a Shard for advanced observer patterns and global error handling.

Additional Helpers

Shard includes debounce and throttle mixins to reduce unnecessary work. Debounce runs a callback after a period of inactivity (e.g. search as the user stops typing). Throttle runs at most once per time window (e.g. load-more on scroll). FutureShard has built-in response caching—see Response Caching for details.

Debounce example: delay search until the user pauses typing.

debounce('search', () => performSearch(query), duration: const Duration(milliseconds: 500));

Throttle example: limit how often load-more can run.

throttle('loadMore', () => _performLoadMore(), duration: const Duration(seconds: 1));

You'll see debounce in the Todo App and throttle in the Infinite Scroll example.

State as a Stream

Every shard exposes a stream getter: a broadcast Stream<T> of state changes. It does not replay the current value to new listeners—read state for that. The stream is created lazily on first access and closed automatically when the shard is disposed. It works well with StreamBuilder, await for, stream combinators, and emitsInOrder in tests.

// A broadcast stream of state changes. Does NOT replay the current value
// (read `state` for that). Created lazily; closes on dispose.
final sub = counter.stream.listen((value) => print('changed: $value'));

// Works with StreamBuilder, `await for`, and emitsInOrder in tests:
await expectLater(counter.stream, emitsInOrder([1, 2, 3]));

Next Steps

Next: Widgets to integrate shards with Flutter widgets.