[优化] 性能优化
This commit is contained in:
@@ -9,7 +9,7 @@ description: Guide Legado Android UI migration from XML/View/RecyclerView/Dialog
|
||||
|
||||
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, read `references/project-patterns.md`.
|
||||
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
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
- Main navigation: `ui/main/MainActivity.kt`, using Navigation3 `NavKey`, `rememberNavBackStack`, `entryProvider`, and `NavDisplay`, plus legacy route extras such as `EXTRA_START_ROUTE`.
|
||||
- Base Compose host: `base/BaseComposeActivity.kt`, which wraps `Content()` in `AppTheme`, configures system bars, locale/font, background image, and LiveBus recreation events.
|
||||
- Compose dependencies are already enabled in `app/build.gradle.kts`; do not add new UI libraries unless the target screen truly requires one.
|
||||
- `kotlinx.collections.immutable` is available for Compose-facing screen state. Prefer `ImmutableList`, `ImmutableSet`, and `ImmutableMap` at `UiState` boundaries when collection state is passed to composables and changes often.
|
||||
|
||||
## Files to Inspect Before Migrating
|
||||
|
||||
@@ -79,6 +80,8 @@ ViewModel rules:
|
||||
- Use `_uiState.update { it.copy(...) }` for state changes.
|
||||
- Emit one-shot work through effects, not booleans in state.
|
||||
- Keep cached current entities private in the ViewModel when existing project behavior needs mutation or incremental sync, but publish immutable render state.
|
||||
- For collection-heavy `UiState`, convert DAO/repository `List`/`Set`/`Map` values to `kotlinx.collections.immutable` at the ViewModel/UI-state boundary with `toImmutableList()`, `toImmutableSet()`, or `toImmutableMap()`. Do not force repository, DAO, or domain APIs to use persistent collections unless the domain contract truly benefits.
|
||||
- Keep internal flow pipelines free to use normal Kotlin collections for sorting, grouping, and persistence work; the immutable collection rule is primarily for values exposed to Compose.
|
||||
- Use existing `BaseViewModel.execute { ... }.onSuccess { ... }.onError { ... }` when the surrounding ViewModel already uses that pattern.
|
||||
|
||||
Activity rules:
|
||||
@@ -102,9 +105,20 @@ Screen rules:
|
||||
|
||||
- Make `FeatureScreen(state, onIntent, ...)` stateless for business state.
|
||||
- Use `remember` and `rememberSaveable` only for local UI affordances such as menu expansion, scroll state, transient animation state, and text field drafts when committing through an intent.
|
||||
- Use `rememberSaveable` for user-driven transient targets that should survive recreation, such as a delete-confirmation item ID or URL. Store only the stable ID in saved state and resolve the current entity from `UiState`.
|
||||
- Use `BackHandler` to route back actions through `FeatureIntent.BackPressed` when ViewModel decisions matter.
|
||||
- Keep expensive derived values in `remember(key)` or ViewModel state when they depend on repository data.
|
||||
- Prefer `LazyColumn`, `LazyRow`, stable keys, and existing fast-scroller/list components for adapter migrations.
|
||||
- Prefer `LazyColumn`, `LazyRow`, stable keys, and existing fast-scroller/list components for adapter migrations. For heterogeneous lazy lists or grids, provide `contentType` in addition to `key`.
|
||||
- In long-lived `LaunchedEffect` collectors, wrap changing callbacks, `Context`-dependent operations, or lambdas from parents with `rememberUpdatedState` when the effect should not restart.
|
||||
- Hoist state only to the lowest common owner that reads and writes it. Move branch-only ViewModel lookup and Flow collection into the branch or a small child composable instead of collecting at a high-level route.
|
||||
- Avoid UI-state feedback loops such as `LaunchedEffect(uiState.items) { viewModel.pruneSelection(...) }`. Prefer deriving consistency inside the ViewModel with `combine(...)`, or reduce the state as part of the flow that produces the data.
|
||||
|
||||
## Current Compose Performance Notes
|
||||
|
||||
- Kotlin 2.x enables strong skipping by default, but ordinary Kotlin `List`, `Set`, and `Map` are still unstable to Compose. This means high-frequency screen state with normal collections can still widen recomposition work.
|
||||
- Use immutable collections for render state that crosses into Compose. Do not mechanically replace every temporary collection, Room query result, or internal mutable accumulator.
|
||||
- If data entities come from modules where the Compose compiler does not run, consider a stable UI model wrapper when the entity is passed deeply through composables and causes measurable recomposition cost.
|
||||
- Treat `SnapshotStateList` / `SnapshotStateMap` as UI-owned mutable state, not as a default ViewModel `UiState` transport type.
|
||||
|
||||
## Clean Architecture Boundaries
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ description: Review existing Legado Jetpack Compose code for architecture, behav
|
||||
|
||||
Review existing Compose code before rewriting it. Focus on concrete defects, architectural drift, behavior risks, and missing verification, especially in early Compose screens that may predate the current MVI/UDF, Clean Architecture, and `MainActivity` navigation expectations.
|
||||
|
||||
Read `references/review-checklist.md` for the project-specific checklist and severity guidance.
|
||||
Read `references/review-checklist.md` for the project-specific checklist, current Compose state/performance checks, and severity guidance.
|
||||
|
||||
## Workflow
|
||||
|
||||
|
||||
@@ -39,11 +39,26 @@ Flag issues when:
|
||||
|
||||
- `Screen` functions own business state instead of receiving `state` and callbacks.
|
||||
- `remember` / `rememberSaveable` stores source-of-truth data that should survive process or route recreation through ViewModel state.
|
||||
- User-driven transient state that should survive recreation, such as a pending delete-confirmation target, is held with plain `remember` instead of `rememberSaveable` or ViewModel state.
|
||||
- `LaunchedEffect` keys are unstable or cause repeated data loading, duplicate navigation, duplicate toasts, or repeated service calls.
|
||||
- A long-lived `LaunchedEffect` collector captures parent callbacks or context-dependent values that can change without using `rememberUpdatedState`, unless the effect is intentionally keyed to restart.
|
||||
- Flows are collected without lifecycle awareness in UI routes where `collectAsStateWithLifecycle()` should be used.
|
||||
- List items lack stable keys where mutation, selection, or animation can make state attach to the wrong row.
|
||||
- Heterogeneous lazy lists or grids provide `key` but omit `contentType`, reducing composition reuse quality when headers, rows, ads, loading items, or expanded content mix in the same lazy layout.
|
||||
- Selection, search query, sorting, filtering, loading, and error states are not represented in a single coherent `UiState`.
|
||||
- Derived values are recomputed expensively on every recomposition instead of living in ViewModel state or `remember(key)`.
|
||||
- UI effects write list-derived consistency back into the ViewModel, such as pruning selection from `LaunchedEffect(uiState.items)`. Prefer deriving this in the ViewModel with `combine(...)` or reducing it when the data flow updates.
|
||||
- State is hoisted higher than the lowest common owner, such as a top-level route collecting a child ViewModel flow only needed inside a conditional branch or one popup menu.
|
||||
|
||||
## Compose Stability and Collection Checks
|
||||
|
||||
Flag issues when:
|
||||
|
||||
- Collection-heavy `UiState` exposed to Compose uses ordinary Kotlin `List`, `Set`, or `Map` in hot paths. Kotlin 2.x strong skipping is helpful but does not make standard Kotlin collections stable to Compose.
|
||||
- DAO or repository lists are passed directly through `UiState` to deep composables without conversion to `ImmutableList` / `ImmutableSet` / `ImmutableMap` or a stable UI model wrapper.
|
||||
- Review recommendations imply replacing every internal collection. Keep the finding scoped to Compose-facing render state; temporary accumulators, sorting inputs, Room DAO signatures, and domain APIs can remain normal collections unless they are the actual recomposition boundary.
|
||||
- Mutable collections such as `ArrayList`, `MutableList`, or mutable maps are stored in Compose state or ViewModel `UiState`.
|
||||
- Entity classes from non-Compose modules are passed deeply through composables and cause visible recomposition churn; prefer stable UI render models if measurement or code shape shows this matters.
|
||||
|
||||
## Navigation and Compatibility Checks
|
||||
|
||||
|
||||
Reference in New Issue
Block a user