Target: D:\workspace\project\GSYGithubAppCompose
Date: 2026-04-14
Scope: app, core/ui, data, feature/* production Compose sources
Excluded from scoring: **/src/test/**, **/src/androidTest/**, preview-only code (none found)
Confidence: High
Overall Score: 46/100
| Category | Score | Weight | Status | Notes |
|---|---|---|---|---|
| Performance | 4/10 | 35% | needs work | Lazy list identity is weak, and compiler reports show too many unstable shared params in feature:detail. |
| State management | 5/10 | 25% | needs work | State ownership is mostly clear, but lifecycle-aware collection is missing across Android UI and some local state can go stale. |
| Side effects | 6/10 | 20% | needs work | Most loading work is inside LaunchedEffect, but there is still composition-time work and event collection is inconsistent. |
| Composable API quality | 5/10 | 20% | needs work | Shared components skip modifier, accept unstable models directly, and navigation is still stringly typed on Navigation 2.8.5. |
-
Performance: many lazy lists still render without stable item keys
- Why it matters: item identity is left to position, so moved/inserted items can trigger unnecessary recomposition, misplaced remembered state, and worse scroll behavior.
- Evidence:
feature/search/src/main/java/com/shuyu/gsygithubappcompose/feature/search/SearchScreen.kt:89,feature/search/src/main/java/com/shuyu/gsygithubappcompose/feature/search/SearchScreen.kt:164,feature/profile/src/main/java/com/shuyu/gsygithubappcompose/feature/profile/ProfileShared.kt:72,feature/detail/src/main/java/com/shuyu/gsygithubappcompose/feature/detail/file/RepoDetailFileScreen.kt:71 - Fix direction: add
key = { ... }everywhere identity matters, and stop indexing into lists whenitems(list, key = ...)oritemsIndexed(...)is available. - References: https://developer.android.com/develop/ui/compose/lists
-
State management: Android screens collect flows without lifecycle awareness
- Why it matters:
collectAsState()keeps collecting even when the screen is stopped, which wastes work and can keep screen state hot longer than intended. - Evidence:
app/src/main/java/com/shuyu/gsygithubappcompose/MainActivity.kt:49,feature/search/src/main/java/com/shuyu/gsygithubappcompose/feature/search/SearchScreen.kt:34,feature/detail/src/main/java/com/shuyu/gsygithubappcompose/feature/detail/RepoDetailScreen.kt:106,feature/profile/src/main/java/com/shuyu/gsygithubappcompose/feature/profile/ProfileScreen.kt:28 - Fix direction: add
lifecycle-runtime-composeand switch Android UI collectors tocollectAsStateWithLifecycle(). - References: https://developer.android.com/develop/ui/compose/state
- Why it matters:
-
Side effects: one screen still launches data loading directly from composition
- Why it matters: composition should stay side-effect free. Calling
loadData(...)in the composable body means recomposition can retrigger work unpredictably. - Evidence:
feature/list/src/main/java/com/shuyu/gsygithubappcompose/feature/list/ListScreen.kt:39 - Fix direction: move initial loading into
LaunchedEffect(userName, repoName, listType)or into the ViewModel init path. - References: https://developer.android.com/develop/ui/compose/side-effects
- Why it matters: composition should stay side-effect free. Calling
-
API quality and performance: shared UI still accepts unstable network/domain models directly
- Why it matters: reusable composables like
UserItem,CommitItem,ProfileHeader, andRepositoryDetailInfoHeaderdepend on unstable models, so the compiler cannot prove stable skipping at the component boundary. - Evidence:
core/ui/build/compose_audit/ui-composables.txt:16,core/ui/build/compose_audit/ui-composables.txt:155,feature/detail/build/compose_audit/detail-composables.txt:26,feature/profile/build/compose_audit/profile-composables.txt:15 - Fix direction: map network/domain models to UI-specific stable models before they cross shared composable APIs, and keep ViewModels out of shared component seams.
- References: https://developer.android.com/develop/ui/compose/architecture, https://developer.android.com/develop/ui/compose/performance/stability
- Why it matters: reusable composables like
What is working
- Compiler diagnostics are enabled and Strong Skipping is on across generated module reports, for example
feature/search/build/compose_audit/release/search-module.json:25andcore/ui/build/compose_audit/release/ui-module.json:25. - Some list-heavy code already uses keys correctly, for example
feature/history/src/main/java/com/shuyu/gsygithubappcompose/feature/history/HistoryScreen.kt:46. GSYPullRefreshusesderivedStateOfplusLaunchedEffectfor load-more triggering instead of polling imperatively:core/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/components/GSYPullRefresh.kt:130.
What is hurting the score
- Stable keys are missing in many
LazyColumn/LazyRowcall sites. - Several reusable composables still take unstable models as direct parameters.
- The compiler reports show one important module with too many inferred unstable classes for a high score.
Performance ceiling check
named-only skippable% = 67/67 = 100.0% -> no cap from skippability band
feature:detail inferred unstable classes = 8 -> falls in ">=8 unstable classes used as params" band -> cap at 4
qualitative score: 6
applied score: 4 (ceiling lowered from 6)
Module-wide aggregate from all generated *-module.json reports is 255/386 = 66.1%, while the named-only aggregate from all generated *-composables.csv reports is 67/67 = 100.0%. This repo is heavily anchored by lambdas, so the named-only metric is the fairer ceiling input, but feature:detail still hits the unstable-class cap on its own.
Evidence
feature/search/src/main/java/com/shuyu/gsygithubappcompose/feature/search/SearchScreen.kt:89— search history list has nokey, so item identity falls back to position. · References: https://developer.android.com/develop/ui/compose/listsfeature/search/src/main/java/com/shuyu/gsygithubappcompose/feature/search/SearchScreen.kt:164— repository/user result lists also omitkey, repeating the same identity problem on bigger result sets. · References: https://developer.android.com/develop/ui/compose/listsfeature/list/src/main/java/com/shuyu/gsygithubappcompose/feature/list/ListScreen.kt:72—items(uiState.list.size)indexes into the backing list instead of using keyed item APIs. · References: https://developer.android.com/develop/ui/compose/listsfeature/detail/src/main/java/com/shuyu/gsygithubappcompose/feature/detail/file/RepoDetailFileScreen.kt:71— file browser rows are also index-based and unkeyed. · References: https://developer.android.com/develop/ui/compose/listscore/ui/build/compose_audit/ui-composables.txt:16—CommitItemtakes unstableRepoCommit. · References: https://developer.android.com/develop/ui/compose/performance/stabilitycore/ui/build/compose_audit/ui-composables.txt:155—UserItemtakes unstableUser. · References: https://developer.android.com/develop/ui/compose/performance/stabilityfeature/detail/build/compose_audit/detail-composables.txt:26—RepositoryDetailInfoHeadertakes unstableRepositoryDetailModel. · References: https://developer.android.com/develop/ui/compose/performance/stabilityfeature/detail/build/compose_audit/release/detail-module.json:15— compiler report showsfeature:detailhas8inferred unstable classes, which triggers the rubric cap. · References: https://developer.android.com/develop/ui/compose/performance/stability/diagnose, https://developer.android.com/develop/ui/compose/performance/tooling
What is working
- Most screen-level loading still comes from ViewModels rather than ad-hoc local state, for example
feature/trending/src/main/java/com/shuyu/gsygithubappcompose/feature/trending/TrendingScreen.kt:20andfeature/issue/src/main/java/com/shuyu/gsygithubappcompose/feature/issue/IssueScreen.kt:67. - Local ephemeral UI state is generally kept local, such as
openAboutDialogandopenFeedbackDialoginfeature/home/src/main/java/com/shuyu/gsygithubappcompose/feature/home/HomeScreen.kt:68.
What is hurting the score
- Android UI code consistently uses
collectAsState()instead ofcollectAsStateWithLifecycle(). GSYMarkdownInputDialogremembers prop-derived text state without keys, so changinginitialTitle/initialTextwhile the dialog stays composed can leave stale text on screen.- Repo detail sub-screens read screen-wide ViewModels through
CompositionLocal, which hides dependencies that should stay explicit at screen boundaries.
Evidence
app/src/main/java/com/shuyu/gsygithubappcompose/MainActivity.kt:49— activity-level language flow is collected with plaincollectAsState(). · References: https://developer.android.com/develop/ui/compose/statefeature/search/src/main/java/com/shuyu/gsygithubappcompose/feature/search/SearchScreen.kt:34— the search screen collects eleven flows with plaincollectAsState(), multiplying off-screen collection cost. · References: https://developer.android.com/develop/ui/compose/statefeature/detail/src/main/java/com/shuyu/gsygithubappcompose/feature/detail/RepoDetailScreen.kt:76— multiple ViewModels are injected throughCompositionLocal, which weakens explicit state ownership and dependency clarity. · References: https://developer.android.com/develop/ui/compose/architecture, https://developer.android.com/develop/ui/compose/compositionlocalfeature/detail/src/main/java/com/shuyu/gsygithubappcompose/feature/detail/file/RepoDetailFileScreen.kt:33— child screen state depends onLocalRepoDetailFileViewModelrather than an explicit parameter. · References: https://developer.android.com/develop/ui/compose/architecture, https://developer.android.com/develop/ui/compose/compositionlocalcore/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/components/GSYMarkdownInputDialog.kt:69—remember { mutableStateOf(TextFieldValue(initialTitle ?: \"\")) }has no key, so it will not reset wheninitialTitlechanges. · References: https://developer.android.com/develop/ui/compose/statecore/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/components/GSYMarkdownInputDialog.kt:70—initialTexthas the same stale-cache problem. · References: https://developer.android.com/develop/ui/compose/state
What is working
- Most initial loads are correctly wrapped in
LaunchedEffect, for examplefeature/profile/src/main/java/com/shuyu/gsygithubappcompose/feature/profile/ProfileScreen.kt:31,feature/trending/src/main/java/com/shuyu/gsygithubappcompose/feature/trending/TrendingScreen.kt:24, andfeature/detail/src/main/java/com/shuyu/gsygithubappcompose/feature/detail/file/RepoDetailFileScreen.kt:42. GSYPullRefreshcorrectly usesLaunchedEffect(shouldLoadMore)for reactive paging instead of mutating state during composition:core/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/components/GSYPullRefresh.kt:150.
What is hurting the score
ListScreenstill performs initial loading directly in composition.- Toast/event collection is duplicated between
BaseScreen,SearchScreen,HomeScreen, andRepoDetailScreeninstead of following one consistent event pattern.
Evidence
feature/list/src/main/java/com/shuyu/gsygithubappcompose/feature/list/ListScreen.kt:39—listViewModel.loadData(...)is called during composition. · References: https://developer.android.com/develop/ui/compose/side-effectsdata/src/main/java/com/shuyu/gsygithubappcompose/data/repository/vm/BaseScreen.kt:24— generic toast collection is hard-coded into a wrapper viaLaunchedEffect(Unit), which makes event handling implicit and spreads side-effect policy across the tree. · References: https://developer.android.com/develop/ui/compose/side-effectsfeature/search/src/main/java/com/shuyu/gsygithubappcompose/feature/search/SearchScreen.kt:52— screen-specific toast collection duplicates the same responsibility already present inBaseScreen. · References: https://developer.android.com/develop/ui/compose/side-effectsfeature/detail/src/main/java/com/shuyu/gsygithubappcompose/feature/detail/RepoDetailScreen.kt:130— repo-detail event collection is yet another custom event pipeline instead of one screen-level convention. · References: https://developer.android.com/develop/ui/compose/side-effects
What is working
- There are real reusable building blocks such as
GSYCardItem,GSYTopAppBar,GSYPullRefresh, andGSYGeneralLoadState, which gives the codebase actual API seams to improve instead of copy-paste UI. GSYCardItemandGSYTopAppBaralready exposemodifierin the conventional position:core/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/components/GSYCardItem.kt:18,core/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/components/GSYTopAppBar.kt:18.
What is hurting the score
- Many shared/reusable composables still omit
modifier. - Shared APIs accept raw network/domain models directly.
- Navigation still uses string routes and string interpolation even though the project is already on Navigation Compose
2.8.5. - No
@Previewcoverage was found for extracted UI components.
Evidence
core/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/components/UserItem.kt:18—UserItemis reusable UI but does not exposemodifier. · References: https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.mdfeature/profile/src/main/java/com/shuyu/gsygithubappcompose/feature/profile/ProfileShared.kt:90—ProfileHeaderis shared UI withoutmodifier, which limits composition and styling from callers. · References: https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.mdfeature/detail/src/main/java/com/shuyu/gsygithubappcompose/feature/detail/RepoDetailScreen.kt:312—RepoActionButtonalso omitsmodifier. · References: https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.mdapp/src/main/java/com/shuyu/gsygithubappcompose/MainActivity.kt:60— routes are declared as string literals likecomposable(\"welcome\")andcomposable(\"repo_detail/{userName}/{repoName}\"). · References: https://developer.android.com/develop/ui/compose/navigationfeature/search/src/main/java/com/shuyu/gsygithubappcompose/feature/search/SearchScreen.kt:172— runtime navigation is built with string interpolation:navigator.navigate(\"person/${user.login}\"). · References: https://developer.android.com/develop/ui/compose/navigationcore/ui/build/compose_audit/ui-composables.txt:155—UserItemtakes unstableUserdirectly instead of a UI model seam. · References: https://developer.android.com/develop/ui/compose/architecture, https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.mdfeature/detail/build/compose_audit/detail-composables.txt:26—RepositoryDetailInfoHeadertakes unstableRepositoryDetailModeldirectly. · References: https://developer.android.com/develop/ui/compose/architecture, https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.mdcore/ui/build/compose_audit/ui-composables.txt:16—CommitItemtakes unstableRepoCommitdirectly. · References: https://developer.android.com/develop/ui/compose/architecture, https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.md
- Move
listViewModel.loadData(userName, repoName, listType)out of composition and intoLaunchedEffect(userName, repoName, listType)infeature/list/src/main/java/com/shuyu/gsygithubappcompose/feature/list/ListScreen.kt:39. References: https://developer.android.com/develop/ui/compose/side-effects - Add stable
key = { ... }and stop index-based list rendering infeature/search/src/main/java/com/shuyu/gsygithubappcompose/feature/search/SearchScreen.kt:89,feature/search/src/main/java/com/shuyu/gsygithubappcompose/feature/search/SearchScreen.kt:164,feature/list/src/main/java/com/shuyu/gsygithubappcompose/feature/list/ListScreen.kt:72, andfeature/detail/src/main/java/com/shuyu/gsygithubappcompose/feature/detail/file/RepoDetailFileScreen.kt:71. References: https://developer.android.com/develop/ui/compose/lists - Add
lifecycle-runtime-composeand replace Android UIcollectAsState()withcollectAsStateWithLifecycle()starting atapp/src/main/java/com/shuyu/gsygithubappcompose/MainActivity.kt:49,feature/search/src/main/java/com/shuyu/gsygithubappcompose/feature/search/SearchScreen.kt:34,feature/detail/src/main/java/com/shuyu/gsygithubappcompose/feature/detail/RepoDetailScreen.kt:106, andfeature/profile/src/main/java/com/shuyu/gsygithubappcompose/feature/profile/ProfileScreen.kt:28. References: https://developer.android.com/develop/ui/compose/state - Introduce stable UI models plus
modifierparameters for reusable components such ascore/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/components/UserItem.kt:18,feature/profile/src/main/java/com/shuyu/gsygithubappcompose/feature/profile/ProfileShared.kt:90, andfeature/detail/src/main/java/com/shuyu/gsygithubappcompose/feature/detail/RepoDetailScreen.kt:312. References: https://developer.android.com/develop/ui/compose/architecture, https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.md
- Full-scope compiler diagnostics were generated with the skill init script under module-local
build/compose_audit/directories. - Weight choice: default
35/25/20/20. - Renormalization: none.
- Compiler diagnostics used: yes — examples include
feature/detail/build/compose_audit/release/detail-module.json,core/ui/build/compose_audit/ui-composables.txt, andfeature/profile/build/compose_audit/profile-composables.txt. - The repo is already on Kotlin
2.2.10, Compose Runtime1.7.6, and Navigation Compose2.8.5, so newer guidance like Strong Skipping defaults and typed navigation routes applies here. - No
@Preview-annotated extracted components were found during the audit.
- Run a
material-3audit next. This repo has many directColor.White,Color.Gray,Color.Red, and explicit UI styling choices, but design-system quality was intentionally out of scope for this v1 audit.