Open-source project
phansier/Coffeegram avatar
phansier/Coffeegram

Coffeegram is a coffee tracker that runs on five platforms from one Kotlin codebase

Android, Desktop, iOS apps using Jetpack & Multiplatform Compose

536 stars41 forksKotlinApache-2.0

At a glance

What is it?
Coffeegram is a Kotlin and Compose Multiplatform project, and it is interesting as much for its build tooling as for its feature set. One codebase targets Android, iOS, desktop JVM, the browser through WebAssembly and a Wear OS watch, with a Makefile that encodes the whole development loop and a tool that turns Compose compiler metrics into a report.
Who is it for?
Adopt Coffeegram as a reference if you are starting a Compose Multiplatform project, since the repository layout and the Makefile show how the shared and platform code is separated and how each target is built and tested. Skip it if you only need an Android app, since the multiplatform setup is the cost and the app itself is a coffee log.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Kotlin, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A sample app that is really a build system

Coffeegram is described as a Kotlin and Compose Multiplatform project, and the repository title says it in one line. The description at the repository level is Android, Desktop and iOS apps using Jetpack and Multiplatform Compose.

The application itself is a coffee tracker, judging by the package identifier on the Play Store listing and the screenshots, which cover a month table, a coffee list and several settings screens. It is a Google Play app, so it is a real application rather than a demo, but it is also clearly a vehicle for demonstrating a particular way of working.

That vehicle matters because the choices it makes are the ones you would otherwise have to make yourself. The version badges are the first clue: Jetpack Compose at 1.10.4, Compose Multiplatform at 1.10.2, and Kotlin at 2.3.10. Three different version numbers for what a newcomer would think of as one thing, because Jetpack Compose and Compose Multiplatform are released separately from Kotlin and from each other. A project that tracks all three explicitly is telling you that the composition is part of the work.

The topics confirm the surface. Kotlin, Kotlin Multiplatform, Compose, Compose Multiplatform, Jetpack, coroutines, MVI, wasm and wear-compose.

There is an Android CI workflow badge and a Hits-of-Code badge, which is a measure of the size of the codebase rather than its quality. For a sample project, the size matters: it tells you how much of the file you would be reading.

The project is Kotlin, Apache-2.0 licensed, with the default branch develop, the last push on 2026-10-16 and a homepage hosted on Cloudflare Pages. The release tags are interesting, because they are named after the toolchain rather than the app: Kotlin-2.1.20, Kotlin-2.0.0 and a wasm tag for Compose WASM.

That convention says the project's releases are cut when the Kotlin version changes, which is a reasonable model for a sample whose whole point is to track the platform.

The Android feature list, and what each Jetpack is doing there

The Android section lists seven specific features, and they are a tour of the modern Android stack rather than a list of app capabilities.

Material 3 dynamic colour is the first, described as Harmonized, following the device's theme, plus day and night themes with a switch enabled. Harmonized colour is the Material 3 mechanism that derives a whole palette from one seed colour, and following the device's theme means the app picks up the wallpaper-derived palette the system is already using.

Glance AppWidgets is the second, and that is a different generation of widget API from the one most Android developers know. Glance is built on Compose rather than on RemoteViews, which means a widget is written in the same declarative model as the app, and it is the first item on the list that would not have existed a few years ago.

Compose for Wearable is the third, and it means the watch app is written in the same toolkit as the phone app. There is a wear/ directory at the root and a dedicated build target for it, so it is a real target rather than an aspiration.

Jetpack Datastore is the fourth, which is the modern replacement for SharedPreferences with coroutine-friendly and typed APIs.

Room Multiplatform as a database is the fifth, and it is the one with the most engineering behind it. Room is the Android database layer that generates code from annotations, and making it multiplatform means the same database code runs on iOS and desktop as well as on Android, which is the single biggest obstacle to sharing an application's data layer across platforms.

The remaining two are polish. There is an animated splash for Android 12, which is the platform's own splash screen API, so the app hands over to it rather than drawing its own. And there is an Android 13 monochrome icon, with a documentation file and a screenshot of the icon after clipping.

The monochrome icon is worth a moment. Android 13 introduced themed icons, where the system takes your icon and applies a theme, and it only works if you supply a proper monochrome variant. That most sample projects do not bother is the point, and the fact that this one does, with documentation and a picture of the result, is the difference between a project that runs and one that looks right on a modern device.

iOS and desktop, and what a shared UI actually means

The iOS section is short but it names the important dependency. The native iOS look and feel comes from Compose Cupertino.

Compose Cupertino is a project that reimplements the iOS look in Compose, and it exists because the standard Compose Multiplatform widgets look like Material on every platform. Without it, a Compose Multiplatform app on iOS looks like an Android app, which is exactly the thing users of an iOS app dislike most.

That dependency is the real content of the iOS support. The code is shared, but the widget library is not, and accepting that split is what makes a shared UI acceptable on a platform with strong conventions of its own.

Running on iOS goes through the Kotlin Multiplatform plugin, with the documentation linking to the plugin release notes.

Desktop is next, and it is the target that feels most like a native application development, which is the point of it. Running on the desktop JVM is:

bash
`./gradlew run`

And there is a separate target for hot reload:

bash
`./gradlew runHot`

Hot reload on the desktop JVM is a feature that changes how you work. Compose has a live reload mechanism that applies code changes to a running desktop application without restarting it, and in the Makefile the hot run target passes an auto-reload flag to a gradle task, so the developer experience is the one you would want from a desktop IDE rather than the one you would get from a mobile build cycle.

That is the argument for having a desktop target at all. The phone and the watch are the delivery targets, and the desktop JVM is where you develop quickly.

The WASM target is the fifth, and it is the newest idea in the list. Running it in the browser is:

bash
`./gradlew :cmp-common:wasmJsBrowserDevelopmentRun`

or, through the Makefile:

bash
`make runWasm`

The repository also carries a kotlin-js-store/ directory, which is where the npm package metadata for the JavaScript and WebAssembly output lives, and the release tags include one for Compose WASM.

The Makefile is the documentation

The Makefile at the root is the most useful file in the repository for understanding how the project works, and it deserves reading closely.

It is a thin wrapper over Gradle. Every target is a gradle invocation with the console set to plain output, and the targets are named after what they produce rather than how they produce it.

The lint target runs detektAll, and there is an autodetekt variant that adds the auto-correct flag and continues past failures. Detekt is the static analysis tool for Kotlin, and having an auto-correct mode in the build means the formatting and style issues are fixed by tooling rather than argued about in review.

Then there is one target per output. testApp runs the debug unit tests. uiBuild compiles the Android instrumentation test Kotlin. buildApp assembles the debug app. buildWear assembles the debug wear app. buildDesktop builds the JVM jar of the common module. And testCommon runs the Android host tests of the common module, which is the shared code, so the shared code is tested on the Android host without needing a device.

Then the target that ties them together:

bash
localCheck: detekt testApp buildApp buildWear buildDesktop testCommon

That is detekt, then the app tests, then the app build, then the wear build, then the desktop build, then the common tests. One command that runs the whole matrix, and the order is deliberate: the cheap checks first and the builds last.

The wasm targets follow the same pattern, with runWasm and buildWasm invoking the wasmJs browser tasks with the configuration cache disabled. That flag is worth noticing. The configuration cache is a Gradle optimisation that caches the result of configuration, and it is well known to break on tasks that depend on the environment, which for a browser run task means the browser launch. Turning it off for those specific tasks is the pragmatic fix.

There is a buildWeb target using the compose compatibility browser distribution, and a buildHealth target, which in the Compose ecosystem is the Gradle task that reports on the health of the Compose compiler across the project.

There is also an unsafe_clear_branches target, and it is named that way for a reason. It fetches with prune, works out which remote branches have no local counterpart, and deletes them. The name and the comment pointing at the Stack Overflow answer it came from are both good practice for a task that deletes things.

The Compose report and what it tells you

One Makefile target is unusual enough to deserve its own look, and it is the one that says the most about what the project is for.

The compose_report target runs a jar, composeReport2Html.jar, with a set of arguments that name the app and point at four metrics files:

bash
	java -jar composeReport2Html.jar \
      -app Coffeegram \
      -overallStatsReport app/build/compose_metrics/app_release-module.json \
      -detailedStatsMetrics app/build/compose_metrics/app_release-composables.csv \
      -composableMetrics app/build/compose_metrics/app_release-composables.txt \
      -classMetrics app/build/compose_metrics/app_release-classes.txt \
      -o app/build/compose_report/

The files are the Compose compiler's own output: an overall stats report as JSON, detailed stats metrics as CSV, composable metrics as text, and class metrics as text, all under the app's compose_metrics directory for the release module. The output goes to a compose_report directory.

What this does is turn the Compose compiler's performance analysis into a readable report. The Compose compiler emits metrics about how composables compile, and they tell you which ones were skipped, which were marked restartable, and how the recomposition scope ended up. A composable that the compiler could not optimise is a recomposition that will run more often than you think.

Having that as a build target means the metrics are part of the daily workflow rather than something you go looking for when a screen feels slow. That is a real practice, and the linked project is the tool that renders the JSON and CSV into HTML.

The other artefacts in the repository root are consistent with a project of this kind. There is a build-logic/ directory, which is where custom Gradle convention plugins live, and that is the modern answer to the buildSrc pattern. There is a detektConfig/ directory with the static analysis rules. There is a local.defaults.properties file for machine-specific settings that should not be in version control. There is a cleanup.sh, a CLAUDE.md for working with automated agents, an .editorconfig, and a PDF about Compose for Wearables and Widgets.

The module layout is the part worth mapping out. There is cmp-common/ for the shared multiplatform code, cmp-iosApp/ and iosApp/ for the iOS application, app/ for the Android application, wear/ for the watch, repository/ with two implementations in repository-room/ and repository-sqldelight/, and date-time-utils/ for the date handling that every platform would otherwise implement differently.

The two repository implementations are the most interesting part of that layout. Having both a Room and a SQLDelight implementation means the data layer is written against an interface, and the project can demonstrate the trade between the two rather than only asserting that the abstraction is a good idea.

Where this fits

The honest way to read Coffeegram is as a reference implementation with a very particular emphasis.

The emphasis is on the build. Everything in the repository, the Makefile, the build-logic directory, the detekt configuration, the Compose report target, the local check chain, points at the idea that the multiplatform build is the hard part and should be scripted rather than remembered.

That emphasis makes sense because the alternative is what most people do, which is to open the IDE, click run, and figure out the command lines later if they ever need them. The result is that nobody knows how to build the watch app, and nobody knows which checks have to pass, and the build becomes folklore.

The second emphasis is on the data layer. Two repository implementations behind one abstraction is the pattern that makes sharing code across platforms viable in practice, and it is the part of a multiplatform project most likely to be gotten wrong on a first attempt.

The third is that the platform-specific features are the modern ones. Glance widgets, Compose for Wear OS, Room Multiplatform, the monochrome themed icon. A sample that taught the older approaches would be worse than useless, and these are the ones that a new project should copy.

The limits are straightforward. This is a coffee tracker, so the app logic is not a realistic workload, and a real application would have more platform-specific behaviour than this one does. The iOS support depends on a third-party Cupertino library rather than on first-party Cupertino components in Compose Multiplatform, so it is a dependency on somebody else's compatibility work. And the sample is behind whatever the newest Compose and Kotlin releases are, since the badges are updated to the current versions.

That last point cuts both ways. It means the project is a good place to look for how the current toolchain is set up, and it means a tutorial written from it may go stale quickly. The linked write-ups, the ProAndroidDev article on the transition to Jetpack Compose and coroutines, the Habr post from the Kaspersky blog and the AppsFest talk, are the part of the repository that explains the reasoning behind the code.

Editorial conclusion

Adopt Coffeegram as a reference if you are starting a Compose Multiplatform project, since the repository layout and the Makefile show how the shared and platform code is separated and how each target is built and tested. Skip it if you only need an Android app, since the multiplatform setup is the cost and the app itself is a coffee log. Verify first which Compose and Kotlin versions the badges reflect against your own toolchain, since the project tracks recent releases closely, and how the local check task behaves, because it runs the whole test matrix rather than a subset.

Frequently asked questions

What platforms does Coffeegram target and what does it use?

It is a Kotlin and Compose Multiplatform project covering Android, iOS, desktop JVM, the browser through WebAssembly and Wear OS. The Android build uses Jetpack Compose 1.10.4 with Material 3 dynamic colour, Glance AppWidgets, Compose for Wearable, Jetpack Datastore and Room Multiplatform as the database, while iOS gets its native look from Compose Cupertino. The versions tracked are Kotlin 2.3.10 and Compose Multiplatform 1.10.2.

How do I run Coffeegram on the desktop and in the browser?

On the desktop JVM you run `./gradlew run`, and `./gradlew runHot` gives you hot reload. For WebAssembly you run `./gradlew :cmp-common:wasmJsBrowserDevelopmentRun` or `make runWasm`, and the Makefile also has buildWasm, buildWeb using the compose compatibility browser distribution, and buildHealth for the Compose compiler health report.

What does the Coffeegram Makefile do?

It wraps the Gradle tasks with plain console output and one target per output: detekt and autodetekt for static analysis, testApp, uiBuild, buildApp, buildWear, buildDesktop and testCommon for the various targets, plus runDesktop, hotRun, runWasm, buildWasm and buildWeb. The localCheck target runs detekt, the app tests, the app build, the wear build, the desktop build and the common tests in that order, and a separate compose_report target turns the Compose compiler metrics into an HTML report.

Official sources

  1. License: Apache-2.0
  2. phansier/Coffeegram on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/phansier-coffeegram.svg)](https://hysenlabs.com/projects/phansier-coffeegram)

Was this useful?