Kraken’s nightly architectural scan flagged NodeChip.kt (dataSourceOrValueLabel,
NodeChipScaffold) and NodeItem.kt for calling collectAsState() directly in a
composable body, without wrapping in remember/LaunchedEffect or keying — claiming
this causes a new collector to launch on every recomposition without cancelling the
previous one, accumulating collectors and leaking memory.
Mostly a false positive, with one real minor inefficiency underneath it.
collectAsState() is Compose’s own produceState/LaunchedEffect wrapper: it keys
its internal collector coroutine on the flow instance, and Compose’s LaunchedEffect
keying already cancels the previous collector before launching a new one when the key
changes. Calling it directly in a composable body — no extra remember needed — is the
documented, idiomatic pattern. NodeItem.kt’s screenCore.editMode /
editFirstSelection / editSecondSelection are stable StateFlow properties held by
a Koin-singleton ScreenCoreImpl, so their instance never changes across recompositions
— no churn, no leak.
The one real gap: ClientNodeManager.readNodeStateOrNull(id) (used by
NodeChip.kt:34,75) returned a fresh MutableStateFlow(null) on every call when
id was null or the node wasn’t yet available (e.g. an unresolved cross-server
@DataSource input). Since collectAsState() keys on flow identity, every recomposition
while a node was unresolved forced Compose to tear down and relaunch the collector
instead of reusing the remembered one — real churn, but never an actual leak (the old
collector was always properly cancelled by LaunchedEffect’s keying), and bounded by
how often those composables recompose while a source is unloaded.
shared/src/commonMain/kotlin/krill/zone/shared/node/manager/ClientNodeManager.kt:
added a single shared absentNodeFlow: MutableStateFlow<Node?> sentinel and return it
from readNodeStateOrNull() instead of allocating a new MutableStateFlow(null) per
call, so repeated calls for a null/unavailable id return the same flow instance.shared/src/jvmTest/kotlin/krill/zone/shared/node/manager/ClientNodeManagerReadOrNullTest.kt:
added two tests asserting readNodeStateOrNull returns the identical flow instance
across repeated calls, for both the null-id and absent-id cases.When Kraken (or anyone) flags collectAsState() used directly in a composable body as
an “unkeyed leak,” check whether the underlying flow is a stable instance across
recompositions first (a class-level StateFlow property, or a map-cached
MutableStateFlow keyed by id) — if so, Compose’s own keying already prevents
accumulation and no fix is needed. The only real risk is a factory function that
allocates a new flow instance per call for the same logical “no value” case; fix that
by returning a shared sentinel instance instead of wrapping every collectAsState() call
site in remember.