Common questions and fixes when working with Shard. If you hit something not covered here, open an issue on GitHub.
This assertion fires when you read a shard that isn't provided above the current widget. Three things to check:
MultiShardProvider) for that exact type is an ancestor of the widget calling context.read<X>(), ShardBuilder, or ShardSelector.context.read<CounterShard>() needs a ShardProvider<CounterShard>, not a base type.// The provider must be ABOVE the widget that reads the shard.
ShardProvider<CounterShard>(
create: () => CounterShard(),
child: const CounterScreen(), // context.read<CounterShard>() works here
)
| You want to… | Use |
|---|---|
Call a method from a callback or initState | context.read<T>() — no rebuild |
| Rebuild UI on every state change | ShardBuilder |
| Rebuild only when one slice of state changes | ShardSelector — preferred for performance |
| Run a side effect (navigate, snackbar) on change | ShardListener |
context.read<T>() does not subscribe, so calling it inside build won't rebuild your widget when state changes — use a builder for that. See Best Practices.
emit() is @protected by design. State changes should be expressed as intent-named methods on the shard, not triggered from the UI. Expose a method and call it via context.read:
class CounterShard extends Shard<int> {
CounterShard() : super(0);
void increment() => emit(state + 1); // intent-named method
}
// In the widget:
onPressed: () => context.read<CounterShard>().increment(),
See Core Concepts.
By default emit() skips notifying listeners when the new state equals the current one. Common causes:
emitForce() to notify anyway.== sees no change. Emit a new instance, or use emitForce().== on List/Set/Map is identity-based. Mix in DeepEqualityMixin (or override stateEquals) so equality compares contents.See State Equality in Core Concepts.
cacheKey defaults to the shard's runtime type name, so every instance of the same type shares one cache entry. Two UserShards with different userIds collide. Override cacheKey to include the parameters that distinguish instances:
@override
String get cacheKey => 'user_$userId';
The default is also unstable under release obfuscation. See Response Caching for the full caveat.
It depends on how you provided it:
ShardProvider(create: …) owns the shard and disposes it when the provider leaves the tree.ShardProvider.value(value: …) does not dispose — you created the instance, so you own its lifecycle.MultiShardProvider follows the same rule per entry: create is disposed, value is not.See Widgets.
Switch from ShardBuilder (rebuilds on every change) to ShardSelector, which rebuilds only when the selected slice changes. For finer control on a builder, pass buildWhen to skip rebuilds you don't need. See Best Practices.
Use a ShardListener, never a builder. Builders can run many times and may be skipped by the framework; a listener fires once per matching change. Gate it with listenWhen:
ShardListener<AuthShard, AuthState>(
listenWhen: (prev, curr) => curr.isLoggedIn && !prev.isLoggedIn,
listener: (context, prev, curr) =>
Navigator.of(context).pushReplacementNamed('/home'),
child: const LoginForm(),
)
get<T>() throws a StateError when nothing is registered for that type. Register the singleton in main() before runApp, or guard with isRegistered. In tests, call ShardLocator.reset() in setUp and register fakes:
setUp(() {
ShardLocator.reset();
ShardLocator.registerSingleton<ApiClient>(FakeApiClient());
});
See ShardLocator.
Timer-driven logic (debounce, throttle, persistence auto-save) needs deterministic time in tests. Use the fake_async package to advance virtual time instead of real delays. It's a dev dependency you add yourself — it doesn't ship with Shard. See Testing.
Back to Best Practices, or revisit Core Concepts to deepen your understanding.