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.
class CounterShard extends Shard<int> {
CounterShard() : super(0);
void increment() {
emit(state + 1);
}
void decrement() {
emit(state - 1);
}
}
Use emit() to update state. This will notify all listeners:
void updateValue(int newValue) {
emit(newValue);
}
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
}
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;
}
}
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.
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.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();
}
}
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.
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.
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: Widgets to integrate shards with Flutter widgets.