Troubleshooting
My listener never receives anything
Section titled “My listener never receives anything”Symptom: ListenFor<T> or ReactTo<T> handler never fires.
Likely causes:
-
Type mismatch. The type argument on the producer and consumer must match exactly.
Broadcast<SessionState>andListenFor<SessionStatus>are two different channels even if the classes have the same fields. -
Reaction fired before the collector was active. Reactions have replay 0 — if the
Triggerfires before theReactTosubscription is collecting, the event is gone. If you need late subscribers to see the value, use the State channel (Broadcast+ListenFor) instead. -
Lifecycle not resumed. In tests, the
LifecycleOwnermust be moved to at leastRESUMEDbefore handlers will collect. If you’re usingSynapseTestRulethis is handled for you; if you’re wiring manually, calllifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME).
NoProviderException
Section titled “NoProviderException”No provider registered for impulse type: FetchProduct.Annotate a Provider implementation with @Provider for this impulse type.Cause: A Request(FetchProduct(...)) was dispatched but no
@SynapseProvider class is registered for FetchProduct.
Fixes:
- Verify a provider class annotated
@SynapseProviderexists for this impulse type and extendsProvider<FetchProduct, Product>. - In multi-module projects, make sure the module containing the provider
applies the
arch-hiltKSP processor and its generatedProviderRegistryContributionis picked up by the Hilt graph. See Hilt Integration.
No SwitchBoard provided!
Section titled “No SwitchBoard provided!”No SwitchBoard provided! Wrap your app in ProvideSwitchBoard.Cause: A composable tried to access LocalSwitchBoard.current but no
CompositionLocalProvider is above it in the tree.
Fix: Wrap your root composable in a CompositionLocalProvider that
supplies the SwitchBoard:
CompositionLocalProvider(LocalSwitchBoard provides switchBoard) { App()}This is typically done in your Activity.onCreate or Application-level
setup. See Compose DSL and Hilt Integration.
Duplicate provider registration
Section titled “Duplicate provider registration”Duplicate provider registration for impulse type: FetchProduct.Each DataImpulse type must have exactly one provider.Cause: Two @SynapseProvider classes claim the same DataImpulse
type, or the same provider is registered twice during
ProviderRegistry building.
Fixes:
- Search for all classes extending
Provider<FetchProduct, ...>— there should be exactly one. - In multi-module builds, make sure two modules don’t both define a
provider for the same impulse. The
ProviderRegistrymerge step during Hilt aggregation will throw if it detects a collision.
Request returns DataState.Error unexpectedly
Section titled “Request returns DataState.Error unexpectedly”A request can surface an error for two reasons:
-
The provider threw. The exception is wrapped in
DataState.Error(cause). Check thecausefor the underlying issue. If the provider had a previous successful result,staleDatacarries the last-known-good value so the UI can display it alongside the error. -
Concurrency limit reached. If the
ProviderManagerhas a configured concurrency cap and it’s full, new requests receiveDataState.Errorwith aProviderConcurrencyException. This is rare in default configurations (the limit is unbounded by default).
Interceptor not running
Section titled “Interceptor not running”Likely causes:
-
Wrong intercept point. Each interceptor registers at a specific channel and direction —
STATE_UPSTREAM,REACTION_DOWNSTREAM, etc. Make sure the point matches the traffic you’re trying to intercept. -
Registration happened after the event. Interceptors only see traffic that flows after they’re installed. In tests, install captures before the action under test.
-
Type doesn’t match. The interceptor’s type argument must match or be a supertype of the data flowing through the channel. An
Intercept<TokenBearer>won’t catch a type that doesn’t implementTokenBearer. -
Registration was disposed.
addInterceptorreturns aRegistrationhandle. If something calledregistration.unregister(), the interceptor is gone.
Test hangs or times out
Section titled “Test hangs or times out”Likely causes:
-
Lifecycle not advanced. Coordinator and Node handlers gate on lifecycle state. If the test’s
LifecycleOwneris stuck atINITIALIZED, subscriptions never start collecting. Move it toRESUMED. -
Dispatcher not set. Without
Dispatchers.setMain(testDispatcher), coroutines launched onDispatchers.Mainhang in a unit test.SynapseTestRulehandles this automatically. -
Late collection on SharedFlow. If you’re manually collecting a
SharedFlowafter the value was emitted, the collector blocks forever waiting. UseSynapseTestRule’sonImpulse/onStatecapture helpers, which install upstream interceptors before the action.
Same request fires twice / provider runs twice
Section titled “Same request fires twice / provider runs twice” Request deduplicates by structural equality of the impulse. If two
call sites dispatch FetchProduct(id = "abc"), they share the same
provider job and DataState flow. But FetchProduct(id = "abc") and
FetchProduct(id = "def") are different impulses and each gets its own
provider run.
If you’re seeing duplicate runs for what you think is the same impulse,
check that the impulse’s equals() / hashCode() — typically via
data class — covers all fields you expect. An extra field with a
different value means a different impulse.
Coordinator disposes before work finishes
Section titled “Coordinator disposes before work finishes”Coordinators are scoped to a LifecycleOwner. When that owner reaches
ON_DESTROY, the coordinator’s CoroutineScope is canceled and all
in-flight work stops. This is by design — it prevents leaks.
If you need work to survive a configuration change, scope the
coordinator to the Activity or Application lifecycle rather than a
Fragment. See Coordinators.