RIPDPI's own Compose conventions: ViewModel pattern, Route/Screen split, DataStore->StateFlow, RipDpiThemeTokens. Use for how this app does Compose, not generic Compose API questions (see compose).
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
RIPDPI Compose performance: stability annotations, compiler metrics, LazyColumn key/contentType, per-screen recomposition notes. Use when diagnosing recomposition, stability, or list performance here.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
설치 방법 보기설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.
The Compose compiler decides at compile time whether each class is stable (safe to skip recomposition when equal) or unstable (must always recompose). Classes from non-Compose modules (protobuf, java.time, Room entities) are unstable by default unless declared in the stability config.
@Immutable -- all public properties never change after construction. Use for sealed-class hierarchies and pure value types (DiagnosticsUiModels, HistoryUiModels, theme tokens like Color, Spacing, Shape, Surface, Motion).
@Stable -- property reads return equal values between recompositions, or the runtime is notified on change. Use for state holders with callbacks or MutableState (SettingsUiModels: DesyncCoreUiState, ProxyNetworkUiState, etc.).
activities/DiagnosticsUiModels.kt -- 60+ annotations, @Immutable for data,
@Stable for models with callbacks.activities/SettingsUiModels.kt -- @Stable on all section state holders.activities/HistoryUiModels.kt -- @Immutable on all models (pure data).activities/MainViewModel.kt -- @Immutable on sealed states.ui/theme/ -- @Immutable on all token classes.Rule: every new class passed to a @Composable MUST be annotated. Explicit annotation prevents regressions when a field type changes.
app/compose-stability.conf marks external types as stable:
com.poyka.ripdpi.proto.*, com.google.protobuf.GeneratedMessageLitejava.time.{Instant,Duration,LocalDate,LocalDateTime,ZonedDateTime,ZoneId}kotlin.time.Duration, kotlinx.collections.immutable.*com.poyka.ripdpi.data.{TcpChainStepModel,UdpChainStepModel,ActivationFilterModel,NumericRangeModel}Add new non-Compose-module types here rather than wrapping in UI models.
Convention plugin ripdpi.android.compose.gradle.kts enables reports when
CI=true or -Pripdpi.composeReports=true:
./gradlew :app:assembleGithubFullRelease -Pripdpi.composeReports=true
Output: app/build/compose-reports/ and app/build/compose-metrics/.
| File | Purpose |
|---|---|
*-composables.txt | Restartable/skippable status and param stability |
*-classes.txt | Stability of every class seen by the compiler |
*-composables.csv | Machine-readable, diff between commits |
*-module.json | Module summary: skippable %, restartable count |
Look for "not skippable" composables (unstable parameter) and "unstable"
classes (often a List<T> that should be ImmutableList<T>).
TrackRecomposition(tag) -- SideEffect counting recompositions. Already in
HomeScreen. Add to any suspect composable.RecompositionReportEffect(intervalMs) -- periodic logcat dump. Place once
in root composable. Markers: ! (delta>5), !!! (delta>20).adb logcat -s RecomposeTracker:D RecomposeReport:I *:SEnable "Show Recomposition Counts" in the toolbar. High recomposition count with low skip count = hot path that needs investigation.
The project includes androidx-compose-runtime-tracing. Capture a Perfetto
trace and open in ui.perfetto.dev, filter for Compose: slices.
Every items() call MUST provide key. Without keys, Compose recreates item
state on every list change. Also provide contentType when mixing different
item structures -- the slot pool is keyed by content type.
Project status:
HistorySections.kt): key = { it.id }, contentType = { "connection_session" } / { "diagnostics_session" } / { "event" }key = { it.label }, contentType = { "trend" }key = { _, entry -> entry.id }, contentType = { _, _ -> "log_entry" }Avoid new lambda instances inside items {}. Hoist callbacks:
// Bad: new lambda per recompose
items(list, key = { it.id }) { item ->
Row(onClick = { viewModel.onClick(item.id) })
}
// Good: stable outer lambda
val onClick = remember(viewModel) { { id: String -> viewModel.onClick(id) } }
items(list, key = { it.id }) { item ->
Row(onClick = { onClick(item.id) })
}
DiagnosticsScanSection's live-probe preview (livePreviewProbes) is a
remember-memoized reversed().take(N) window over the growing
completedProbes list, so the raw display index is not stable across new
completions. The list key uses liveProbeItemKey(completedProbeCount, previewIndex): Int = completedProbeCount - previewIndex - 1, which resolves
to each probe's own append-order position in the untruncated list rather
than its position in the preview window -- that position never changes once
assigned, and it no longer risks colliding on repeated target/outcome
pairs the way a literal "$index-${probe.target}-${probe.outcome}" key
would. Follow this pattern (a stable position derived from a monotonically
growing count, not the display index) for any other reversed/truncated
live-preview list.
rememberInfiniteTransition for connecting-state pulse -- correctly scoped
to ConnectionState.Connecting, stops when idle.animateColorAsState, animateFloatAsState for press feedback.AnimatedContent for state transitions.TrackRecomposition("HomeScreen").derivedStateOf for event filtering -- correct pattern.beyondBoundsPageCount = 0 -- do not increase it.derivedStateOf for FAB visibility based on scroll position.key but no contentType (fix opportunity).ImmutableList<LogEntry> before UI exposure.entries.size in header causing parent recomposition on each new
log line. Int is stable, but keep parent scope narrow.item(key = "..."), all state holders are @Stable.items() calls with key +
contentType live in HistorySections.kt, not HistoryScreen.kt.derivedStateOf for empty-state detection.-composables.csv against baselinekey and contentTyperemember lambdas passed to LazyColumn itemsImmutableList for list params to @Composable, not List<T>아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.
로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.
RIPDPI's own Compose conventions: ViewModel pattern, Route/Screen split, DataStore->StateFlow, RipDpiThemeTokens. Use for how this app does Compose, not generic Compose API questions (see compose).
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
ADB-based RIPDPI device/emulator debugging: build/install/launch, logcat filtering, fixture port-forwarding, instrumented tests, crash/ANR triage. Use when debugging on a real device or emulator.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
RIPDPI Appium launch-contract reference: start routes and permission/service/data presets. Use when a test launches to the wrong screen or a new automation route is added. General flakiness: appium-test-debug.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
RIPDPI Appium test authoring: page objects, resource-id locators, assertions, wait tiers. Use when writing a new Appium test or page object, or adding coverage for a new screen.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
RIPDPI Appium failure triage: flaky tests, locator/session/wait issues, screenshot and element-tree debugging. Use when an Appium test fails or is flaky.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
Add or modify a GitHub Actions job. Use when editing a file under .github/workflows/. Not for running a workflow locally (local-ci-act) or release signing (release-signing).
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.