On Wednesday night last week there was no Kanora for Android. There was a plan, committed at nine minutes past eleven, not even a repository existed. By Monday a signed build was on my phone and on its way to the first testers. It browses and streams the library from my Mac, keeps its own library on the phone, plays Apple Lossless, and covers 70 of the 95 iPhone features listed in its plan.
I did not write it. Claude did, while I was mostly out with my family for my birthday.
Kanora on an Android phone: the album library in Cover Flow, with one cover large in the centre and its neighbours turned away at either side.
A year of work as the starting point
The first commit in Kanora's Apple repository is dated 6 October 2025, a year ago today. Since then there have been something like 2,600 commits across the Mac, iPhone, iPad, Watch and Apple TV apps, and a lot of writing that never ships to anyone: decisions with the reasoning attached, a constitution for the whole product, a wiki for each app with a log of what changed and how it was checked, and a set of skills that tell an agent how this codebase expects work to be done.
I had been treating most of that as overhead. It turned out to be what the Android app was built from.
The Android plan, ALL-PLN-003, was written from a scan of the Apple code. Its core is a parity matrix: 95 rows, each one a feature of the iOS app, each marked Port, Adapt, Flag or Defer for Android, and each assigned to a wave. There is no separate spec document. The plan says why, and I like the reasoning: the specification already exists, and it is the iOS app as built.
Under the matrix sit 47 tasks in ten waves:
- Foundation: the Gradle project, a design module, the app shell, the verification gate.
- Host: the Companion API client, network discovery, pairing with the Mac, keeping the session in the Android Keystore.
- Browse: one library view that merges the phone's own music with the Mac's, artwork, the list and detail screens, Home and Search.
- Playback: a Media3 playback service with notification and lock-screen controls, the queue, Now Playing, and a check of every audio format Kanora handles.
- Local library: a library on the phone with the same
.kanoracatalogue the Apple apps use, downloads, guided import with MusicBrainz lookups, editors, playlists and collections. - Stats and display modes: listening history, the Stats tab, and the Shelf and Cover Flow album views.
- Later: Android Auto, Wear OS, and a hardening pass.
Every task carries a kickoff prompt: the five or so lines you paste into a fresh session to start it. That detail is what let me run the weekend from my phone.
How the weekend actually went
The plan went into the repository at 23:09 on Wednesday 30 September. The first Android commit had landed four minutes earlier.
Thursday and Friday were the foundation and the host work, including pairing with the Mac. Before any coding was done, an Android project structure was set up with the requisite CLAUDE.md file, CONSTITUTION.md, a scaffolded LLM Wiki for documentation on the go and, most importantly, a GitHub repo to put it all in.
On Saturday it built the library screens. Playback started that day and finished on Sunday. The phone library, downloads and import landed on Sunday and Monday, along with stats and the album views. On Monday a release-signed 0.1.0 went to the first testers. Tuesday was the review of the last wave.
I was not at a desk for most of it. I kicked off the session from my MacBook and got on with my birthday weekend plans. We had a wander around Fowey, lunch in the amazing North Street Kitchen and then home to enjoy some wine and bits we had picked up on Fowey high street for tea. I ran the day from my phone with Claude Code's remote control, and the loop looked like this:
- Claude works through a task: code, tests, the gate, a commit.
- When it needs a decision, or there is something to look at on the phone, it pings me.
- I answer. When a task finishes, I clear the context and tell it to continue with the next one.
Managing Context
Clearing the context between tasks sounds like it should hurt. It did not, because nothing important lived only in the conversation. The plan has a "resume at" section that names the next task. The kickoff prompt says what to read. The wiki log says what the last session did and how it was verified. A fresh session starts from the repository, and the repository was always up to date.
My Pixel 6a sat on a desk plugged into the Mac the whole time. Claude installed builds on it, ran the device tests on it, and drove it through live runs against the Mac mini that holds my real library: pairing, streaming, checking each audio format. When it said there was something to see, I picked the phone up and looked. More than once, I was blown away.
Managing myself
Now, I want to be clear. I was NOT glued to my phone waiting for updates all day. Signal is patchy in Cornwall at the best of times and so I just replied as and when I felt like it. My wife and kids did not become Claude widows and orphans. The Saturday morning I kicked off another session from my phone before I got up, did presents with the kids, grabbed a coffee and played Zelda for a couple of hours with the boys before we went out for lunch.
I probably looked at my phone 2-3 times? The plan was so well formed and detailed that Claude really only had to ask for permission to carry on with the next task. I cleared the context to save tokens, approved and moved on.
The decisions it brought to me
The pings were sometimes questions that only I could answer. A few that changed the app:
Android patterns, Kanora branding. Early on it asked whether the bottom bar should copy the iPhone's More tab. My answer went into the decisions log word for word: "This doesn't have to feel like iOS. It has to have Kanora branding, but we should build this to be android native." So the app has a Material navigation bar with five destinations, tabs inside Library, an A to Z letter strip, and a Now Playing sheet that uses the system output switcher. It keeps Kanora's colours and type, which come from the same design tokens as the Apple apps, generated into Kotlin.
Apple Lossless. The Pixel has no ALAC decoder, and a lot of my library is ALAC. The options were written up as a decision record and reviewed before anything was built. The answer: Kanora carries its own decoder, written in Kotlin from Apple's open-source reference implementation and checked bit for bit against Apple's own output. An ALAC file now plays on the phone as it is.
Signing before testers. Changing the signing key later would force every tester to uninstall, and an uninstall wipes the imported library and the pairing. So I chose the release key before the first upload.
What to leave out. DLNA speaker output and smart playlists stayed out of the first build.
Why it held together
I think it held together because of three things that already existed before Wednesday.
A contract to write against. Every Kanora client talks to the Mac through the Companion API, and its OpenAPI spec is about 3,100 lines. One of the product's rules is that a new client assumes zero changes on the host. The Android app was written against that contract, and it paired with the Mac mini I already had.
Rules written down where an agent reads them. The Android repository started with an
AGENTS.md, a constitution that points back to the product's rules instead of copying them, and
three skills: implementation conventions, a pipeline for new work, and the wiki. They are about
1,100 lines between them. Much of what they say was learned the hard way on the Apple side.
A reviewer that did not write the code. After the waves, a separate reviewer agent with fresh context read the work against the rules and wrote a report. There are seven. The Wave 4 review failed first time and passed after a fix.
The numbers
From the Android repository as of the Wave 6 review:
- 131 commits and 41 merged pull requests. Every non-merge commit is co-authored by Claude: 81 by Opus 5.5 and 8 by Fable 5.1.
- About 68,000 lines of Kotlin: 50,500 of app code and 17,400 of tests.
- 748 tests: 642 on the JVM and 106 on the device.
- Ten Gradle modules, from the Companion client and secure pairing to playback and the design tokens.
- 38 of 47 plan tasks done, and 70 of the 95 rows in the parity matrix delivered. The rows are not equal in size, so treat that as a rough measure.
Restoring the catalogue of my full library, 6,875 tracks, takes under eight seconds on the Pixel.
Now Playing on Android: large artwork on a background tinted from it, the track, artist and album, the audio format, a scrubber and transport controls.
What it can't do
There is no Android Auto and no Wear OS app; both are parked, and I do not own an Android watch. It cannot send audio to DLNA speakers. Tablet layouts have only been run on an emulator. A couple of live checks against the Mac still need me in the room. Everything left in the plan is now its own item on the board.
It is a 0.1 build. It will have bugs that a weekend of testing on one phone could not find.
Two native apps, and no cross-platform framework
The usual advice for one developer who wants to be on iPhone and Android is to pick a cross-platform framework, React Native or Flutter or similar, and write the app once. I have argued against that since the days of PhoneGap, and this week did not change my mind. It took away the one argument for it that ever had any weight.
I spent years at a mobile-first agency building apps for big clients: banks, an investment platform, a global logistics company, a hi-fi brand. Nearly all of them wanted to save money by going cross-platform, and nine times out of ten we did it, knowing the result would be a compromise.
Cross-platform has always been a false economy. The single codebase is single in name only. The moment you need something one platform does and the other does not, you fork inside it: a branch for iOS here, a workaround for Android there, until the shared code is mostly special cases. And because it is all one codebase, every fix for one platform is a risk to the other. You fix a bug on Android and ship a new one to iPhone, or the other way round.
A music player makes that worse, because it sits close to the operating system: audio sessions, lock-screen and notification controls, media buttons, the way each platform expects a library to look and behave. I wanted Swift and SwiftUI on Apple devices and Kotlin and Compose on Android, each one built the way that platform's users expect.
I have also always thought the effort goes to the wrong place. If developers put as much time into learning the other platform as they put into learning a framework that sits on top of both, they would be better developers for it, and there would be far fewer JavaScript apps lying around on frameworks nobody maintains any more.
The argument that did have weight was cost. Two native apps means two codebases, and for one person that is a lot of work. This week is the first real evidence I have that the cost has changed.
The Android app shares no code with the iPhone app. What it shares is everything around the code: the API contract, the design tokens, the product rules, the written decisions, and an iOS app precise enough to serve as the spec. The agent did the translation, into Android's own components and patterns.
Most of the cost of a second platform turns out to be knowing exactly what to build and how it should behave, and that can be written down once and reused. Agents, plus a skill set for each platform, plus a product knowledge base, gave Kanora a native Android app in five days. Five days is one data point. The real test is keeping the two apps in step, and that takes a lot longer than a weekend. But the case for a cross-platform framework was always the cost, and that case is getting harder to make.
What I take from it
The decisions, the conventions, the API spec and the iOS app itself added up to a description complete enough that a second platform could be built from it in five days, mostly without me.
I spent my birthday weekend with my family. Kanora got an Android app.
If you use Android and would like to try it, the private testing page says how to get in touch.