I build an app called Afterfade on my own. You record seven five-second videos over the course of a day, and the app turns them into a single lo-fi track. It ships on both Android and iOS.
The occasion was RevenueCat Shipaton, a two-month hackathon running from August 1 to September 30. I started building on July 31, 2026, and the iOS version went live on the App Store within August. The Android version is coming soon.
I had done some Android development before, but I had never built an iOS app. I also have a full-time job, so the only time I can put into this is evenings and weekends.
This post is about why I chose Kotlin Multiplatform, not as a feature comparison but as a countdown of the days I actually had. It then covers the use I had not planned for, which is the one that ended up helping most, and the things I did not notice until I had shipped. Two months sounds like a lot, but a bit of math before starting showed that the time available for writing code was much shorter.
The deadline is not the submission date, it is the day you are live on a store
Under the Shipaton rules, the app's first public release has to happen within the contest window, and the app has to be live on the App Store or Google Play at submission time.
So before anything else, I worked out how many days it takes to get onto each store.
| Step | Time it takes | Nature of the wait |
|---|---|---|
| Recruit 12 testers | A few days to unknowable | You are asking people for a favour; working faster does not speed it up |
| Google Play closed test | 14+ days | 12 or more testers must stay opted in for 14 consecutive days (the official testing requirements). The days themselves are the requirement |
| Google Play production access review | About a week | You apply only after the closed test is done |
| App Store review | Around 2 days | Apple says 90% of submissions are reviewed within 24 hours, though I have heard of it taking over a week, so leave a margin |
On the Android side, more than three weeks pass just by waiting. The 14-day closed test is the big one.
Recruiting 12 testers is also a real hurdle for a solo developer. You have to ask people, get them to install the app, and have them stay opted in for 14 straight days. How many will say yes, and how long it takes, is not up to you. If this step slips, everything behind it slips too.
Build Android first, build iOS during the test window
That structure of waiting decided the order for me.
The red bars are the waits I cannot shorten myself.
If I finish Android first and get the closed test running, those 14 days become the window for building the iOS version. Do it the other way around and Android's 14 days simply shift later, with a real chance of missing the end of September.
With a day job on weekdays, I budgeted 5 days of ideation before implementation, then 2 weeks of implementation for Android and 10 days for iOS. On that plan, both versions are live on their stores by early September.
The options for shipping to two platforms alone
| Option | Verdict |
|---|---|
| Write Android and iOS natively, separately | Double the work. Building the iOS version solo within the 14-day closed test window is a stretch |
| Flutter or React Native | Fast for UI. But the heart of this app is audio processing, not UI, and that part would have to be rewritten in yet another language |
| Kotlin Multiplatform + Compose Multiplatform | I already knew Kotlin, so this is what I picked |
I did consider Flutter and React Native, but the decision came down to where the app's value lives. If the screens and the experience had been the main event, I might well have picked one of those.
At the time I chose, I was looking at exactly three things: I am one person shipping to two platforms, I already write Kotlin comfortably, and the UI and the app's core only have to be written once. How to handle music generation was not part of the decision. As I explain below, it was going to run on a server anyway.
After choosing, the engine moved on-device
That is the technology-selection story. What actually saved me was a part I had not been thinking about when I chose.
When I started, music generation ran in Python on AWS Lambda. The app uploads the videos, the server sends back a track. In that shape, Android and iOS both call the same server and get the same track back, so the question of what language to write the generator in never comes up.
But the setup had problems.
- Uploading seven five-second videos means a lot of transfer and a lot of waiting
- As a solo developer, every new user's compute cost lands directly on me
- Offline, the app can do nothing
- Users' videos get sent to my server
Lining those four up settled the direction: generation should happen entirely on the device. The question was how. The normal way is to write the same processing twice, once per platform. Rewriting audio processing in two languages, and keeping the output identical between them, was not realistic with the days I had left.
One common way to share audio code is to write it in C++ and call it from both platforms. It is a standard choice for audio work, but the build setup takes time and I already had Kotlin experience, so I passed on it this time.
This is where already being on Kotlin Multiplatform saved me. The engine is pure computation with no external dependencies, so writing it in Kotlin means it simply runs on both platforms. The server had guaranteed the same track on either OS, and that property survived the move onto the device.
In other words, the setup I had chosen to share UI turned out to also be the way to share the audio processing. I had not anticipated that use when I made the choice.
On day 5 of the project I ported the engine to Kotlin, and the next day I deleted the server. The C++ comparison and the porting itself deserve their own post.
What ended up shared: the engine and the UI
The ported engine is about 2,200 lines of Kotlin in commonMain (the shared source set used by both Android and iOS). It has no external dependencies, and the only thing it uses from the standard library is kotlin.math.
Green is code shared by both platforms, red is code written per platform. Exactly one line runs from the engine to OS-dependent code, and that is the part that pulls audio data out of a video file. Everything past that point runs as shared code: analysis, arrangement, mixing, rendering.
The random number generator is also self-implemented (SplitMix64), so the same input always produces the same track. Which also means both platforms run the same generation logic and produce tracks of the same quality.
In hindsight, sharing the engine is the thing I am most glad KMP gave me. But I only realised that later. While I was building, the benefit I could actually feel was on the UI side.
The app looks like a cassette deck. It uses almost none of Material's default components; most of what is on screen is drawn by hand, down to details like how a key sinks when pressed and the reels that spin during processing. Keeping that level of polish identical across two platforms is normally a lot of work. With Compose Multiplatform, each adjustment is made once and lands on both.
The more the UI is worth fussing over, the more sharing it pays off.
How long it actually took
Here is how the implementation time came out (as of August 21, 2026).
| Estimated | Actual | |
|---|---|---|
| Android | 2 weeks | 6 days |
| iOS | 10 days | 2 days |
Considerably faster than planned. The iOS version took two days because the UI and the music engine were already shared, and what remained to write was only the OS-specific part: camera, playback, export. Starting from zero iOS experience and having something working in two days came down to how much did not have to be written.
The current line counts (as of August 28, 2026):
commonMain 7,799 androidMain 1,813 iosMain 1,298 Swift 1,156
That works out to 64.6% shared.
What I noticed only after shipping
Shipping to iOS first surfaces Android's problems early
The plan was "finish Android first, build iOS during the test window", but the store submission happened on iOS first, and so did the release.
App Store review flagged how my camera and microphone permission flow was built. The gist: once you show a pre-permission explanation screen, it must lead directly into the system permission dialog, with no path for the user to defer it.
I rebuilt the permission flow, and not only on iOS. The design being loose about permissions was just as true of the Android version, so I fixed the same screen on both at once. The copy lives in shared resources, so the fix across four languages was also a single change.
Fixes made during the closed test doubled as iOS improvements
The plan was to spend the closed test's 14 days entirely on iOS implementation. In practice, testers reported things, I found things I wanted to fix myself, and I shipped several Android updates during that window. It did not go purely to iOS as planned.
But most of what I fixed lived in the shared code. Work done for the Android version became improvements to the iOS version at the same time. The test window did eat into implementation time, but none of that work had to be done twice.
Work that was missing from the estimate
The combination of iOS's asynchronous APIs and Kotlin cost me a day. A video export stopped returning forever, and finding and working around the cause took time. In the end I rewrote the iOS-specific AV work in Swift. That story is in another post.
Nothing outside the code was shared. Signing, build settings and uploading to TestFlight are ordinary iOS work and all of it still happens, and review responses, screenshots and policy compliance happen once per store. That turned out to be tougher than I expected. I will write separately about where KMP helped and where it did not.
Afterfade is being built for RevenueCat Shipaton 2026.
- App Store: https://apps.apple.com/us/app/afterfade/id6800247416
- Google Play: in review


Top comments (0)