Quick Start
A screen is a CreateContext plus one or more Nodes. CreateContext pushes
a shared value down to every child composable, and each Node is an
independent state machine that talks to the rest of the app through typed
impulses on the bus — not through shared state or callbacks.
class AppConfig(val refreshIntervalMs: Long)
data class AuthSession(val userId: String)data class Refresh(val timestamp: Long) : Impulse()data class FetchProfile(val userId: String) : DataImpulse<Profile>()data class Profile(val name: String, val email: String)
@Composablefun ProfileScreen(config: AppConfig) { CreateContext(config) { Column { ProfileCard() RefreshControl() } }}
// Subscribes to session state, reacts to refresh events, and fetches data.@Composablefun ContextScope<AppConfig>.ProfileCard() { Node(ProfileState()) { // State channel — replay=1, latest value on subscribe ListenFor<AuthSession> { session -> update { it.copy(userId = session.userId) } }
// Reaction channel — fire-and-forget events ReactTo<Refresh> { impulse -> update { it.copy(lastRefresh = impulse.timestamp) } }
// Request channel — refetches when lastRefresh changes Request(FetchProfile(state.userId), key = state.lastRefresh) { data -> update { it.copy(profile = data) } }
when (val profile = state.profile) { is DataState.Success -> Text("${profile.data.name} — ${profile.data.email}") is DataState.Loading -> CircularProgressIndicator() else -> {} } }}
// Throttles clicks using its own timestamp state and the configured interval.@Composablefun ContextScope<AppConfig>.RefreshControl() { Node(initialState = 0L) { // last refresh timestamp Button(onClick = { val now = System.currentTimeMillis() if (now - state >= context.refreshIntervalMs) { update { now } scope.launch { Trigger(Refresh(now)) } } }) { Text("Refresh") } }}A few things to notice:
ProfileScreenowns no state — it’s a context boundary plus layout.- Two independent Nodes —
ProfileCardandRefreshControlshare no state and hold no references to each other. - All three channels are in play —
ListenForsubscribes to State,ReactTo+Triggerwork the Reaction channel, andRequestfetches data on the Request channel. AppConfigflows through the context —RefreshControlreadscontext.refreshIntervalMsto decide whether to fire, without anyone passing the config down as a parameter. That’s whatCreateContextis for.- Node state carries the throttle —
RefreshControl’s state is its last-refresh timestamp. The click handler checks the elapsed interval before updating the timestamp and firing aRefreshimpulse with the new value. - The
Requestkey re-runs the fetch —ProfileCardusesstate.lastRefreshas thekey, so each new refresh timestamp triggers a fresh Provider run.
- Three Channels — State, Reaction, Request in depth
- Compose DSL —
CreateContext,Node, andNodeScope - Providers — how
Requestgets fulfilled