Kurt CapatanMobile Dev
← All work

Fueled · NationsBenefits

Benefits Pro app icon

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.
  • Kotlin
  • Jetpack Compose
  • Multi-module
  • MVI
  • Hilt
  • Coroutines
  • Flow
  • Apollo GraphQL
  • Gradle convention plugins
  • KSP
  • JaCoCo
Shopping, cart and the three-step checkout ending in an order confirmation
Checkout, end to end with Apollo GraphQL. I built the checkout module and its screens.

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 List as unstable, because it could be backed by a mutable list. I created a generic @Immutable ImmutableList wrapper and converted lists only at the composable layer, so ViewModel state and domain models keep plain List. 186 files now use the wrapper.
  • Value classes and dates. I replaced value-class parameters with primitives and stopped passing java.time types straight into composables.
  • Annotations. I fixed @Immutable imported from the wrong package, and warned the team that @Immutable is 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.csv and 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 LazyColumn key. 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.immutable or a stability-configuration file instead of a hand-rolled wrapper. Stability still matters: a new list with the same content is a new instance.