Troubleshooting

FAQ

Common questions and fixes when working with Shard.

Overview

Common questions and fixes when working with Shard. If you hit something not covered here, open an issue on GitHub.

"No ShardProvider<X> found in context"

This assertion fires when you read a shard that isn't provided above the current widget. Three things to check:

  1. A ShardProvider (or MultiShardProvider) for that exact type is an ancestor of the widget calling context.read<X>(), ShardBuilder, or ShardSelector.
  2. The type parameter matches exactlycontext.read<CounterShard>() needs a ShardProvider<CounterShard>, not a base type.
  3. In tests, the widget under test is wrapped in a provider.
// The provider must be ABOVE the widget that reads the shard.
ShardProvider<CounterShard>(
  create: () => CounterShard(),
  child: const CounterScreen(), // context.read<CounterShard>() works here
)

context.read vs ShardBuilder vs ShardSelector

You want to…Use
Call a method from a callback or initStatecontext.read<T>() — no rebuild
Rebuild UI on every state changeShardBuilder
Rebuild only when one slice of state changesShardSelector — preferred for performance
Run a side effect (navigate, snackbar) on changeShardListener

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.

Why can't I call emit() from my widget?

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.

My UI doesn't rebuild after emit()

By default emit() skips notifying listeners when the new state equals the current one. Common causes:

  • You emitted an equal value. That's the equality check working as intended. Use emitForce() to notify anyway.
  • Mutable state changed in place. If you mutate an object/list and emit the same reference, == sees no change. Emit a new instance, or use emitForce().
  • Collections with identical contents. Dart's == on List/Set/Map is identity-based. Mix in DeepEqualityMixin (or override stateEquals) so equality compares contents.

See State Equality in Core Concepts.

Two FutureShards return each other's data

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.

Is my shard disposed automatically?

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.

My widget rebuilds too often

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.

How do I navigate or show a snackbar on a state change?

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(),
)

ShardLocator.get throws StateError

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.

My debounce/throttle tests are flaky

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.

Next Steps

Back to Best Practices, or revisit Core Concepts to deepen your understanding.