Fueled · NationsBenefits
NationsBenefits Android App
One of several Android developers and a code owner on a large Compose app.
- Role
- Senior Android Developer
- When
- Sep 2022 – Oct 2023
- Platforms
- Android
- Industry
- Healthcare, E-commerce
- Release
- Live on Google Play as Benefits Pro.
About the app
Benefits Pro is for members of health plans that include spending benefits. Members check their benefit-wallet balances, then shop approved over-the-counter and health products and pay with those benefits. They can also scan a product to check if it is covered, find stores and manage a flex card.
What I built
- Built most of the checkout flow with Apollo GraphQL. I created the checkout module and wrote about two-thirds of its current code: the forms and validation, the address picker, the split between what benefits cover and what the member pays, order review and order confirmation.
- Made the app’s composables skippable (the deep dive below).
- Built core parts of the shared Compose design system: the text-style system, most of the theme colors, a global bottom sheet, and a reusable form system — field models, validation errors and value handlers that the login, payment, shipping and address forms share.
- Improved the build as a code owner: one shared Gradle convention-plugin setup for every module, JaCoCo coverage reports for every build variant, and faster builds by moving Moshi code generation from kapt to KSP.
- On the home screen (another developer created it) I built the animated greeting header, the loading placeholders and the analytics.
Deep dive: making composables skippable
A composable is skippable only when all its parameters are stable — then Compose can skip it during recomposition when nothing changed. One unstable parameter makes it run every time its parent recomposes.
My understanding of Compose stability was vague, so I studied it in depth. I wrote up what I learned in a long pull request, with screenshots of the Compose compiler report results, and guided my teammates on how to keep their composables stable. Then, in one 70-file pull request, I fixed the problems the reports found:
- Lists. The compiler treats
Listas unstable, because it could be backed by a mutable list. I created a generic@Immutable ImmutableListwrapper and converted lists only at the composable layer, so ViewModel state and domain models keep plainList. 186 files now use the wrapper. - Value classes and dates. I replaced value-class parameters with primitives and stopped
passing
java.timetypes straight into composables. - Annotations. I fixed
@Immutableimported from the wrong package, and warned the team that@Immutableis a promise to the compiler, not a check.
My evidence is the compiler report: composables that were not skippable became skippable. I did not measure recomposition counts, so I never quote one.
Technical notes
-
Turning on the compiler reports. In 2022, on Kotlin 1.x, the reports were switched on with compiler flags. I put them behind a Gradle property, so they ran only when asked for (
-PcomposeCompilerReports=true):kotlinOptions { freeCompilerArgs += listOf( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=$reportsDir", "-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=$metricsDir", ) }Since Kotlin 2.0, the Compose Compiler Gradle plugin has its own block for this:
composeCompiler { reportsDestination = layout.buildDirectory.dir("compose_reports") metricsDestination = layout.buildDirectory.dir("compose_metrics") } -
A pull-request report I built. A Gradle task read each module’s
composables.csvand built an HTML report per module, plus an index with each module’s count of unskippable composables. A custom fastlane action, written in Ruby, ran that task in CI, read the index and posted the counts as a table in a comment on the pull request. It was a report, not a gate. -
A crash from duplicate list keys: the review screen crashed when the cart held the same product twice, paid from two wallets, because both rows produced the same
LazyColumnkey. I built the key from the product ID, item number and wallet code. -
What I’d do today: since Kotlin 2.0.20, strong skipping is on by default, so I’d reach for
kotlinx.collections.immutableor a stability-configuration file instead of a hand-rolled wrapper. Stability still matters: a new list with the same content is a new instance.
Showcase
From development and test builds, mostly with test data. The released app may differ.