Sample audits · Open-source projects, first eight

android/architecture-samples

Google's reference Android app.

Auditedandroid/architecture-samples at commit ee66e1526b84c026615df032c705842b7d2a521f
Date11 October 2026
How it ranAPI run on the Nacodex server, full audit, Standard review
Verdict after reviewPass with notes (rule: Fail if a High finding remains after review, otherwise Pass with notes)
0
High after review
2
Medium after review
15
Low after review
12
Info after review
0
excluded on review
How to read this page

Each finding keeps the number it has in the audit report. The rating shown first is the one after review; the first automated rating is listed with it. 10 findings were first rated Medium or High; 2 of them are Medium or High after review. Text marked "From the report" is quoted from the audit report; fixes are suggestions and were not tested. Code locations link to the file and line at the audited commit.

Medium after review (2)

14. Medium Refresh can overwrite local changes that have not reached the network yet
First rating: Medium · Reviewed rating: Medium · Review: confirmed
From the report
Evidence
val remoteTasks = networkDataSource.loadTasks()
localDataSource.deleteAll()
localDataSource.upsertAll(remoteTasks.toLocal())
Why it matters

Every local write pushes the whole table to the network in a background job that returns immediately. refresh() then replaces the local table with whatever the network holds. clearCompletedTasks() calls refresh() straight after the local delete (TasksViewModel.kt:108-110), and a pull-to-refresh right after an edit has the same race. If the read reaches the network before the push, cleared tasks come back, and new or edited tasks revert or disappear.

Suggested fix, not tested

Do not replace local rows that have unsynced changes. Track a pending-sync flag or version per task and merge by row, or wait for any outstanding push to finish before pulling. Do not refresh immediately after a local mutation.

Review: Mechanism verified. clearCompletedTasks() deletes locally then calls saveTasksToNetwork() (DefaultTaskRepository.kt:125-128), which is scope.launch { localDataSource.getAll(); ...; networkDataSource.saveTasks } (173-180) and returns immediately. The ViewModel then calls refresh() (TasksViewModel.kt:110 -> 143). Both then queue on the network source's accessMutex (TaskNetworkDataSource.kt:41,46; mutex is FIFO; 2000 ms simulated latency, line 52). The refresh will therefore normally take the lock first, read the old list that still holds the completed tasks, and after 2 s deleteAll() + upsertAll(old) brings the cleared tasks back locally (the network is then corrected by the push, which snapshotted earlier). The same applies to pull-to-refresh within about 2 s of any edit. Not executed; the ordering is inferred from the code, but the report's own hedge ("If the read reaches the network before the push") is accurate and the likely order favours the failure. Upstream: open PR #1067 "Fix clear completed tasks being undone by immediate refresh" (2026-05-23) states the same: "An immediate full refresh can reload stale network data before saveTasksToNetwork() finishes (the fake network has a 2s delay), which can restore completed tasks the user just cleared". It removes the call at TasksViewModel:110 only; the general pull-to-refresh race is not covered.

Upstream: Already reported upstream: #1067.

29. Medium The keyboard covers the add/edit form and the save button
First rating: Medium · Reviewed rating: Medium · Review: confirmed
From the report
Evidence
Scaffold(
    modifier = modifier.fillMaxSize(),
Why it matters

The activity calls enableEdgeToEdge() (TodoActivity.kt:33), so the manifest's adjustResize no longer shrinks the window. Nothing in app/src/main applies IME insets (searched for imePadding and WindowInsets: none found). With the keyboard open, the lower part of the 350dp description field and the save button sit under the keyboard. Users have to dismiss the keyboard to reach the save button.

Suggested fix, not tested

Add Modifier.imePadding() to the form column, or include IME insets in the Scaffold's content insets.

Review: Verified: no imePadding/WindowInsets in app/src/main (grep returned only unrelated ime substrings), targetSdk is 35 (libs.versions.toml: targetSdk = "35"), Compose BOM 2024.12.01, and the FAB sits in the Scaffold at the bottom. With edge-to-edge the window is not resized, so the keyboard overlaps the FAB. Not run, but upstream reports the exact symptom: issue #1025 "Using edgeToEdge breaks windowSoftInputMode" ("buttons hidden behind the keyboard", with screenshots), PR #1028 "Resolve layout issues caused by edgeToEdge and keyboard overlap", PR #1056 "show save button on New task screen when keyboard is displayed" (all open). Fair: MEDIUM (primary form unusable until keyboard is dismissed), for a sample app. ---------------------------------------------------------------------

Upstream: Already reported upstream: #1025, #1028, #1056.

Low after review (15)

1. Low Release shrinker configuration keeps all app code and ships test-only rules
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
# Some methods are only called from tests, so make sure the shrinker keeps them.
-keep class com.example.android.architecture.blueprints.** { *; }
Why it matters

The release build type enables minification with this file. The blanket keep rule, added only so tests can reach some methods, turns off shrinking and obfuscation for the whole app in production. -ignorewarnings (line 19) hides real missing-class problems, and the Espresso keep rules ship in the release APK. The result is a larger, easier-to-reverse-engineer release, and build warnings that could flag broken code are silenced.

Suggested fix, not tested

Move the test-only keep rules to proguardTest-rules.pro, or to a debug-only rules file. Keep only what release actually needs (Room and Hilt entry points) and remove -ignorewarnings.

Review: Factually right. app/build.gradle.kts:55-58: release has isMinifyEnabled = true, proguardFiles(..., "proguard-rules.pro"). proguardTest-rules.pro exists (build.gradle.kts:57), so the author's intent is visible. Impact is small: it is a sample not published to a store; library code and resources are still shrunk; obfuscation is not a security control; no secret is in the app. It is the same rule file the project has carried since 2017. Upstream: issue #306 "[all] Proguard test rules" (open since 2017-03-22) says exactly this ("the proguard-rules.pro file contains a rules which keeps all the app classes... for release builds this seems unneeded"). Build file, not shipped library code.

Upstream: Already reported upstream: #306.

2. Low Test-only idling-resource helper and dependency are in the production build
First rating: Low · Reviewed rating: Low · Review: confirmed
From the report
Evidence
implementation(libs.androidx.test.espresso.idling.resources)
Why it matters

The Espresso idling-resource library is on the production classpath. The helper util/SimpleCountingIdlingResource.kt lives in the main source set although no production code uses it.

Suggested fix, not tested

Move the helper to a debug or test source set (or :shared-test). Change the dependency to debugImplementation or androidTestImplementation.

Review: util/SimpleCountingIdlingResource.kt imports androidx.test.espresso.IdlingResource (:19) and nothing else in the repo references the class (searched all *.kt/*.kts). Dead helper plus a tiny test-support dependency in implementation. Hygiene only.

4. Low Statistics load errors are shown as "no tasks"
First rating: Low · Reviewed rating: Low · Review: confirmed
From the report
Evidence
// TODO: Show error message?
StatisticsUiState(isEmpty = true, isLoading = false)
Why it matters

When loading fails, the screen shows the empty state with no explanation. The user cannot tell a failure from having no tasks.

Suggested fix, not tested

Add a userMessage to StatisticsUiState, as the other screens have, and show the existing "error while loading tasks" string in a snackbar.

Review: StatisticsUiState (lines 37-42) has no message field. Branch is only reachable when the Room flow throws, so rare.

5. Low Background sync failures are swallowed silently
First rating: Low · Reviewed rating: Low · Review: confirmed
From the report
Evidence
} catch (e: Exception) {
    // In a real app you'd handle the exception e.g. by exposing a `networkStatus` flow
Why it matters

If the backup to the network fails, the user is never told their data is not backed up, and a later refresh can replace the unsaved local data (see finding 14). Catching Exception also swallows coroutine cancellation.

Suggested fix, not tested

Rethrow CancellationException. Expose a sync-status or error state that the UI can show, and retry, for example with WorkManager.

Review: True, and the comment plus KDoc (165-172) say so. Catching Exception in scope.launch also swallows CancellationException, but the scope is the application scope so cancellation is not exercised. Related upstream: #858 (open) "refreshTasks() might throw an exception" is about unhandled exceptions in refresh, not this catch.

Upstream: No exact upstream report; related items: #858.

6. Low Text cursor is invisible and the edit fields have no visible boundary
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
cursorColor = MaterialTheme.colorScheme.onSecondary
Why it matters

The app theme does not override onSecondary, so it stays white in the light colour scheme, and the caret is drawn white on a near-white surface. Both fields also have transparent borders in every state. Users get no sign of where to tap or where they are typing.

Suggested fix, not tested

Use a contrasting cursor colour such as colorScheme.primary. Give the fields a visible focused border or a filled container.

Review: TodoTheme.kt:11-15 overrides only primary, secondary, tertiary; onSecondary therefore keeps the Material 3 lightColorScheme default (white) and the page background is the near-white default. So the caret colour is white on near-white: the cursor claim is right (static reasoning from M3 defaults; I did not run the app). The transparent borders are a deliberate look (placeholders show where to type; README says the UI is intentionally simple). Typed text and placeholders remain visible, so this is cosmetic. Fair: LOW.

Upstream: No matching upstream report found (11 October 2026).

8. Low Leftover colour and drawable resources from the old View-based UI
First rating: Low · Reviewed rating: Low · Review: confirmed
From the report
Evidence
<color name="colorPrimary">#FFFFF0</color>
Why it matters

Several colour and drawable tokens, and the drawables built on them, are no longer referenced by any code. attrs.xml still declares an unused styleable. colorPrimary here also contradicts the Compose primary colour. This makes it unclear which colour source is authoritative.

Suggested fix, not tested

Delete the unused resources and keep one source of colour tokens.

Review: colorPrimary and colorTextPrimary have zero references; drawer_item_color, list_completed_touch_feedback, touch_feedback, trash_icon, ic_add, ic_done, ic_edit, ic_menu, ic_assignment_turned_in_24dp are referenced nowhere in app/src or shared-test; the ScrollChildSwipeRefreshLayout styleable is referenced only by its own declaration (attrs.xml). The claim that colorPrimary "contradicts the Compose primary" is moot since it is unused. (PR #1078 also removes the leftover styleable.)

10. Low Task-detail back arrow does not mirror in right-to-left locales
First rating: Low · Reviewed rating: Low · Review: confirmed
From the report
Evidence
Icon(Icons.Filled.ArrowBack, stringResource(id = R.string.menu_back))
Why it matters

The app declares RTL support, and the add/edit bar uses the auto-mirrored icon. The detail bar uses the deprecated non-mirrored one, so its arrow points the wrong way in RTL languages.

Suggested fix, not tested

Use Icons.AutoMirrored.Filled.ArrowBack in both places.

11. Low Task list items have no stable key
First rating: Low · Reviewed rating: Low · Review: confirmed
From the report
Evidence
items(tasks) { task ->
Why it matters

After a refresh, a filter change or a completion toggle, item identity and scroll position follow the list index rather than the task, so the view can jump or animate the wrong row.

Suggested fix, not tested

Use items(tasks, key = { it.id }).

Review: No key =. Minor scroll/animation identity issue.

12. Low Two pull-to-refresh implementations, and the edit screen's loading state is blank
First rating: Low · Reviewed rating: Low · Review: confirmed
From the report
Evidence
SwipeRefresh(
Why it matters

The shared loading container uses the deprecated Accompanist SwipeRefresh, while the add/edit screen uses Material 3 PullToRefreshBox. In the add/edit screen, isRefreshing is never set to true, onRefresh does nothing and the content is empty, so the loading state is a blank screen. An Accompanist theme dependency is also carried that no code uses.

Suggested fix, not tested

Move to Material 3 PullToRefreshBox throughout. Show a progress indicator while the edit form loads, and drop the unused Accompanist dependencies.

Review: All as described; accompanist.appcompat.theme is in build.gradle.kts:145 with no import anywhere. The blank state lasts only for one local DB read. PR #1078 ("Use the material3 pull-to-refresh instead of accompanist", open) describes the same migration.

Upstream: Already reported upstream: #1078, #955.

18. Low Background pushes are unordered, so an older snapshot can overwrite a newer one
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
private fun saveTasksToNetwork() {
    scope.launch {
        val localTasks = localDataSource.getAll()
Why it matters

Each mutation launches an independent job that reads a snapshot and then waits for the network. With quick successive edits, the jobs can reach the network in a different order from the one in which they took their snapshots. The last write then carries stale data, and the next refresh brings the stale data back into the app.

Suggested fix, not tested

Keep a single sync job. If one is in flight, mark it dirty and run it once more when it finishes, or use a conflated channel or WorkManager unique work, so the final push always carries the latest snapshot.

Review: Each push is an independent job (snapshot, then saveTasks). Reordering requires job B to take its snapshot after job A yet reach the FIFO accessMutex (TaskNetworkDataSource.kt:46) first: a window of well under a millisecond, while user edits arrive at human speed and each push then holds the lock for 2 s. A later snapshot cannot overtake while an earlier push holds the lock. With a real concurrent HTTP backend the risk would be larger, but this repo ships only the in-memory simulation, and the KDoc (165-172) tells readers to use WorkManager. Fair: LOW.

Upstream: No matching upstream report found (11 October 2026).

19. Low Double-tapping save creates duplicate tasks
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
private fun createNewTask() = viewModelScope.launch {
    taskRepository.createTask(uiState.value.title, uiState.value.description)
Why it matters

The save button (AddEditTaskScreen.kt:74) stays active while a save runs, and each call generates a new random id. A second tap before navigation completes creates a second, identical task.

Suggested fix, not tested

Ignore saveTask() while a save is in progress (an isSaving flag or a held job), or generate the id once per screen so repeated calls update the same row.

Review: No in-flight guard; createTask makes a new UUID.randomUUID() per call (DefaultTaskRepository.kt:54-56). The defect is real in code. Whether two taps can land is uncertain: the first createTask only does one Room insert and returns (the network push is non-blocking), then isTaskSaved triggers navigation (AddEditTaskScreen.kt:91-95); the second tap must hit inside that few-ms window or the exit transition. Cannot verify the transition-time input handling without running. Fair: LOW.

Upstream: No matching upstream report found (11 October 2026).

25. Low The local table replace during refresh is not atomic
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
localDataSource.deleteAll()
localDataSource.upsertAll(remoteTasks.toLocal())
Why it matters

The two statements run as separate database operations. List observers can briefly receive an empty table, so the "no tasks" state flickers. If the process dies or the insert fails between the two calls, every local task is gone, including any not yet pushed.

Suggested fix, not tested

Do both in one Room transaction (a @Transaction DAO method such as replaceAll, or withTransaction {}), or upsert the remote rows and delete only the rows missing from the remote list.

Review: Two separate DAO calls (TaskDao.kt:77-78, 99-100), no @Transaction/withTransaction anywhere. Observers of observeAll() can see an empty table between them (brief "no tasks" flash on each refresh). Data loss needs a process death or exception between two calls that run milliseconds apart, and the unsynced rows are exposed to the larger overwrite problem of finding 14 anyway. Fair: LOW (cosmetic flicker + small integrity window).

Upstream: No matching upstream report found (11 October 2026).

26. Low Saving an edit to a task that no longer exists crashes the app
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
) ?: throw Exception("Task (id $taskId) not found")
Why it matters

When the task is missing, AddEditTaskViewModel.loadTask silently shows an empty form (lines 140-143). The task can vanish this way after a refresh (finding 14). If the user fills the form and taps save, updateTask throws inside viewModelScope.launch with no handler, and the app crashes.

Suggested fix, not tested

When the task is null, show a "task not found" message and close the screen. Catch the failure in the ViewModel, or have updateTask return a result instead of throwing.

Review: Code path verified: the exception is thrown inside viewModelScope.launch with no handler, so it propagates to the thread's uncaught-exception handler (app crash). loadTask leaves an empty form for a null task (lines 140-143). The trigger requires the task to be present when the edit screen opens, then absent at save: only via the refresh overwrite window (finding 14/25) with a very fast user, or a concurrent delete. Rare; reviewed LOW. Related open issue #858 (unhandled exception in refresh inside viewModelScope.launch) is a different path.

Upstream: No exact upstream report; related items: #858.

27. Low The task-detail stream declares a non-null type, so the "task not found" state never appears
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
fun observeById(taskId: String): Flow<LocalTask>
Why it matters

The repository exposes Flow<Task?> and the detail ViewModel has a dedicated "task not found" branch. Because the DAO promises a non-null row, a missing or just-deleted task fails inside the stream instead of emitting null. The user sees the generic "error while loading" message and the stream stops. The in-memory test fakes emit null, so the tests hide the difference.

Suggested fix, not tested

Declare Flow<LocalTask?>, map with it?.toExternal(), and add a test that deletes the observed task.

Review: With the real repository it.toExternal() returns a non-null Task, so handleTask(null) can never run: the "task not found" branch is dead on the real path (certain from types). When the row disappears Room (2.6.1, libs.versions.toml:53) cannot emit a valid LocalTask; the stream throws and .catch { emit(Async.Error(R.string.loading_task_error)) } (TaskDetailViewModel.kt:64) shows the generic error (exact exception type depends on Room's generated code; not run). Test hiding is confirmed: FakeTaskRepository.getTaskStream returns Flow<Task?> with firstOrNull (FakeTaskRepository.kt:69-73) and FakeTaskDao.observeById is TODO("Not implemented") (FakeTaskDao.kt:76); no test touches the real DAO stream. Visible effect: an error message instead of "not found", and a brief error state before the delete navigation in TaskDetailScreen.kt:93-97. Fair: LOW.

Upstream: No matching upstream report found (11 October 2026).

28. Low Statistics screen applies the scaffold padding twice
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
modifier = modifier.padding(paddingValues)
Why it matters

The padded modifier goes to the refresh container (line 85) and is also the base of commonModifier (line 77), which the inner column uses. The top-bar and system-bar insets are therefore added twice, and the statistics text sits visibly lower than on the other screens. The screen's own modifier parameter is also reused on both the Scaffold and the content.

Suggested fix, not tested

Pass Modifier.padding(paddingValues) to the content. Apply the insets once on the outer container, and build the inner modifier from a fresh Modifier.

Review: modifier already includes padding(paddingValues) and is applied to the SwipeRefresh container (via LoadingContent, ComposeUtils.kt:49-52) and again, through commonModifier, to the inner Column: the scaffold insets are applied twice in the non-empty state (the empty state renders the Text directly and pads once). Also the screen's own modifier is applied to the Scaffold (:51) and re-used. Cosmetic (extra gap below the top bar). Fair: LOW.

Upstream: No matching upstream report found (11 October 2026).

Info after review (12)

3. Info Jetifier still enabled although the project is fully on AndroidX
First rating: Low · Reviewed rating: Info · Review: could not be settled
From the report
Evidence
android.enableJetifier=true
Why it matters

Jetifier rewrites every dependency at build time. On an AndroidX-only project it only adds build time.

Suggested fix, not tested

Set android.enableJetifier=false and confirm the build still passes.

Review: The setting exists. No com.android.support reference appears in the sources, toml or manifest (only the word supportsRtl), but whether every transitive dependency is AndroidX cannot be established without resolving the dependency graph, so the "set it false" advice is unproven. Costs build time only.

7. Info Drawer header colour is a hard-coded copy of the theme colour
First rating: Low · Reviewed rating: Info · Review: rated too high
From the report
Evidence
val primaryDarkColor: Color = Color(0xFF263238)
Why it matters

The same value is declared in TodoTheme, here, and in colors.xml. The drawer header paints this constant but takes its text colour from the theme, so any theme change (such as a future dark scheme) breaks the pairing.

Suggested fix, not tested

Use MaterialTheme.colorScheme.primary and onPrimary in the drawer header, and delete the constant.

Review: Same value as TodoTheme.kt:12 and colors.xml:19. Used at TodoDrawer.kt:119 with text colorScheme.surface (line 131); works today. Duplication, no defect.

9. Info Most previews use the default Material theme instead of the app theme
First rating: Low · Reviewed rating: Info · Review: rated too high
From the report
Evidence
private fun TasksContentPreview() {
    MaterialTheme {
        Surface {
Why it matters

The previews show a different colour scheme from the one the app renders, so visual reviews are misleading.

Suggested fix, not tested

Wrap every preview in TodoTheme.

Review: MaterialTheme { appears at TasksScreen.kt:233, 283, 315, 333; TodoTheme { at :302 and in TodoDrawer.kt:179 and TopAppBars.kt:194, 204, 214, 224. So "most previews" is false repo-wide: it is 4 of 5 in one file. Preview-only code.

13. Info Stale Compose version entry contradicts the BOM
First rating: Low · Reviewed rating: Info · Review: rated too high
From the report
Evidence
androidxCompose = "1.2.0"
Why it matters

The entry is unused, but it suggests a Compose version far older than the APIs the code relies on. That misleads readers and tooling.

Suggested fix, not tested

Remove it so the Compose BOM is the only version source.

Review: Only occurrence of the key in the repo (no version.ref = "androidxCompose"). Harmless.

15. Info The network model's priority field is dropped in the round trip
First rating: Low · Reviewed rating: Info · Review: rated too high
From the report
Evidence
fun LocalTask.toNetwork() = NetworkTask(
    id = id,
    title = title,
Why it matters

NetworkTask has a priority field that the local and external models do not store. Each push would write it back as null. Nothing in this codebase sets a priority today, but it would be silently erased once a real backend supplies one.

Suggested fix, not tested

Carry priority through the local model (with a Room migration), or remove it from the network model until it is supported.

Review: NetworkTask.priority: Int? = null (NetworkTask.kt) is never set anywhere in the repo; toLocal() (62-67) does not read it either. Hypothetical, as the report admits.

16. Info Each write path repeats "local write, then push" with no single gate
First rating: Low · Reviewed rating: Info · Review: rated too high
From the report
Evidence
localDataSource.updateCompleted(taskId = taskId, completed = true)
saveTasksToNetwork()
Why it matters

Seven methods repeat the same pair of calls. Every current path does call the push, but a future write method that forgets it would silently leave the network copy out of date.

Suggested fix, not tested

Route all mutations through one private helper that performs the local write and schedules the sync.

Review: Seven methods call saveTasksToNetwork() (createTask, updateTask, completeTask, activateTask, clearCompletedTasks, deleteAllTasks, deleteTask). The report concedes "Every current path does call the push". Refactoring suggestion only.

17. Info The "title and description required" rule is enforced only in the UI
First rating: Low · Reviewed rating: Info · Review: rated too high
From the report
Evidence
val isEmpty
    get() = title.isEmpty() || description.isEmpty()
Why it matters

Only the add/edit ViewModel checks this. Repository create and update calls, and data imported from the network, can write rows that break it.

Suggested fix, not tested

Validate in createTask and updateTask, and filter or normalise imported rows.

Review: Real validation lives at AddEditTaskViewModel.kt:70. Task.isEmpty is never read anywhere (grep), so the cited line is dead code. titleForList (Task.kt:37-38) shows the model deliberately tolerates an empty title. UI-level validation is a normal design choice in a sample.

20. Info Overlapping refreshes hide the spinner too early
First rating: Low · Reviewed rating: Info · Review: rated too high
From the report
Evidence
_isLoading.value = true
viewModelScope.launch {
    taskRepository.refresh()
Why it matters

A pull-to-refresh and a menu refresh can run at the same time. The first to finish clears the loading flag while the other is still running. The same pattern appears in the detail and statistics ViewModels.

Suggested fix, not tested

Keep a refreshJob and return early while it is active.

Review: True for Tasks and TaskDetail (TaskDetailViewModel.kt:111-117). The claim "the same pattern appears in the statistics ViewModel" is wrong: StatisticsViewModel.refresh() (63-67) has no loading flag at all. Cosmetic.

21. Info Open design decisions live only in scattered TODO comments
First rating: Low · Reviewed rating: Info · Review: rated too high
From the report
Evidence
// TODO: Show error message?
Why it matters

Unresolved questions, such as error display on statistics and the sync-failure strategy in the repository, are not recorded anywhere a maintainer would look. The only documents in the repository are README.md and CONTRIBUTING.md (searched **/*.md).

Suggested fix, not tested

Keep a short "open decisions" section in a design document and link the TODOs to it.

Review: Same line as finding 4 (StatisticsViewModel.kt:75). Only README.md and CONTRIBUTING.md exist (find). A process remark, not a defect; duplicates 4.

22. Info The core user journey is documented only on an external wiki
First rating: Low · Reviewed rating: Info · Review: rated too high
Location: README.md:21
From the report
Evidence
For more information, see the [app's specification](https://github.com/googlesamples/android-architecture/wiki/To-do-app-specification).
Why it matters

The repository itself contains no worked example of the main flow (create, complete, filter, clear, refresh). The external link can drift from the code.

Suggested fix, not tested

Add a short written walk-through of the main flow to the repository docs.

Review: Accurate quote; not a defect.

23. Info Pure filtering helpers are instance members of the ViewModel
First rating: Low · Reviewed rating: Info · Review: rated too high
From the report
Evidence
private fun filterTasks(tasks: List<Task>, filteringType: TasksFilterType): List<Task> {
Why it matters

filterTasks and getFilterUiInfo hold no state but can only be tested through the ViewModel. The statistics feature already keeps its pure logic in a separate utility file.

Suggested fix, not tested

Move both to top-level functions next to TasksFilterType and unit-test them directly.

Review: Accurate; style preference.

24. Info One-shot snackbar message handling is copied across three screens
First rating: Low · Reviewed rating: Info · Review: rated too high
From the report
Evidence
private val _userMessage: MutableStateFlow<Int?> = MutableStateFlow(null)
Why it matters

The same message state, the show/clear functions and the LaunchedEffect snackbar block are repeated in the tasks, task-detail and add/edit ViewModel/screen pairs (TaskDetailScreen.kt:84, TasksScreen.kt:112, AddEditTaskScreen.kt:98). Fixes such as message de-duplication have to be made three times.

Suggested fix, not tested

Extract one small message holder and one SnackbarMessageEffect composable into util/, and use them in all three places.

Review: Duplication confirmed in TasksViewModel.kt:67 and TaskDetailViewModel.kt:59 and the LaunchedEffect blocks (TaskDetailScreen.kt:84-86, TasksScreen.kt:112, AddEditTaskScreen.kt:98-103). AddEdit keeps the message inside its UiState rather than a separate flow, so it is similar not identical. Duplication in a sample.

Want this for your code?

Upload a ZIP or link a public GitHub repo, pick the areas and the depth, and get findings by severity with fixes.