Legado Compose Migration
Overview
Migrate one UI surface at a time, or create one new Compose destination at a time. Preserve behavior first for migrations; for newly created screens, prefer standard modern Android architecture over mixed legacy patterns. Use the project's existing MainActivity navigation, BaseComposeActivity compatibility hosts, *Screen, *Contract, *ViewModel, StateFlow, SharedFlow, Koin, theme, and widget patterns.
Before editing, inspect the target View implementation if one exists, MainActivity navigation when adding a destination, and at least two nearby migrated Compose screens. For concrete project patterns, including current Compose state/performance rules, read references/project-patterns.md.
Workflow
State assumptions and success criteria.
- Name the exact screen or destination being created/migrated.
- For migrations, define behavior that must remain unchanged: inputs, result codes, navigation, menu actions, dialogs/sheets, list selection, refresh, persistence, and event bus behavior.
- For new screens, define the route owner, UI state, events/effects, domain/usecase boundary, and verification target.
- If the target mixes UI and business logic heavily, keep the migration surgical and defer deeper domain cleanup unless needed.
Map the old surface.
- For migrations, read the Activity/Fragment, XML layouts, adapters, menu XML, dialogs, result launchers, and ViewModel.
- For new screens, read
MainActivity, nearby route screens, the relevant ViewModel/usecase/repository patterns, and shared UI components. - List UI state, user intents, one-shot effects, and external side effects.
- Identify reusable Compose components under
ui/widget/componentsbefore creating new components.
Choose the minimal migration shape.
- For new Compose-first screens, add a
MainActivitynavigation destination instead of creating a standalone Activity. - Use
BaseComposeActivityfor full-screen Activity migrations only when a legacy Activity must remain as an entry point. - Keep existing Activity Result APIs,
Intentextras, permission flows, and Android framework calls in the host/compatibility Activity. - If an unreworked View screen still starts the migrated screen with
Intent, keep the old Activity only as a compatibility host that parses legacy extras and delegates to the Compose screen orMainActivityroute boundary. - Put renderable state in
UiState, user actions inIntent, and one-shot navigation/framework work inEffect. - For new screens, use standard Android/Compose architecture: UDF/MVI-style state hoisting, lifecycle-aware Flow collection, ViewModel-owned state, repository/usecase boundaries, and UI free of business logic.
- Ensure the new screen handles edge-to-edge correctly: use
Scaffold(which respectsWindowInsetsautomatically) or applyModifier.windowInsetsPadding(WindowInsets.safeDrawing)on the outermost container. Do not carry overfitsSystemWindows/ manual padding patterns from XML. - Use mixed legacy patterns only as an integration boundary for unreworked View screens or existing framework contracts.
- Use existing repositories/usecases when they already fit; introduce new domain/usecase classes when a new screen needs clean business boundaries or when a migration would otherwise duplicate or entangle business logic.
- For new Compose-first screens, add a
Implement by layers.
- Create or update
FeatureContract.ktfirst forUiState,Intent,Effect, dialog/sheet models, and menu action enums. - Update
FeatureViewModel.ktto exposeuiState: StateFlow<UiState>andeffects: SharedFlow<Effect>, with a singleonIntent(...)entry point unless the existing feature has a simpler established pattern. - Create
FeatureScreen.ktas a stateless route-level composable:state, callbacks, andonIntent. - Wire new Compose destinations through
MainActivityroute handling; update a retained Activity only when legacyIntentcompatibility is required. - Collect state with
collectAsStateWithLifecycle()and collect effects inLaunchedEffect(Unit)from the route or compatibility host. - Register new ViewModels in
di/appModule.ktwithviewModelOf(::FeatureViewModel)unless parameters requireviewModel { ... }.
- Create or update
Remove only obsolete migration artifacts.
- Delete XML layouts, adapters, menu resources, binding fields, and imports only when the migrated screen no longer references them.
- Do not refactor unrelated View screens or shared utilities.
Verify.
- Prefer the smallest Gradle check that compiles the touched app code, usually
.\gradlew.bat :app:compileAppDebugKotlin. - If resources, manifests, or XML deletion are involved, run
.\gradlew.bat :app:assembleAppDebugwhen feasible. - For behavior-heavy changes, add or update focused tests only where the project already has a practical test seam.
- Prefer the smallest Gradle check that compiles the touched app code, usually
Boundaries
- Keep Compose functions side-effect-light. Use
LaunchedEffectfor collecting effects and use callbacks for user actions. - Do not pass
Activity,View, binding objects, or mutable domain entities deep into composables unless an existing local pattern requires it. - Prefer project theme/components:
LegadoTheme,AppScaffold,ListScaffold,AppAlertDialog,AppModalBottomSheet,RoundDropdownMenu, top bar helpers, setting items, cover components, and list utilities. - Prefer
StateFlow/SharedFlowoverLiveDatafor newly migrated Compose surfaces. - Annotate
UiStatedata classes and UI-facing model wrappers with@Stablewhen they hold collections or entity data passed to composables. Kotlin 2.x strong skipping is on by default, but explicit@Stableprovides the strongest compiler guarantee and helps document contract boundaries. - When using
FeatureIntentfor MVI user actions, distinguish it from AndroidIntentextras and launch APIs in names, comments, and explanations where both appear. - For new Compose-first screens, do not copy View-era shortcuts such as UI logic in Activity/Fragment, direct binding-like mutable UI state, adapter-owned state, or Activity-context business operations.
- Keep one-off Android actions out of
UiState: navigation, file opening, clipboard, dialogs implemented as Android DialogFragments, result launchers, permission requests, and callbacks that require host context should beEffects handled byMainActivityroute handling or a compatibility Activity. - Prefer
MainActivitynavigation for new Compose destinations. Treat standalone Activities for migrated screens as legacy entry points only when existing View code still depends onIntentnavigation. - For edge-to-edge: this project targets SDK 37, so edge-to-edge is enforced on Android 15+. Use Material 3
Scaffoldinsets orWindowInsets.safeDrawing/safeContentpadding; ensure migrated screens draw behind system bars viaModifier.windowInsetsPadding()or top-level scaffold padding rather than manual hardcoded offsets. Migrated screens that relied onfitsSystemWindowsin XML must be updated. - For predictive back: New Compose destinations should work with the predictive back gesture (enabled by default in Navigation 3). When a screen requires back confirmation (unsaved changes, selection mode), use
BackHandlerto intercept and route throughFeatureIntent.BackPressed. - Keep existing Chinese string resources and localization behavior; add strings to resources when user-facing text is new.
Reference
Read references/project-patterns.md when implementing or reviewing a migration. It contains project-specific examples, file placement rules, state/effect conventions, and verification commands.