Rewrite the app in Kotlin Multiplatform and Compose for 2.0 - #6
Merged
Merged
Conversation
Replaces the Java/Activities/raw-SQLite app with Kotlin Multiplatform, Compose and SQLDelight, shipped to the same Play listing as versionCode 7. The old app is deleted rather than migrated in place. Almost nothing survived contact with the rewrite: the schema changed, the UI is entirely new, and the only real continuity is the package name and the data users already have. What is new: - Scanning expiry dates off a label with ML Kit, with the GenAI Prompt API handling awkward formats on devices that support it - A Glance home screen widget with four layouts chosen by available size - Photos and notes per item, search, custom categories, locale-aware date formats, and reminders at a configurable lead time and hour - Eight languages, machine translated and not yet reviewed by speakers Carrying over from 1.5: - LegacyImporter reads the old database and image cache on first launch. It is idempotent and leaves the old data untouched. This is the only path that can lose data users cannot recover, so it has the most tests. Tooling: - ktlint, detekt and Android Lint, all reachable through ./gradlew ciCheck - GitHub Actions running that task on push - 188 unit tests and 10 instrumented tests minSdk moves from 26 to 31, which strands users on Android 8 to 11 on 1.5.
A single ciCheck step meant a red run said only that something failed. One step per gate names it in the job summary instead. Each step runs even when an earlier one failed, so one push reports every problem rather than one per round trip. Also fixes the branch: the workflow triggered on main and gated the release bundle on refs/heads/main, neither of which exist here. The default branch is master, so push builds and the bundle job have never run.
.kotlin/ is the Kotlin plugin's per-build session directory. It appears at the root the first time the project is opened and was not covered by any existing rule, so it would have shown up as untracked on a clean checkout. Also scopes the Inspect Code export rules to the root. index.html, script.js and styles.css are generic enough that an unanchored rule could swallow a real file.
The rewrite's README dropped the store link, the badge and the license line, and replaced the original About/Features/Technicalities shape with something longer. This keeps the old skeleton and updates the contents for 2.0.
The README is for someone deciding whether to install the app, so build commands, module layout, signing and the migration internals do not belong in it. AGENTS.md also collects the things that have already gone wrong here and are invisible in the code: R8 stripping ML Kit's reflective constructors, dex refusing commas in test method names, Robolectric double-starting Koin.
Steps inside a single job all report as one check. Separate jobs surface as CI / Format, CI / Lint, CI / Unit tests and so on, which is what the checks list and branch protection can actually see. A matrix rather than five near-identical jobs. Each compiles independently, which the Gradle cache mostly absorbs.
The job logs already say what failed, and nobody was downloading the HTML. The release bundle upload stays, since that is a build output rather than a report.
CI died on 'Attempt 1/1 failed: Unexpected end of file from server' while fetching the distribution zip. The wrapper was configured with retries=0, so a single dropped connection failed the job, and a 10s timeout is tight for a 130 MB download behind a redirect. Three retries with a 1s backoff, and a 60s timeout.
gradle/gradle-daemon-jvm.properties pinned the daemon to JDK 25, so every build ran there whatever JAVA_HOME said. detekt 1.23 embeds a Kotlin compiler that cannot parse a 25.x version string and died with 'IllegalArgumentException: 25.0.3' before analysing anything, which is what failed CI. Dropping the generated toolchainUrl entries with it: they pointed at JDK 25 downloads and would be wrong for a 21 request. Gradle now uses the locally installed 21 that dev machines and the runner already have. With detekt actually running, three real findings surfaced and one config bug: - BADGE_ALPHA and BORDER_ALPHA in UrgencyStyle.kt were dead - MagicNumber listed excludes, which replaces detekt's defaults instead of adding to them, so the test sources lost their exemption and 55 deliberate literals in tests were flagged - parseIsoish trips ReturnCount at 7 returns, all of them guard clauses, which is the house style; guards are no longer counted Also renames UrgencyBadge.kt to UrgencyStyle.kt. It holds no UrgencyBadge, which ktlint's filename rule caught and which had been failing since the file was written.
filteringByCategoryHidesTheOtherCategories matched the Medicine chip by text and clicked it. The filters are a LazyRow, so a chip that does not fit on screen is never composed and cannot be found. It passed on a 1080x2400 emulator and failed on CI's narrower one. The row now carries a test tag and the test scrolls to the chip, and takes the label from resources rather than hardcoding English. Also raises the waitUntil timeout from 5s to 15s. Three tests timed out on CI purely because a cold emulator is slower than a warm local one, which reports as a behaviour failure rather than as the machine being busy.
Booting an emulator cost most of ten minutes on every push and was the flakiest thing in the pipeline, so the e2e job is gone. The instrumented tests stay in the repo behind ./gradlew ciCheckDevice for when a real device is the point. What replaces them runs on the JVM under Robolectric, using the same Compose test API, with MockK standing in for the repositories: - ItemListComponentTest: urgency grouping, the attention count, category filtering - AddItemComponentTest: what a save actually sends, the reopen-after-save regression, the empty-name case, and the scan button gated off Both hold time still. Clock.System was read at six call sites, which made every assertion about 'expires in five days' depend on the day it ran and the timezone it ran in. Today is now injected and fixed at 2026-06-15 in tests. Writing these found what CI had been failing on. The form is taller than the sheet, so on a short screen Save sits below the fold, and performClick on an off screen node silently does nothing: the sheet never closed and the wait timed out 15 seconds later looking like a slow machine. My emulator is tall enough to show the button, CI's was not. Both suites now scroll to Save first.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rewrite of the app in Kotlin Multiplatform and Compose, shipped to the existing
Play listing as versionCode 7. Already published.
New
Changed
LegacyImporterimports the 1.5 database and image cache on first launch, once, leaving the old data in place./gradlew ciCheck, run by GitHub ActionsKnown gaps