Project Showcase: Dialed In — From Flutter to Native

7. Apr. 2026 (Heute)

As both a specialty coffee enthusiast and a software developer, it was only a matter of time before I combined these two passions. What started as a Flutter side project has evolved into a fully native, privacy-first espresso tracking app — built from the ground up in Swift for iOS and Kotlin for Android.

This is the story of Dialed In.

The Origin: A Flutter Prototype

The first version of Dialed In was built with Flutter and Dart. It worked — you could log beans, track shots, and share sticker-style images of your extraction data. But as the feature set grew, I kept bumping into Flutter's limitations: platform-specific haptics felt disconnected, the UI never quite matched the native feel of iOS or Android, and integrating with system features like Dynamic Island or iCloud was either impossible or required brittle workarounds.

I made the decision to rewrite everything natively. Not a port — a complete rethinking of the architecture, the UI, and the user experience.

Going Native: Swift + Kotlin

The iOS version is built entirely with SwiftUI and SwiftData, targeting iOS 26. Zero third-party dependencies. Every framework is Apple-native: ActivityKit for Dynamic Island, PhotosUI for image handling, AVFoundation for audio feedback, Charts for data visualization, WidgetKit for home screen shortcuts.

The Android version follows the same philosophy: Jetpack Compose, Material 3 Expressive, and Kotlin with DataStore for persistence and kotlinx.serialization for data handling. Also zero third-party dependencies beyond the Jetpack ecosystem.

Both apps share the same data model and JSON export format, so you can move your data between platforms.

The Bean Vault

The core of Dialed In is the Bean Vault — a visual collection of every coffee you've brewed with. Each bean entry captures:

  • Origin, roast level, and process (Natural, Washed, Honey)
  • Flavor profile with five scored attributes (Acidity, Body, Sweetness, Bitterness, Aftertaste) plus up to three custom attributes you define
  • Blend composition (Arabica/Robusta percentage with a visual bar)
  • Package tracking — weight, roast date with resting day counter, and expiry warnings
  • Photos from your gallery, displayed as a parallax header in the detail view
  • Flavor tags rendered in a custom wrapping FlowLayout

Beans are filterable by roast level and sortable by your personal ranking. Each bean shows its shot history with a grind-size-over-time chart using catmullRom interpolation.

Precision Shot Logging

Every espresso shot can capture 15+ parameters:

  • Grind size — set via an interactive rotary dial (more on this below)
  • Dose in / dose out — with live brew ratio calculation
  • Duration — either a real-time timer with 0.1-second precision or manual picker input
  • Pressure, temperature, RPM, pre-infusion time — the extraction deep cuts
  • Water type — for those who track their water recipes
  • Machine and grinder — linked from your equipment list, with defaults auto-populated
  • Flavor position — plotted on a 2D grid (Sour-Bitter x Weak-Strong) with real-time troubleshooting suggestions

The flavor troubleshooter is one of my favorite details. Based on where you place the dot on the grid, the app immediately tells you what to adjust: "Grind finer", "Increase yield", "Decrease temperature" — context-specific guidance that saves you from Googling extraction theory mid-brew.

The Grind Dial

I spent a disproportionate amount of time on this component, and it shows. The grind dial is a full Canvas-rendered rotary control with:

  • Three-tier tick marks — major ticks at whole numbers, mid ticks at halves, minor ticks for everything else
  • Number labels spaced dynamically based on your grinder's range
  • Drag gesture with dynamic sensitivity — slow drags for precision, fast swipes for big jumps
  • Two-tier haptic feedback — a strong rigid impact on whole-number steps, a lighter tap on sub-steps, powered by pre-allocated UIImpactFeedbackGenerators for zero-latency feedback
  • Click sound — an AVAudioPlayer pool of four instances cycling round-robin so rapid spinning never drops audio. Volume locked at half the phone level. Plays through the silent switch via the .playback audio category
  • Free-wheel mode — for stepless grinders, the dial rotates continuously without snapping
  • Per-grinder configuration — each grinder in your equipment list stores its own min, max, step size, clicks mode, dial label, sound toggle, and haptic toggle

The dial is the most satisfying part of the app to use. It genuinely feels like adjusting a real grinder.

Visual Shot Sharing

Every shot can be turned into a shareable sticker image, designed for Instagram Stories or messages:

  • Photo style — your bean image as the background with a gradient overlay and stats in a frosted glass pill
  • Clean style — transparent background with monospaced typography, perfect for layering on your own photos
  • Customizable colors — seven font colors and seven accent colors, all persisted so your preference sticks
  • Content toggles — choose which data to show: origin, grind, ratio, equipment, date
  • Export options — share to any app, save to Photos, or copy to clipboard

The rendering pipeline on iOS uses ImageRenderer at 3x scale, with a special CGImage path for the clean style to guarantee true transparency in the PNG output. On Android, Compose's GraphicsLayer captures the sticker composable as a bitmap at device density.

Guided Brew Timer

Dialed In isn't just for espresso. The brew timer supports guided recipes for pour-over methods — V60, Chemex, AeroPress, French Press, and custom recipes you create yourself.

Each recipe is a sequence of timed steps (Bloom, Pour, Wait, Stir, Press, Drawdown, Serve) with instructions and water amounts. During a brew:

  • A circular progress ring counts down each step
  • Step dots show your position in the recipe
  • Cumulative water is tracked so you know how much you've poured total
  • Steps auto-advance when their timer reaches zero, with a heavy haptic on each transition
  • Manual steps (duration = 0) wait for you to tap "Next"

The timer doesn't auto-start — you see the full recipe breakdown first, then tap play when you're ready. If you background the app mid-brew, the timer recalculates elapsed time from the real wall clock when you return.

After completion, you can log the brew with your bean, grinder, rating, flavor profile, and notes.

Dynamic Island Integration

This is where going native really pays off. When the brew timer is running, a Live Activity appears on the Dynamic Island and Lock Screen:

  • Compact pill — coffee cup icon, current step name, and counting timer
  • Expanded view (long-press) — recipe name, step X/Y indicator, progress bar, step instruction, and dose info
  • Lock Screen banner — full context without unlocking your phone

The timer text uses Text(date, style: .timer) which lets the OS render the counting animation natively — no periodic updates needed from the app. For pause/resume, I compute a synthetic effectiveStartDate backdated by the accumulated elapsed time. The progress bar uses ProgressView(timerInterval:countsDown:false) for the same OS-driven animation.

The Live Activity updates on every step advance, showing the new step title and instruction. It ends automatically when you finish, reset, or leave the brew view.

Android Foreground Service Notification

On Android, the brew timer runs as a ForegroundService — the system-level equivalent of a Live Activity. An ongoing notification shows:

  • Recipe name and current step
  • Elapsed time with step X/Y indicator
  • Pause and Stop action buttons directly in the notification

The service uses companion-object StateFlows for zero-latency UI synchronization — the same state that drives the notification also drives the Compose UI, so returning to the app after backgrounding shows the exact current state with no recalculation needed.

Equipment & Maintenance

Your gear is tracked with full stats:

  • Machines — name, default pressure/temperature/pre-infusion, water tank volume
  • Grinders — name, RPM, plus the full grind dial configuration (min/max/step/clicks/label/sound/haptic)
  • Brew equipment — kettles, drippers, filters, scales

Each piece of equipment shows its shot count, brew count, and last-used date. When you select a machine or grinder in the shot form, their defaults auto-populate the advanced fields.

The maintenance system lets you create tasks with flexible intervals:

  • By shots — "Clean after every 50 shots"
  • By days — "Descale every 30 days"
  • By water usage — "Replace filter every 200L"

Each task has a progress bar that turns yellow at 80% and red when overdue. You can opt into local notifications that remind you when a task is due — the app estimates timing based on your average usage patterns.

Home Screen Widget

A Glance-powered 2×2 widget shows your total shot count and most recently brewed bean at a glance, with a quick "Add Shot" button that deep-links straight into the shot form. Data flows through a SharedPreferences bridge that updates every time you log a shot.

iCloud Sync & Data Portability

All data syncs automatically via CloudKit's private database — your beans, shots, recipes, equipment, and maintenance tasks stay in sync across all your Apple devices. No account required, no third-party servers.

For cross-platform portability, the entire dataset exports as a clean JSON file with ISO 8601 dates and UUID identifiers. The Android app reads the same format, so migrating between platforms is seamless. The only thing that doesn't transfer is bean photos — they're local files, not embedded in the JSON.

Privacy by Design

Dialed In collects no analytics, no crash reports, no telemetry. There are zero third-party SDKs in the project. The App Store privacy label is "Data Not Collected."

Photos are stored locally in the app's Documents folder. iCloud sync uses Apple's end-to-end encrypted private database. There is no backend server, no user account, no tracking of any kind.

This isn't a privacy afterthought — it's a core design constraint. Every feature is evaluated against it.

The Tech Stack

iOS:

  • Swift / SwiftUI
  • SwiftData + CloudKit (private database)
  • ActivityKit (Dynamic Island / Live Activities)
  • WidgetKit (Lock Screen + Control Center widgets)
  • AVFoundation (dial click audio)
  • PhotosUI + Photos (gallery picker + save to camera roll)
  • Charts (grind size trend visualization)
  • UserNotifications (maintenance reminders)

Android:

  • Kotlin / Jetpack Compose
  • Material 3 Expressive with MaterialExpressiveTheme + MotionScheme.expressive()
  • Dynamic Color (Material You) — adapts to the user's wallpaper on Android 12+
  • DataStore + kotlinx.serialization (with bidirectional iOS JSON compatibility)
  • Glance API (home screen widget with shot stats)
  • ForegroundService + ongoing notification (brew timer background equivalent)
  • SplashScreen API (core-splashscreen)
  • ToneGenerator (dial click audio)
  • AlarmManager (maintenance notification scheduling)
  • App Shortcuts (launcher long-press quick actions)
  • Deep linking (dialedin://addshot, dialedin://addbean)
  • Custom CoffeeBeanShape Path clip for bean images
  • Google Fonts (Roboto + Roboto Flex via downloadable fonts)

Shared:

  • JSON export/import format (ISO 8601 dates, UUID strings)
  • 3 languages: English, German, Spanish (~380 localized keys)
  • Zero third-party dependencies on both platforms

What's Next

The iOS app is available on the App Store. The Android version is in active development with full feature parity as the goal.

On the roadmap:

  • Smart Scale Integration — Bluetooth connectivity for Acaia, Felicita, and Decent scales with live weight during extraction
  • Water Profiles — TDS, mineral composition, and water recipe tracking per shot
  • Advanced Analytics — grind-to-extraction trend analysis, flavor evolution over time, best-performing bean rankings
  • Apple Watch Companion — quick shot logging and brew timer from your wrist

I built Dialed In because I wanted a tool that respects both the craft of espresso and the craft of software. No subscription, no cloud dependency, no data harvesting — just a precise, beautiful logbook that makes great coffee easier to repeat.

If you're the kind of person who weighs their beans to the tenth of a gram, this app is for you.