Skip to content
cccskills
FreePublished on GitHub

android-compose-patterns

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

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md5.6 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Android Compose Patterns

Overview

RIPDPI uses Jetpack Compose with Material 3 and a sealed-class navigation system. State flows from DataStore through ViewModels to Composables via StateFlow + collectAsStateWithLifecycle().

Diagnostics UI is a current hotspot in this repo. It layers internal UI models over shared diagnostics contracts, exposes stable automation tags through RipDpiTestTags, and routes callback-style actions such as opening Advanced Settings or candidate-detail sheets through screen-level parameters.

Data Flow

DataStore (proto) -> ViewModel (StateFlow + combine) -> Composable (collectAsStateWithLifecycle)
User action -> ViewModel method -> DataStore.updateData { } or ServiceManager.start/stop
One-shot effects -> Channel<Effect> -> receiveAsFlow() -> LaunchedEffect collector

Navigation

Routes are defined as a sealed class in app/.../ui/navigation/Route.kt.

Adding a New Screen

  1. Add route to Route sealed class with route string, @StringRes titleRes, optional icon
  2. If top-level: add to Route.topLevel list
  3. Add to Route.all list
  4. Add composable(Route.YourRoute.route) { ... } in RipDpiNavHost.kt
  5. Navigate: navController.navigate(Route.YourRoute.route) { launchSingleTop = true; restoreState = true }

Navigation Conventions

  • Use launchSingleTop = true and restoreState = true for all navigations
  • Bottom bar shows only for isTopLevelDestination() routes
  • Pass ViewModels to route composables, not raw state

ViewModel Pattern

class ExampleViewModel(application: Application) : AndroidViewModel(application) {
    // State: combine multiple sources into single UI state
    val uiState: StateFlow<UiState> = combine(
        application.settingsStore.data,
        _localState,
    ) { settings, local -> UiState(/*...*/) }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), UiState())

    // Effects: one-shot events (navigation, snackbar, permission requests)
    private val _effects = Channel<Effect>(Channel.BUFFERED)
    val effects = _effects.receiveAsFlow()

    // Actions: public methods called by Composables
    fun onAction() { viewModelScope.launch { /* ... */ } }
}

Key Conventions

  • SharingStarted.WhileSubscribed(5_000) for all StateFlow exports
  • AndroidViewModel (not plain ViewModel) when DataStore access needed
  • Mutex for thread-safe state transitions (see MainViewModel.toggleService)
  • Channel<Effect> for one-shot UI events, collected via LaunchedEffect

Composable Pattern

// Route composable: connects ViewModel to screen
@Composable
fun ExampleRoute(viewModel: ExampleViewModel, navController: NavController) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    ExampleScreen(uiState = uiState, onAction = viewModel::onAction)
}

// Screen composable: pure UI, no ViewModel reference
@Composable
fun ExampleScreen(uiState: UiState, onAction: () -> Unit) { /* ... */ }

Conventions

  • Split Route (stateful) from Screen (stateless) composables
  • Use collectAsStateWithLifecycle() (not collectAsState())
  • Pass callbacks, not ViewModel references, to Screen composables
  • Use RipDpiThemeTokens for colors, spacing, typography

Representative ViewModels

ViewModelLocationPurpose
MainViewModelactivities/MainViewModel.ktConnection state, VPN/proxy toggle, metrics
ConfigViewModelactivities/ConfigViewModel.ktProxy config presets, draft editing, validation
SettingsViewModelactivities/SettingsViewModel.ktApp settings, theme, DataStore persistence
DiagnosticsViewModelactivities/DiagnosticsViewModel.ktDiagnostics scan orchestration, history, export/share state, and strategy-probe presentation

Diagnostics UI Conventions

  • Keep diagnostics projection logic in the activities/DiagnosticsUi* support files instead of recomputing report metadata directly in composables.
  • Strategy-probe screens now have specialized presentation states such as candidate-aware progress, audit assessment, winners-first layout, and workflow restriction remediation.
  • Prefer stable automation tags from app/src/main/kotlin/com/poyka/ripdpi/ui/testing/RipDpiTestTags.kt for any new externally exercised UI.
  • Route navigation callbacks such as onOpenAdvancedSettings, onSelectCandidate, and sheet-dismiss actions through Route/Screen parameters rather than letting deep child composables navigate directly.

Common Mistakes

MistakeFix
Using collectAsState()Use collectAsStateWithLifecycle() for lifecycle awareness
Passing ViewModel to Screen composablePass uiState and callbacks; keep Screen stateless
Hardcoding colors/spacingUse RipDpiThemeTokens and Material 3 theme
Missing WhileSubscribed(5_000)Required for proper lifecycle handling in all StateFlow exports
Creating ViewModel in ComposableUse viewModel() in Route composable or pass from NavHost

Related Skills

  • compose (.agents/skills/compose/SKILL.md): General Jetpack Compose API reference (guidance docs plus a live-fetch index into the actual androidx source) and the scored RIPDPI Compose quality audit. Use for questions about how Compose APIs work internally, correct API usage patterns, recomposition mechanics, Modifier ordering, side-effects, performance, or accessibility.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

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.

The English summary is being prepared. Showing the original description.

po4yka/RIPDPI81Updated Oct 19, 2026

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.

The English summary is being prepared. Showing the original description.

po4yka/RIPDPI81Updated Oct 19, 2026

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.

The English summary is being prepared. Showing the original description.

po4yka/RIPDPI81Updated Oct 19, 2026

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

The English summary is being prepared. Showing the original description.

po4yka/RIPDPI81Updated Oct 19, 2026

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

The English summary is being prepared. Showing the original description.

po4yka/RIPDPI81Updated Oct 19, 2026

Conservative review of end-user legal exposure from diagnostics targets and probe hosts, by jurisdiction. Use when shipping or editing diagnostics target lists, probe hosts, or bootstrap endpoints.

The English summary is being prepared. Showing the original description.

po4yka/RIPDPI81Updated Oct 19, 2026

See all skills by po4yka

Report a problem with this skill