本文へ移動
cccskills
無料GitHub で公開

compose-performance

RIPDPI Compose performance: stability annotations, compiler metrics, LazyColumn key/contentType, per-screen recomposition notes. Use when diagnosing recomposition, stability, or list performance here.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md7.6 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Compose Performance -- RIPDPI

1. Stability system

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.).

Project conventions

  • 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.

Stability configuration file

app/compose-stability.conf marks external types as stable:

  • com.poyka.ripdpi.proto.*, com.google.protobuf.GeneratedMessageLite
  • java.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.

2. Compiler metrics

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/.

FilePurpose
*-composables.txtRestartable/skippable status and param stability
*-classes.txtStability of every class seen by the compiler
*-composables.csvMachine-readable, diff between commits
*-module.jsonModule summary: skippable %, restartable count

Look for "not skippable" composables (unstable parameter) and "unstable" classes (often a List<T> that should be ImmutableList<T>).

3. Recomposition debugging

TrackRecomposition (built-in, ui/debug/TrackRecomposition.kt)

  • 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).
  • Filter: adb logcat -s RecomposeTracker:D RecomposeReport:I *:S
  • No-op in release builds (gated on BuildConfig.DEBUG).

Layout Inspector

Enable "Show Recomposition Counts" in the toolbar. High recomposition count with low skip count = hot path that needs investigation.

Compose runtime tracing

The project includes androidx-compose-runtime-tracing. Capture a Perfetto trace and open in ui.perfetto.dev, filter for Compose: slices.

4. LazyColumn optimization

Keys and contentType

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:

  • History screen family (HistorySections.kt): key = { it.id }, contentType = { "connection_session" } / { "diagnostics_session" } / { "event" }
  • DiagnosticsLiveSection: key = { it.label }, contentType = { "trend" }
  • LogsScreen: key = { _, entry -> entry.id }, contentType = { _, _ -> "log_entry" }

Lambda captures

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) })
}

Derived keys for a truncated live preview

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.

5. Project-specific screen performance

HomeScreen

  • rememberInfiniteTransition for connecting-state pulse -- correctly scoped to ConnectionState.Connecting, stops when idle.
  • animateColorAsState, animateFloatAsState for press feedback.
  • AnimatedContent for state transitions.
  • Already instrumented with TrackRecomposition("HomeScreen").
  • Watch: entire screen recomposing on connection state change. MainUiState is @Immutable, so parameters should remain stable.

DiagnosticsScreen (~1200 lines)

  • HorizontalPager with multiple tabs, each containing a LazyColumn.
  • derivedStateOf for event filtering -- correct pattern.
  • LazyRow chips with proper keys and contentType.
  • Watch: off-screen page recomposition. HorizontalPager defaults to beyondBoundsPageCount = 0 -- do not increase it.

LogsScreen

  • Streaming viewer, potentially thousands of entries.
  • derivedStateOf for FAB visibility based on scroll position.
  • LazyColumn has key but no contentType (fix opportunity).
  • ViewModel converts to ImmutableList<LogEntry> before UI exposure.
  • Watch: entries.size in header causing parent recomposition on each new log line. Int is stable, but keep parent scope narrow.

AdvancedSettingsScreen

  • RipDpiSettingsScaffold wrapping LazyColumn, ~20 expandable sections.
  • Each section is item(key = "..."), all state holders are @Stable.
  • Architecture passes individual section state (DesyncCoreUiState, etc.) so each section only recomposes when its own state changes.

History screen family

  • Three tabs, each with a LazyColumn. The items() calls with key + contentType live in HistorySections.kt, not HistoryScreen.kt.
  • Models are @Immutable. Uses derivedStateOf for empty-state detection.

6. Quick wins checklist

  • Run compiler reports and diff -composables.csv against baseline
  • Verify new UI model classes have @Immutable or @Stable
  • New compose-stability.conf entries must match actual immutability
  • New LazyColumn: always provide key and contentType
  • Hoist and remember lambdas passed to LazyColumn items
  • Use ImmutableList 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).

日本語の概要は準備中です。原文の説明を表示しています。

po4yka/RIPDPI792026年10月11日 更新

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.

日本語の概要は準備中です。原文の説明を表示しています。

po4yka/RIPDPI792026年10月11日 更新

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.

日本語の概要は準備中です。原文の説明を表示しています。

po4yka/RIPDPI792026年10月11日 更新

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.

日本語の概要は準備中です。原文の説明を表示しています。

po4yka/RIPDPI792026年10月11日 更新

RIPDPI Appium failure triage: flaky tests, locator/session/wait issues, screenshot and element-tree debugging. Use when an Appium test fails or is flaky.

日本語の概要は準備中です。原文の説明を表示しています。

po4yka/RIPDPI792026年10月11日 更新

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).

日本語の概要は準備中です。原文の説明を表示しています。

po4yka/RIPDPI792026年10月11日 更新

po4yka のスキルをすべて見る

このスキルの問題を報告する