chore: add AGENTS.md and .agents/ for Codex agent configuration
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: legado-compose-migration
|
||||
description: Guide Legado Android UI migration from XML/View/RecyclerView/DialogFragment screens to Jetpack Compose and Material 3, and guide new Compose-first screens using standard Android architecture. Use when creating, migrating, rewriting, reviewing, or planning a Legado screen, Activity, Fragment, dialog, adapter, navigation destination, or settings page, especially when MainActivity navigation, MVI/UDF, StateFlow/SharedFlow, Koin ViewModels, domain/usecase boundaries, edge-to-edge insets, predictive back, or existing Compose component conventions matter.
|
||||
---
|
||||
|
||||
# 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
|
||||
|
||||
1. 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.
|
||||
|
||||
2. 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/components` before creating new components.
|
||||
|
||||
3. Choose the minimal migration shape.
|
||||
- For new Compose-first screens, add a `MainActivity` navigation destination instead of creating a standalone Activity.
|
||||
- Use `BaseComposeActivity` for full-screen Activity migrations only when a legacy Activity must remain as an entry point.
|
||||
- Keep existing Activity Result APIs, `Intent` extras, 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 or `MainActivity` route boundary.
|
||||
- Put renderable state in `UiState`, user actions in `Intent`, and one-shot navigation/framework work in `Effect`.
|
||||
- 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 respects `WindowInsets` automatically) or apply `Modifier.windowInsetsPadding(WindowInsets.safeDrawing)` on the outermost container. Do not carry over `fitsSystemWindows` / 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.
|
||||
|
||||
4. Implement by layers.
|
||||
- Create or update `FeatureContract.kt` first for `UiState`, `Intent`, `Effect`, dialog/sheet models, and menu action enums.
|
||||
- Update `FeatureViewModel.kt` to expose `uiState: StateFlow<UiState>` and `effects: SharedFlow<Effect>`, with a single `onIntent(...)` entry point unless the existing feature has a simpler established pattern.
|
||||
- Create `FeatureScreen.kt` as a stateless route-level composable: `state`, callbacks, and `onIntent`.
|
||||
- Wire new Compose destinations through `MainActivity` route handling; update a retained Activity only when legacy `Intent` compatibility is required.
|
||||
- Collect state with `collectAsStateWithLifecycle()` and collect effects in `LaunchedEffect(Unit)` from the route or compatibility host.
|
||||
- Register new ViewModels in `di/appModule.kt` with `viewModelOf(::FeatureViewModel)` unless parameters require `viewModel { ... }`.
|
||||
|
||||
5. 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.
|
||||
|
||||
6. 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:assembleAppDebug` when feasible.
|
||||
- For behavior-heavy changes, add or update focused tests only where the project already has a practical test seam.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Keep Compose functions side-effect-light. Use `LaunchedEffect` for 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`/`SharedFlow` over `LiveData` for newly migrated Compose surfaces.
|
||||
- Annotate `UiState` data classes and UI-facing model wrappers with `@Stable` when they hold collections or entity data passed to composables. Kotlin 2.x strong skipping is on by default, but explicit `@Stable` provides the strongest compiler guarantee and helps document contract boundaries.
|
||||
- When using `FeatureIntent` for MVI user actions, distinguish it from Android `Intent` extras 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 be `Effect`s handled by `MainActivity` route handling or a compatibility Activity.
|
||||
- Prefer `MainActivity` navigation for new Compose destinations. Treat standalone Activities for migrated screens as legacy entry points only when existing View code still depends on `Intent` navigation.
|
||||
- For edge-to-edge: this project targets SDK 37, so edge-to-edge is enforced on Android 15+. Use Material 3 `Scaffold` insets or `WindowInsets.safeDrawing` / `safeContent` padding; ensure migrated screens draw behind system bars via `Modifier.windowInsetsPadding()` or top-level scaffold padding rather than manual hardcoded offsets. Migrated screens that relied on `fitsSystemWindows` in 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 `BackHandler` to intercept and route through `FeatureIntent.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.
|
||||
@@ -0,0 +1,194 @@
|
||||
# Legado Compose Migration Project Patterns
|
||||
|
||||
## Existing Shape
|
||||
|
||||
- App module: `app`.
|
||||
- Main source root: `app/src/main/java/io/legado/app`.
|
||||
- UI package: `ui/...`, usually grouped by feature.
|
||||
- Data package: `data/dao`, `data/entities`, `data/repository`.
|
||||
- Domain package: `domain/gateway`, `domain/model`, `domain/repository`, `domain/usecase`.
|
||||
- DI: `di/appModule.kt`, using Koin `singleOf`, `viewModelOf`, and parameterized `viewModel { ... }`.
|
||||
- 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
|
||||
|
||||
Inspect these as local examples, not as APIs to copy blindly:
|
||||
|
||||
- `ui/book/info/BookInfoActivity.kt`
|
||||
- `ui/book/info/BookInfoContract.kt`
|
||||
- `ui/book/info/BookInfoViewModel.kt`
|
||||
- `ui/book/info/BookInfoScreen.kt`
|
||||
- `ui/book/search/SearchContract.kt`
|
||||
- `ui/book/search/SearchViewModel.kt`
|
||||
- `ui/book/search/SearchScreen.kt`
|
||||
- `ui/main/MainActivity.kt`
|
||||
- `ui/main/MainScreen.kt`
|
||||
- `ui/widget/components/AppScaffold.kt`
|
||||
- `ui/widget/components/list/ListScaffold.kt`
|
||||
- `ui/theme/LegadoTheme.kt`
|
||||
- `ui/theme/AppTheme.kt`
|
||||
|
||||
## Recommended Feature Layout
|
||||
|
||||
For a migrated feature, prefer colocated files:
|
||||
|
||||
```text
|
||||
ui/<area>/<feature>/
|
||||
FeatureActivity.kt # only for legacy Intent compatibility or full-screen migration host
|
||||
FeatureContract.kt
|
||||
FeatureViewModel.kt
|
||||
FeatureScreen.kt
|
||||
FeatureSheets.kt # only if sheets are substantial
|
||||
FeatureDialogs.kt # only if dialogs are substantial
|
||||
```
|
||||
|
||||
Use the smallest set of files. Do not split files just to match this shape.
|
||||
|
||||
## MVI and UDF Rules
|
||||
|
||||
Use this shape for behavior-heavy screens:
|
||||
|
||||
```kotlin
|
||||
@Stable
|
||||
data class FeatureUiState(
|
||||
val isLoading: Boolean = false,
|
||||
val items: ImmutableList<Item> = persistentListOf(),
|
||||
val dialog: FeatureDialog? = null,
|
||||
val sheet: FeatureSheet = FeatureSheet.None,
|
||||
)
|
||||
|
||||
sealed interface FeatureIntent {
|
||||
data object BackPressed : FeatureIntent
|
||||
data object Refresh : FeatureIntent
|
||||
data class ItemClick(val id: Long) : FeatureIntent
|
||||
}
|
||||
|
||||
sealed interface FeatureEffect {
|
||||
data object Finish : FeatureEffect
|
||||
data class OpenDetail(val id: Long) : FeatureEffect
|
||||
}
|
||||
```
|
||||
|
||||
ViewModel rules:
|
||||
|
||||
- Keep `_uiState = MutableStateFlow(FeatureUiState())` private and expose `uiState = _uiState.asStateFlow()`.
|
||||
- Keep `_effects = MutableSharedFlow<FeatureEffect>(extraBufferCapacity = 8)` private and expose `effects = _effects.asSharedFlow()`.
|
||||
- Prefer `fun onIntent(intent: FeatureIntent)` as the UI entry point.
|
||||
- Treat `FeatureIntent` as an MVI/user-action type. When Android `Intent` launch/extras are also involved, name variables and explanations clearly enough that the two concepts are not confused.
|
||||
- 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.
|
||||
- Annotate `UiState` data classes with `@Stable` to give the Compose compiler the strongest stability guarantee. This is especially important when `UiState` fields include collections or entity types that come from non-Compose modules.
|
||||
- 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:
|
||||
|
||||
- Prefer `MainActivity` as the owner of Compose-first navigation. Add a Navigation3 route/key and `entryProvider` entry there for new destinations.
|
||||
- Extend `BaseComposeActivity` for migrated Activity screens only when a standalone Activity remains necessary.
|
||||
- Keep a legacy Activity for a migrated screen only when unreworked View code still calls it through `Intent`; in that case, make the Activity a thin compatibility host that translates extras/results and delegates to Compose state/effects.
|
||||
- Keep Activity Result launchers, file openers, clipboard, permission APIs, Android DialogFragments, and framework navigation in the Activity.
|
||||
- In `Content()`, collect state with `collectAsStateWithLifecycle().value`.
|
||||
- Collect effects in `LaunchedEffect(Unit) { viewModel.effects.collectLatest { ... } }`.
|
||||
- Pass only `state` and callbacks into the screen.
|
||||
- Edge-to-edge: Since target SDK ≥ 35, edge-to-edge is enforced. In `BaseComposeActivity`, `enableEdgeToEdge()` is called before `setContent`. Ensure screen-level composables apply `Modifier.windowInsetsPadding(WindowInsets.safeDrawing)` on the outermost non-scaffold container, or rely on Material 3 `Scaffold` (which handles `WindowInsets` automatically). Do not use manual `statusBarsPadding()` / `navigationBarsPadding()` on top of scaffold-level insets — double-padding is a common pitfall.
|
||||
|
||||
Navigation rules:
|
||||
|
||||
- For new Compose-first destinations, add the route to `MainActivity` instead of adding a new Activity.
|
||||
- Follow `MainActivity`'s current route style: serializable `NavKey` routes for in-process navigation and launcher `Intent` helper methods only when external or legacy callers need a stable entry point.
|
||||
- Keep legacy extra names stable when replacing an old Activity entry point.
|
||||
- When a compatibility Activity is retained, let it translate old `Intent` inputs/results; do not make it the source of truth for navigation or business behavior.
|
||||
- Predictive back: Navigation 3 enables predictive back by default on Android 14+. New composable destinations work with it automatically. When a screen needs back confirmation (unsaved changes, selection mode, add-to-shelf prompt), use `BackHandler(enabled = ...) { onIntent(FeatureIntent.BackPressed) }` to intercept and let the ViewModel decide — don't bypass ViewModel back logic.
|
||||
|
||||
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. 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 + Compose compiler plugin (`org.jetbrains.kotlin.plugin.compose`) enables **strong skipping by default**. This means composable functions with unstable parameters CAN still be skipped if their arguments are equal by `equals()`. However, ordinary Kotlin `List`, `Set`, and `Map` are still **unstable types** — passing them as parameters prevents the compiler from inferring stability, so changing a list reference still triggers recomposition of callers even with strong skipping.
|
||||
- Use `@Stable` on `UiState` data classes and UI model wrappers to give the Compose compiler the strongest stability guarantee. `@Stable` tells the compiler: "if `equals()` says two instances are the same, the UI hasn't changed." This is critical for data classes holding collections.
|
||||
- Use `kotlinx.collections.immutable` (`ImmutableList`, `ImmutableSet`, `ImmutableMap`) for render state that crosses into Compose. These are recognized as stable by the Compose compiler. Do not mechanically replace every temporary collection, Room query result, or internal mutable accumulator.
|
||||
- If data entities come from modules where the Compose compiler plugin is not applied, 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
|
||||
|
||||
This project is mid-migration, so use Clean Architecture pragmatically. For newly created screens, default to standard modern Android architecture and avoid mixed legacy patterns unless they are needed to interoperate with View screens that have not been rewritten.
|
||||
|
||||
- For new screens, keep UI, presentation, domain, and data boundaries explicit: Compose renders state, ViewModel owns state and intent handling, usecases hold reusable business actions, repositories mediate data access.
|
||||
- For new screens, do not place business rules in Activity/Fragment or composables.
|
||||
- For new screens, do not read DAOs directly from UI/presentation unless the project already has no reasonable repository/usecase boundary and adding one would be disproportionate.
|
||||
- For migrations, use existing DAOs/repositories directly if the old ViewModel already does and the migration is UI-only.
|
||||
- Prefer existing `domain/usecase` classes for reusable business actions such as cache, delete, group update, startup maintenance, reading progress, and WebDAV flows.
|
||||
- Add a usecase when a new screen has meaningful business rules, when logic would otherwise be duplicated across features, or when a UI migration needs to extract meaningful business rules from an Activity/adapter.
|
||||
- Keep UI-only formatting and Compose layout decisions out of domain.
|
||||
- Keep entity-to-display mapping near the UI or ViewModel unless it is reused business language.
|
||||
|
||||
## Compose Components and Theme
|
||||
|
||||
Prefer project components before raw Material widgets:
|
||||
|
||||
- Scaffold/top bars: `AppScaffold`, `GlassTopAppBar...`, `DynamicTopAppBar`, `TopBarActionButton`, `TopBarNavigationButton`.
|
||||
- Lists: `ListScaffold`, `TopFloatingStickyItem`, `VerticalFastScroller`, `SelectionBottomBar`.
|
||||
- Dialog/sheet: `AppAlertDialog`, `AppModalBottomSheet`, `OptionSheet`, text list input components.
|
||||
- Settings: `ClickableSettingItem`, `SwitchSettingItem`, `SliderSettingItem`, `ListSettingItem`, `SettingCard`.
|
||||
- Buttons/chips: `AppIconButton`, `SmallIconButton`, `SmallTextButton`, `ToggleChip`, `AlertButton`.
|
||||
- Covers/images: `CoilBookCover`, `BookshelfCover`, `buildCoverImageRequest`; use Coil for Compose image loading.
|
||||
- Theme: `LegadoTheme.colorScheme`, `LegadoTheme.typography`, `ThemeResolver`, `ProvideThemeOverride` where existing feature behavior needs theme override.
|
||||
|
||||
Avoid introducing a second visual system. If a raw Material 3 component is used, style it from `LegadoTheme`.
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
Before editing:
|
||||
|
||||
- Identify old XML layouts, ViewBinding fields, adapters, decorations, menu XML, dialogs, and result contracts.
|
||||
- Capture behavior: empty/loading/error states, refresh, search, sort, selection, long click, swipe, menu actions, back behavior, result codes, events, and persisted preferences.
|
||||
- Decide whether the task is UI-only or needs domain extraction.
|
||||
- For new screens, explicitly decide the presentation/domain/data boundary before coding; do not default to legacy mixed architecture.
|
||||
|
||||
During editing:
|
||||
|
||||
- Create/update `Contract`, then `ViewModel`, then `Screen`, then `MainActivity` route or compatibility host.
|
||||
- For new Compose destinations, wire navigation through `MainActivity` unless a legacy `Intent` caller must remain supported.
|
||||
- When retaining an Activity for compatibility, keep it small: parse legacy extras, collect effects, bridge result codes, and avoid placing feature business logic there.
|
||||
- Verify edge-to-edge: ensure the screen draws edge-to-edge without overlapping system bars. Use `Scaffold` or `Modifier.windowInsetsPadding(WindowInsets.safeDrawing)`. Remove any `fitsSystemWindows`-based margin hacks from the old layout.
|
||||
- Reuse existing string resources and add new resources for new user-facing text.
|
||||
- Register the ViewModel in `di/appModule.kt`.
|
||||
- Remove only resources made obsolete by this migration.
|
||||
- Keep existing public intent extra names and result codes unless explicitly changing an API.
|
||||
|
||||
Verification:
|
||||
|
||||
- Run `.\gradlew.bat :app:compileAppDebugKotlin` for Kotlin-only changes.
|
||||
- Run `.\gradlew.bat :app:assembleAppDebug` when resources, manifests, XML deletion, or generated bindings are affected.
|
||||
- If a screen uses Room queries or migration-sensitive repositories, run the relevant existing Android/unit tests if practical.
|
||||
- For manual verification, open the migrated screen and compare core flows against the old behavior list.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Do not put navigation or Activity launchers inside composables.
|
||||
- Do not store one-time navigation as nullable fields in `UiState` unless the existing local pattern already does; prefer `SharedFlow` effects.
|
||||
- Do not keep adapter selection state split between old adapter and new Compose state.
|
||||
- Do not delete XML/menu resources until all references and generated binding imports are gone.
|
||||
- Do not add speculative domain layers for a single migrated screen.
|
||||
- Do not use mixed architecture for new screens unless compatibility with unreworked View UI requires it.
|
||||
- Do not make a migrated screen's retained Activity the primary navigation model when `MainActivity` can own the Compose destination.
|
||||
- Do not bypass `BaseComposeActivity` unless the screen must remain inside a Fragment or legacy host.
|
||||
- Do not replace existing event bus or service interactions as part of a UI migration unless required for correctness.
|
||||
- Do not forget edge-to-edge: XML `fitsSystemWindows` does not carry over to Compose. Rely on `Scaffold` or `Modifier.windowInsetsPadding()` instead.
|
||||
- Do not forget `@Stable` on `UiState` data classes — missing annotations cause unnecessary recomposition, especially when state contains collections.
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: legado-compose-review
|
||||
description: Review existing Legado Jetpack Compose code for architecture, behavior, maintainability, and project convention issues. Use when auditing, reviewing, inspecting, evaluating, or finding problems in Legado Compose screens, routes, ViewModels, contracts, dialogs, sheets, navigation, or early Compose implementations, especially for MVI/UDF, StateFlow/SharedFlow, Clean Architecture, MainActivity navigation, legacy Activity compatibility, edge-to-edge insets, @Stable stability annotations, predictive back, and View-era mixed-pattern drift.
|
||||
---
|
||||
|
||||
# Legado Compose Review
|
||||
|
||||
## Overview
|
||||
|
||||
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.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Define the review scope.
|
||||
- Identify the exact screen, route, ViewModel, contract, or package under review.
|
||||
- State whether the user wants review only or review plus fixes. If unclear, review first and do not edit code.
|
||||
- Treat unrelated legacy View code as context, not as part of the review, unless it affects the Compose surface.
|
||||
|
||||
2. Build context from code.
|
||||
- Read the `*Screen`, `*ViewModel`, `*Contract`, host Activity/route, DI registration, and any repositories/usecases used by the feature.
|
||||
- Read the old View implementation only if it is still a compatibility caller or behavior reference.
|
||||
- Compare against nearby current examples such as `BookInfo*`, `Search*`, `MainActivity`, and shared components under `ui/widget/components`.
|
||||
|
||||
3. Review by risk, not style preference.
|
||||
- Prioritize behavior regressions, state duplication, lifecycle bugs, navigation bugs, business logic in UI, direct data access from UI/presentation, recomposition hazards, and missing compatibility handling.
|
||||
- Flag style-only issues only when they conflict with established project conventions or make future migration harder.
|
||||
- Do not require broad refactors for a small screen unless the current code creates real behavior or maintenance risk.
|
||||
|
||||
4. Output findings first.
|
||||
- Use code-review style: list findings before summaries.
|
||||
- Include file and tight line references.
|
||||
- Explain the concrete impact and the smallest credible fix.
|
||||
- If using Codex review directives, emit one `::code-comment{...}` per finding.
|
||||
- If no issues are found, say that clearly and mention remaining test/manual verification gaps.
|
||||
|
||||
5. Suggest fixes only after findings.
|
||||
- Group fixes into small, reviewable steps.
|
||||
- For architecture drift, separate compatibility-preserving fixes from larger cleanup.
|
||||
- Recommend `legado-compose-migration` only when the finding implies a migration or rewrite workflow.
|
||||
|
||||
## Review Priorities
|
||||
|
||||
- **P0/P1**: behavior breakage, lost navigation/result behavior, unsafe lifecycle collection, stale state, data corruption, crashes, compatibility entry points that no longer work, or content obscured by system bars (edge-to-edge breakage).
|
||||
- **P2**: architecture drift that will cause duplicated logic, hard-to-test behavior, recomposition bugs, incorrect ownership of state/effects, or missing `@Stable` annotations that cause measurable recomposition.
|
||||
- **P3**: convention drift, maintainability issues, weak naming, missing previews/tests, or small cleanup that should not block behavior.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Do not rewrite code during a review unless the user asks for fixes.
|
||||
- Do not demand pure architecture where a compatibility boundary is necessary for unreworked View screens.
|
||||
- For new Compose-first code, expect standard Android architecture: `MainActivity` route ownership, ViewModel-owned state, UDF/MVI user actions, `StateFlow`/`SharedFlow`, repositories/usecases, and UI without business logic.
|
||||
- For migrated code, allow a thin retained Activity only for Android `Intent` compatibility with unreworked View callers.
|
||||
- Distinguish MVI `FeatureIntent` user actions from Android `Intent` launch/extras when both appear.
|
||||
- For edge-to-edge: expect new and migrated Compose screens to handle system bar insets correctly — `Scaffold` or `Modifier.windowInsetsPadding()`, not hardcoded padding or XML `fitsSystemWindows` hacks. Target SDK 37 enforces edge-to-edge on Android 15+.
|
||||
- For stability: expect `@Stable` on `UiState` data classes and UI model wrappers. Flag unannotated state classes that hold collections or entity types from non-Compose modules, as they cause unnecessary recomposition.
|
||||
|
||||
## Reference
|
||||
|
||||
Read `references/review-checklist.md` for detailed checks. When a review turns into implementation, also read `../legado-compose-migration/references/project-patterns.md` if available.
|
||||
@@ -0,0 +1,117 @@
|
||||
# Legado Compose Review Checklist
|
||||
|
||||
## Files to Read
|
||||
|
||||
For a feature review, inspect the smallest complete slice:
|
||||
|
||||
- `FeatureScreen.kt` and any `FeatureSheets.kt` / `FeatureDialogs.kt`.
|
||||
- `FeatureViewModel.kt`.
|
||||
- `FeatureContract.kt` if present.
|
||||
- Host route in `MainActivity.kt` or retained `FeatureActivity.kt`.
|
||||
- Koin registration in `di/appModule.kt`.
|
||||
- Repositories/usecases used by the feature.
|
||||
- Old View caller or XML/adapters only when still used for compatibility or behavior comparison.
|
||||
|
||||
Use current examples as references:
|
||||
|
||||
- `ui/main/MainActivity.kt` for Navigation3 route ownership.
|
||||
- `ui/book/info/BookInfoContract.kt`, `BookInfoViewModel.kt`, `BookInfoScreen.kt` for behavior-heavy MVI/UDF.
|
||||
- `ui/book/search/SearchContract.kt`, `SearchViewModel.kt`, `SearchScreen.kt` for route-level search state.
|
||||
- `ui/widget/components/...` and `ui/theme/...` for shared UI conventions.
|
||||
|
||||
## Architecture Checks
|
||||
|
||||
Flag issues when:
|
||||
|
||||
- A new Compose-first screen is implemented as a standalone Activity instead of a `MainActivity` destination without a compatibility reason.
|
||||
- A retained Activity contains feature business logic instead of acting as a thin Android `Intent` compatibility host.
|
||||
- Business rules, repository calls, DAO calls, persistence writes, or service orchestration live in composables.
|
||||
- ViewModel exposes mutable state directly, exposes `MutableStateFlow`, or lets UI mutate domain objects.
|
||||
- UI state is split across Activity fields, composable `remember` state, adapter state, and ViewModel state in a way that can diverge.
|
||||
- One-shot events such as navigation, result codes, file opening, permission requests, or clipboard writes are represented as persistent `UiState` fields that can replay incorrectly.
|
||||
- MVI `FeatureIntent` user actions are confused with Android `Intent` launch/extras.
|
||||
- Domain/usecase boundaries are bypassed in new screens when a meaningful business action exists.
|
||||
- A migration introduces new domain abstractions for a single trivial UI action without reducing real complexity.
|
||||
|
||||
## UDF and State Checks
|
||||
|
||||
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.
|
||||
- `LaunchedEffect` keys are unstable or cause repeated data loading, duplicate navigation, duplicate toasts, or repeated service calls.
|
||||
- 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.
|
||||
- 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)`.
|
||||
|
||||
## Navigation and Compatibility Checks
|
||||
|
||||
Flag issues when:
|
||||
|
||||
- New Compose destinations skip `MainActivity` Navigation3 route registration.
|
||||
- Legacy Android `Intent` extras or result codes change without an explicit compatibility plan.
|
||||
- A migrated screen is reachable both through `MainActivity` and a retained Activity with inconsistent state initialization.
|
||||
- Navigation is performed directly inside nested composables instead of through callbacks/effects.
|
||||
- Activity Result launchers, file pickers, permission requests, or Android DialogFragments are hidden inside reusable UI composables.
|
||||
- Back behavior bypasses ViewModel decisions when unsaved changes, selection mode, add-to-shelf prompts, or confirmation dialogs exist.
|
||||
|
||||
## UI and Project Convention Checks
|
||||
|
||||
Flag issues when:
|
||||
|
||||
- Raw Material components ignore `LegadoTheme` or existing shared components where the project already has a wrapper.
|
||||
- A screen recreates shared UI already present under `ui/widget/components`.
|
||||
- User-facing text is hardcoded instead of using string resources, except for temporary debug-only text.
|
||||
- Compose UI keeps ViewBinding, adapter, or XML assumptions after migration.
|
||||
- Dialogs and bottom sheets use inconsistent project components when `AppAlertDialog`, `AppModalBottomSheet`, or existing option sheets fit.
|
||||
- Image loading bypasses existing Coil cover/image helpers where cover behavior, cache, SVG, or GIF handling matters.
|
||||
|
||||
## Clean Architecture Checks
|
||||
|
||||
For new screens, expect:
|
||||
|
||||
- Compose renders state and emits user actions.
|
||||
- ViewModel owns state, intent handling, and effect emission.
|
||||
- Usecases own reusable business actions.
|
||||
- Repositories mediate data access.
|
||||
- DAOs remain behind repositories unless adding a boundary would be disproportionate and local project patterns already allow direct use.
|
||||
|
||||
For migrated screens, allow pragmatic intermediate code only when:
|
||||
|
||||
- It preserves behavior.
|
||||
- It is isolated behind a clear compatibility boundary.
|
||||
- It does not spread View-era assumptions into new Compose-first code.
|
||||
|
||||
## Output Template
|
||||
|
||||
Use this shape unless the user asks for another format:
|
||||
|
||||
```text
|
||||
Findings
|
||||
- [P1] Title
|
||||
file:line
|
||||
Impact: ...
|
||||
Fix: ...
|
||||
|
||||
Open Questions
|
||||
- ...
|
||||
|
||||
Notes
|
||||
- No issues found in ... / Tests not run ...
|
||||
```
|
||||
|
||||
When using Codex inline review comments, emit one directive per finding with tight line ranges:
|
||||
|
||||
```text
|
||||
::code-comment{title="[P2] Keep route state in ViewModel" body="..." file="/absolute/path/FeatureScreen.kt" start=42 end=45 priority=2 confidence=0.8}
|
||||
```
|
||||
|
||||
## Verification Suggestions
|
||||
|
||||
Recommend verification based on the reviewed change:
|
||||
|
||||
- Kotlin-only review/fix: `.\gradlew.bat :app:compileAppDebugKotlin`.
|
||||
- Resource/XML/manifest impact: `.\gradlew.bat :app:assembleAppDebug`.
|
||||
- Navigation or compatibility changes: manually open both `MainActivity` route and any retained legacy Android `Intent` entry point.
|
||||
- Behavior-heavy ViewModel changes: add or run focused tests only where the project has a practical test seam.
|
||||
@@ -0,0 +1,389 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
|
||||
|
||||
## Coding Guidelines
|
||||
|
||||
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
||||
|
||||
### Think Before Coding
|
||||
|
||||
- State assumptions explicitly. If uncertain, ask.
|
||||
- If multiple interpretations exist, present them — don't pick silently.
|
||||
- If a simpler approach exists, say so. Push back when warranted.
|
||||
- If something is unclear, stop. Name what's confusing. Ask.
|
||||
|
||||
### Simplicity First
|
||||
|
||||
- No features beyond what was asked.
|
||||
- No abstractions for single-use code.
|
||||
- No "flexibility" or "configurability" that wasn't requested.
|
||||
- No error handling for impossible scenarios.
|
||||
- If you write 200 lines and it could be 50, rewrite it.
|
||||
|
||||
### Surgical Changes
|
||||
|
||||
- Don't "improve" adjacent code, comments, or formatting.
|
||||
- Don't refactor things that aren't broken.
|
||||
- Match existing style, even if you'd do it differently.
|
||||
- If you notice unrelated dead code, mention it — don't delete it.
|
||||
- Remove imports/variables/functions that YOUR changes made unused.
|
||||
- Don't remove pre-existing dead code unless asked.
|
||||
|
||||
### Goal-Driven Execution
|
||||
|
||||
Transform tasks into verifiable goals:
|
||||
- "Add validation" → "Write tests for invalid inputs, then make them pass"
|
||||
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
|
||||
- "Refactor X" → "Ensure tests pass before and after"
|
||||
|
||||
## Build / Test / Run
|
||||
|
||||
```bash
|
||||
# Quick compile check (Kotlin only, no dex/package — fastest for verifying code compiles)
|
||||
.\gradlew.bat :app:compileAppDebugKotlin
|
||||
|
||||
# Assemble all variants
|
||||
./gradlew assembleAppRelease
|
||||
|
||||
# Assemble without R8 (for crash debugging — no minification/shrinking)
|
||||
./gradlew assembleAppNoR8
|
||||
|
||||
# Debug build
|
||||
./gradlew assembleAppDebug
|
||||
|
||||
# Run unit tests (JVM, local)
|
||||
./gradlew test
|
||||
|
||||
# Run a single test class
|
||||
./gradlew test --tests "io.legado.app.model.cache.CacheDownloadQueueTest"
|
||||
|
||||
# Run connected Android tests
|
||||
./gradlew connectedAndroidTest
|
||||
|
||||
# Lint
|
||||
./gradlew lint
|
||||
|
||||
# Update Cronet (after changing CronetVersion in gradle.properties)
|
||||
./gradlew app:downloadCronet
|
||||
```
|
||||
|
||||
The project uses JDK 21 for development (set in `build.gradle.kts` via `jvmToolchain`). CI uses JDK 17 for building.
|
||||
|
||||
Gradle properties: 8 GB heap, configuration cache disabled (`gradle.properties:31`), non-transitive R classes, precise resource shrinking enabled.
|
||||
|
||||
## Architecture
|
||||
|
||||
This is a Material Design 3 fork of [Legado](https://github.com/gedoor/legado). `app/src/main/java/io/legado/app/` uses **Clean Architecture** with three layers:
|
||||
|
||||
| Layer | Package | Role |
|
||||
|---|---|---|
|
||||
| Data | `data/` | Room DB (`AppDatabase`, version 85, ~22 DAOs, ~25 entities), repository implementations |
|
||||
| Domain | `domain/` | Gateway interfaces, use cases (14), domain models — no framework dependencies |
|
||||
| UI | `ui/` | Jetpack Compose screens, Navigation 3 routes, ViewModels |
|
||||
|
||||
Additional top-level packages:
|
||||
- **`help/`** — Infrastructure "glue": HTTP (OkHttp + Cronet), book content processing, backup/WebDAV, JS engine, config
|
||||
- **`model/`** — Runtime state coordinators (not entities): `ReadBook`, `AudioPlay`, `CacheBook`, `BookCover`, etc.
|
||||
- **`service/`** — Android foreground/background services (audio playback, TTS, download, web server)
|
||||
- **`web/`** — Embedded HTTP server (Ktor) for remote bookshelf/source editing
|
||||
- **`lib/`** — Third-party library wrappers (MOBI parser, WebDAV client, legacy View theme system, cronet)
|
||||
- **`base/`** — Abstract Activity/Fragment/ViewModel base classes
|
||||
- **`utils/`** — Extension functions and utility classes (~70 files)
|
||||
|
||||
Modules: `:app`, `:modules:book` (epub/TXT parsing, namespace `me.ag2s`), `:modules:rhino` (Rhino JS wrapper, namespace `com.script`). There is also a Vue 3 web frontend in `modules/web/` (pnpm, separate from the Android build).
|
||||
|
||||
## Dependency Injection (Koin)
|
||||
|
||||
Two modules loaded in `App.onCreate()`:
|
||||
|
||||
```kotlin
|
||||
startKoin {
|
||||
modules(appDatabaseModule, appModule)
|
||||
}
|
||||
```
|
||||
|
||||
- **`di/appDatabaseModule.kt`** — Singleton `AppDatabase` + factory bindings for all 22 DAOs
|
||||
- **`di/appModule.kt`** — Singletons (repositories, use cases, gateways, Coil `ImageLoader`), `viewModelOf` / `viewModel { }` for all ViewModels, some parameterized definitions
|
||||
|
||||
Gateways are bound to their repository implementations explicitly (e.g., `single<LocalBookGateway> { LocalBookRepository(get()) }`), not through `singleOf`.
|
||||
|
||||
## Navigation
|
||||
|
||||
Uses **Jetpack Navigation 3** (`androidx.navigation3`) with type-safe `@Serializable` sealed interfaces for route keys:
|
||||
|
||||
```kotlin
|
||||
@Serializable
|
||||
private sealed interface MainRoute : NavKey
|
||||
@Serializable
|
||||
private data object MainRouteHome : MainRoute
|
||||
@Serializable
|
||||
private data class MainRouteCache(val groupId: Long) : MainRoute
|
||||
```
|
||||
|
||||
`MainActivity` holds a single `NavDisplay` with `entryProvider { ... }` defining all composable entries. `Launcher0` through `LauncherW` extend `MainActivity` to provide multiple launcher icon alias entries. Separate activities handle the reader (`ReadBookActivity` — still View-based), book info, source management, replace rules, file manager, QR scanner, etc.
|
||||
|
||||
## Theme System
|
||||
|
||||
A multi-engine theming system in `ui/theme/`:
|
||||
|
||||
1. **Material 3 Expressive** (default): Uses `MaterialExpressiveTheme` with `MotionScheme.expressive()`
|
||||
2. **Miuix** (alternative): Uses `top.yukonga.miuix.kmp` theming engine
|
||||
|
||||
14 theme modes (`AppThemeMode` enum) — Dynamic (Monet), 12 named presets, Custom (MaterialKolor seed-color generation), Transparent. `CustomColorScheme` wraps `com.materialkolor` with configurable `PaletteStyle` (TonalSpot, Neutral, Vibrant, Expressive, Rainbow, etc.) and `ColorSpec` (2021 vs 2025).
|
||||
|
||||
Legacy View-based theme still exists in `lib/theme/` (used by non-migrated screens like `ReadBookActivity`).
|
||||
|
||||
## Hybrid Compose + View
|
||||
|
||||
The app is mid-migration from Views to Compose. View-based screens (reader, book info, source management) coexist with Compose screens (main tabs, settings, search, RSS, cache management). XML layouts, `viewBinding`, and traditional Activities are still heavily used. The `viewBinding` build feature is enabled but Compose screens are the target.
|
||||
|
||||
## Jetpack Compose Requirements (new screens MUST follow)
|
||||
|
||||
All **new** UI screens must be implemented in Jetpack Compose following the patterns below. Do **not
|
||||
** create new View-based Activities/Fragments/XML layouts. Existing View-based screens can remain
|
||||
until migrated.
|
||||
|
||||
### MVI/UDF Architecture
|
||||
|
||||
Every Compose screen follows a strict **Model-View-Intent** pattern with three artifacts defined in
|
||||
a `*Contract.kt` file:
|
||||
|
||||
```
|
||||
ui/{feature}/
|
||||
├── XxxContract.kt // UiState, Intent, Effect (and optionally Sheet/Dialog)
|
||||
├── XxxViewModel.kt // ViewModel
|
||||
├── XxxScreen.kt // Screen composable
|
||||
└── XxxRouteScreen.kt // (optional) outer wrapper for activity results / lifecycle
|
||||
```
|
||||
|
||||
**Contract definitions:**
|
||||
|
||||
```kotlin
|
||||
// @Stable data class — all screen state in one place
|
||||
@Stable
|
||||
data class XxxUiState(
|
||||
val loading: Boolean = false,
|
||||
val items: ImmutableList<ItemUi> = persistentListOf(),
|
||||
val activeSheet: XxxSheet? = null,
|
||||
val activeDialog: XxxDialog? = null,
|
||||
)
|
||||
|
||||
// sealed interface — every user action is an Intent
|
||||
sealed interface XxxIntent {
|
||||
data class LoadData(val id: Long) : XxxIntent
|
||||
data object Refresh : XxxIntent
|
||||
}
|
||||
|
||||
// sealed interface — one-shot side effects (navigation, toast, etc.)
|
||||
sealed interface XxxEffect {
|
||||
data class ShowToast(val message: String) : XxxEffect
|
||||
data class NavigateTo(val route: MainRoute) : XxxEffect
|
||||
}
|
||||
|
||||
// (optional) sealed interface for multi-sheet/dialog scenarios
|
||||
sealed interface XxxSheet { data object Filter : XxxSheet }
|
||||
sealed interface XxxDialog { data class Confirm(val msg: String) : XxxDialog }
|
||||
```
|
||||
|
||||
**Naming rules:**
|
||||
|
||||
- State: `{Feature}UiState` — `@Stable data class`
|
||||
- Intent: `{Feature}Intent` — `sealed interface` with `data class` / `data object` members
|
||||
- Effect: `{Feature}Effect` — `sealed interface`
|
||||
- Sheet/Dialog: `{Feature}Sheet`, `{Feature}Dialog` — `sealed interfaces` stored in UiState
|
||||
|
||||
### ViewModel
|
||||
|
||||
```kotlin
|
||||
class XxxViewModel(/* injected dependencies */) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(XxxUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
private val _effects = MutableSharedFlow<XxxEffect>(extraBufferCapacity = 16)
|
||||
val effects = _effects.asSharedFlow()
|
||||
|
||||
fun onIntent(intent: XxxIntent) {
|
||||
when (intent) {
|
||||
is XxxIntent.LoadData -> loadData(intent.id)
|
||||
is XxxIntent.Refresh -> refresh()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadData(id: Long) {
|
||||
// Use viewModelScope, update _uiState via update { it.copy(...) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key rules:
|
||||
|
||||
- Extend `ViewModel()` directly (not `BaseViewModel`).
|
||||
- `_uiState` is `MutableStateFlow`, exposed as `StateFlow` via `.asStateFlow()`.
|
||||
- `_effects` is `MutableSharedFlow(extraBufferCapacity = 16)`, exposed via `.asSharedFlow()`.
|
||||
- Emit effects via `_effects.tryEmit(...)`.
|
||||
- Single `onIntent()` entry point, dispatched via `when`.
|
||||
|
||||
### Screen Composable
|
||||
|
||||
```kotlin
|
||||
// Stateless screen — ViewModel wired in entry provider or RouteScreen
|
||||
@Composable
|
||||
fun XxxScreen(
|
||||
state: XxxUiState,
|
||||
onIntent: (XxxIntent) -> Unit,
|
||||
effects: Flow<XxxEffect>, // one-shot effects from ViewModel
|
||||
onBack: () -> Unit,
|
||||
onNavigateToYyy: (YyyRoute) -> Unit,
|
||||
) {
|
||||
// Collect effects
|
||||
LaunchedEffect(Unit) {
|
||||
effects.collectLatest { effect ->
|
||||
when (effect) {
|
||||
is XxxEffect.ShowToast -> { /* ... */ }
|
||||
is XxxEffect.NavigateTo -> onNavigateToYyy(effect.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
GlassMediumFlexibleTopAppBar(
|
||||
title = { Text("Title") },
|
||||
scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior(),
|
||||
navigationButton = { TopBarNavigationButton(onBack) },
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
// UI content, no business logic here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key rules:
|
||||
|
||||
- Screen is **stateless** — receives `state`, `onIntent`, `effects`, never accesses ViewModel
|
||||
directly.
|
||||
- Effects collected in `LaunchedEffect(Unit) { ... }` using `collectLatest`.
|
||||
- Alternatively, effects can be collected in the outer `RouteScreen` or entry provider if the screen
|
||||
doesn't need them directly.
|
||||
- Use project custom widgets: `AppScaffold`, `AppText`, `AppIcon`, `AppIcons`, `AppAlertDialog`,
|
||||
`AppModalBottomSheet`, `NormalCard`, `GlassMediumFlexibleTopAppBar`, `TopBarNavigationButton`,
|
||||
`TopBarActionButton`, etc.
|
||||
- No business logic, no direct DB/network calls in composables.
|
||||
|
||||
Two input patterns are acceptable:
|
||||
|
||||
- **Stateless (preferred for new screens):** `state: XxxUiState` + `onIntent: (XxxIntent) -> Unit` —
|
||||
ViewModel wired in entry provider or RouteScreen.
|
||||
- **ViewModel as default param:** `viewModel: XxxViewModel = koinViewModel()` — simpler for
|
||||
standalone screens.
|
||||
|
||||
### Stability
|
||||
|
||||
- All `UiState` and UI item data classes **must** be annotated with `@Stable`.
|
||||
- Use `ImmutableList` (from `kotlinx.collections.immutable`) for list properties in state classes,
|
||||
not `List` or `MutableList`.
|
||||
- Prefer `persistentListOf()` / `toImmutableList()` for default values.
|
||||
|
||||
### Navigation
|
||||
|
||||
Uses **Navigation 3** (`androidx.navigation3`). Routes are `@Serializable` sealed interfaces:
|
||||
|
||||
```kotlin
|
||||
// In MainNavKey.kt
|
||||
@Serializable
|
||||
data class MainRouteXxx(val id: Long) : MainRoute
|
||||
```
|
||||
|
||||
Entry registered in `MainNavGraph.kt`:
|
||||
|
||||
```kotlin
|
||||
entry<MainRouteXxx> { route ->
|
||||
val viewModel = koinViewModel<XxxViewModel>()
|
||||
XxxScreen(
|
||||
state = viewModel.uiState.collectAsStateWithLifecycle().value,
|
||||
onIntent = viewModel::onIntent,
|
||||
onBack = { onNavigateBack() },
|
||||
onNavigateToYyy = { onNavigateToRoute(it) },
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Key rules:
|
||||
|
||||
- Screens **never** reference the navigator directly — receive `onBack`, `onNavigateToXxx` lambdas.
|
||||
- Navigation is callback-based, wired by the entry provider.
|
||||
- New routes added to the `MainRoute` sealed interface in `MainNavKey.kt`.
|
||||
|
||||
### Koin DI
|
||||
|
||||
- Register ViewModels in `di/appModule.kt` with `viewModelOf(::XxxViewModel)`.
|
||||
- Inject in Compose via `koinViewModel()` (default param or explicit in entry provider).
|
||||
- For keyed ViewModels (e.g. per-book): `koinViewModel<XxxViewModel>(key = route.bookUrl)`.
|
||||
- Repositories/gateways/use cases registered as `singleOf(::...)`.
|
||||
|
||||
### Activity Base Class
|
||||
|
||||
New standalone Compose activities extend `BaseComposeActivity`:
|
||||
|
||||
```kotlin
|
||||
class XxxActivity : BaseComposeActivity() {
|
||||
@Composable
|
||||
override fun Content() {
|
||||
// Screen content — AppTheme is already applied by the base class
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### RouteScreen Wrapper
|
||||
|
||||
For screens needing activity result handling, lifecycle observation, or permission requests, use a
|
||||
two-layer pattern:
|
||||
|
||||
- Outer `XxxRouteScreen`: handles `ActivityResultLauncher`, lifecycle callbacks, file pickers,
|
||||
permission requests. Wires ViewModel.
|
||||
- Inner `XxxScreen`: pure UI, stateless with `state` + `onIntent`.
|
||||
|
||||
### Material 3 vs Miuix
|
||||
|
||||
The project supports two Compose theme engines. If a screen needs engine-specific UI, branch on:
|
||||
|
||||
```kotlin
|
||||
if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
|
||||
// Miuix implementation
|
||||
} else {
|
||||
// Material 3 implementation
|
||||
}
|
||||
```
|
||||
|
||||
For detailed Compose review conventions and migration patterns, see
|
||||
`.Codex/skills/legado-compose-review/`.
|
||||
|
||||
## Rhino JavaScript Engine
|
||||
|
||||
Book sources, RSS sources, and HTTP TTS use JavaScript rules. `initRhino()` in `App.kt` registers `NativeBaseSource` wrappers for `BookSource`, `RssSource`, `HttpTTS` (writable JS objects) and `ReadOnlyJavaObject` wrappers for rule entities. Rule parsing logic lives in `help/source/` and `model/analyzeRule/`.
|
||||
|
||||
## Important Constraints
|
||||
|
||||
- **Do not update jsoup** beyond 1.16.2 — a breaking change in newer versions (see [jsoup#2017](https://github.com/jhy/jsoup/pull/2017)) affects `AnalyzeByJSoup.kt` and the JsoupXpath library
|
||||
- **Do not update hutool** beyond 5.8.22 — pinned in `libs.versions.toml:42`
|
||||
- Package name discrepancy: code namespace is `io.legado.app` but `applicationId` is `io.legato.kazusa`
|
||||
- Min SDK 26, target SDK 37, compile SDK 37
|
||||
- Release builds enable R8 minification + resource shrinking; `noR8` variant disables both for crash debugging
|
||||
- APK is split by ABI (`armeabi-v7a`, `arm64-v8a`, plus universal)
|
||||
- Firebase Analytics and Performance are included; `google-services` plugin applied
|
||||
|
||||
## Web Frontend
|
||||
|
||||
Located in `modules/web/` — a Vue 3 + TypeScript + Vite project for remote bookshelf and source editing. Must connect to the app's built-in HTTP server (started via `WebService` in the main activity settings). Commands:
|
||||
|
||||
```bash
|
||||
cd modules/web
|
||||
pnpm install
|
||||
pnpm dev # dev server
|
||||
pnpm build # production build
|
||||
```
|
||||
|
||||
Set `VITE_API` in `.env.development` to the app's web service IP.
|
||||
Reference in New Issue
Block a user