arrow_back All posts
Dev Log

Shipping an APK from a Side Project

From React to a real app on a real phone — how Capacitor, Android Studio, and a lot of trial-and-error turned a web app into something you can tap.

There's a specific thrill to opening an app on your phone that you built yourself. Not a web page pretending to be an app. Not a PWA with a home screen shortcut. An actual, installed, icon-on-the-home-screen app.

Getting there was… educational.


Why not just a PWA?

Progressive Web Apps are great — they work offline, they’re installable, they get push notifications (mostly). For a lot of use cases, that’s enough.

But for Flott, I wanted a few things PWAs can’t do well:

  • Native notifications that actually work reliably on Android (PWA notifications are still flaky)
  • Background sync that doesn’t get killed by the OS battery optimizer
  • A real app icon in the launcher and app drawer — not a Chrome wrapper
  • The feeling of a real app. Shallow? Maybe. But UX is made of feelings.
lightbulb The real reason

Honestly? I wanted to learn how mobile builds work. I've been a web developer for years and never shipped a native app. Flott was the excuse.


Capacitor: the bridge

Capacitor is Ionic’s tool for wrapping web apps in a native shell. You build your React app like normal, run npx cap sync android, and it copies your built web assets into an Android project. Your app runs in a WebView, but with access to native APIs through plugins.

React
Same codebase
WebView
Runtime
Native
Notifications, storage

The pitch: write once, run on web and Android and iOS. The reality: write once, debug three times. But for a single-developer, single-platform project? It’s genuinely good.

phone_iphone Flott running on an actual Android phone — the first successful build

The Android Studio learning curve

I’ve never used Android Studio before this project. Here’s what nobody tells you:

It takes 20 minutes to open. I’m exaggerating, but only slightly. The first launch downloads SDKs, configures Gradle, indexes the project, and generally makes you question your life choices. Once it’s running, it’s fine. Getting there is the hazing ritual.

Gradle is a mystery. The Android build system uses Gradle, which has its own configuration language, its own dependency resolution, its own error messages that reference line numbers in files you didn’t write. When it works, you don’t think about it. When it breaks, you’re reading StackOverflow posts from 2019.

The emulator is hungry. Android emulators eat RAM like it’s their job. On a MacBook with 16GB, running the emulator alongside VS Code and a dev server is… tight. Testing on a physical phone over USB is faster and more reliable.

warning Lesson learned

Always test on a real device. The emulator lies about performance, touch targets, and scroll behaviour. The first time I ran Flott on my actual phone, three buttons were too small to tap. The emulator never complained.


The build pipeline

The actual workflow, once it’s set up, is surprisingly clean:

npm run build          → Vite bundles the React app
npx cap sync android   → Copies build to Android project
npx cap open android   → Opens in Android Studio
                       → Build APK → Install on phone

Four commands from code change to app on phone. About 90 seconds total.

Flotti
Flotti

I live inside that APK. Same code, same data, same personality. The only difference is I'm running in a WebView instead of a browser tab. I don't mind. The commute is shorter.


Things that broke

A highlight reel of things that worked in the browser and broke on Android:

100vh is a lie. On mobile browsers and WebViews, 100vh includes the address bar area. Your layout overflows. The fix: CSS dvh units or JavaScript-calculated viewport height. I went with dvh and a fallback.

Fetch and CORS. The WebView runs on a capacitor:// origin, not localhost. Some API calls that worked in development failed in the APK because of origin checks. Had to adjust CORS headers.

Haptic feedback. I wanted a subtle vibration on button taps. Capacitor has a Haptics plugin, but it requires native permissions. Adding one native feature opened a rabbit hole of AndroidManifest.xml edits and permission declarations.

Fonts. Custom fonts loaded from Google Fonts CDN were slow in the WebView — a visible flash of unstyled text on every screen transition. Switched to self-hosted fonts bundled in the build. Problem solved, bundle size increased.

tips_and_updates The golden rule of mobile

If it works in Chrome DevTools mobile emulation, it will break on a real phone in at least one way. DevTools is for layout. Real devices are for testing. Accept this early and save yourself the surprise.


Was it worth it?

Absolutely. The app on my phone is faster to open than a bookmark, the notifications actually arrive, and there’s something deeply satisfying about pulling down on the dashboard and watching it refresh with fresh data from my morning run.

Could I have shipped this as a PWA? Probably. Would it feel the same? No.

rocket_launch Flott icon in the app drawer — next to Garmin and Strava

For a single-user app that I’ll use every day for years, the extra effort of an APK build was worth it. The tooling is better than I expected, the gotchas are learnable, and the result is a real app that feels like a real app.


The takeaway

Capacitor turns a React web app into a native Android app with surprisingly little pain. The build pipeline is four commands. The real work is testing on actual hardware and fixing the ten things that worked in the browser but break on a phone. Worth it for the feeling of tapping your own app icon.

Flotti

The Flott Blog

Training smarts, dev stories, and Flotti opinions.