When we set out to build Billin - our smart invoice maker for freelancers, small businesses, and professionals - we knew we wanted to deliver a clean, reliable experience that helps people create, send, and track professional invoices in seconds.
On Android, this was straightforward: Kotlin and Jetpack Compose gave us the productivity we needed, and our users loved the fast, polished results. But we didn’t want to stop there. Freelancers and small businesses often work across devices and ecosystems, and many of them asked for an iOS version. That’s where Compose Multiplatform (CMP) came into play.
Why Compose Multiplatform?
With Billin, users can preview invoices as PDFs, track payments, manage clients, and manage their services inventory - all from their phone. To bring this experience to iOS, we had two choices:
- Build a completely separate SwiftUI app.
- Leverage the tools we already loved (Kotlin + Compose) and extend them to iOS.
We chose option two - and Compose Multiplatform (CMP) proved to be the right call.
Challenges and solutions
Migrating from Android-first to CMP came with a few bumps. For example:
- On Android, we relied on the AndroidX PDF Preview component to display invoices. On iOS, this wasn’t available. Our solution was to generate image previews of invoices, which worked flawlessly across both platforms.
- Some UI conventions simply didn’t translate well. We disabled ripple effects on iOS and replaced certain screen transitions with animations that feel more at home in the Apple ecosystem.
- We carefully reworked layouts and spacing so that iOS users wouldn’t feel like they were using a “ported” Android app.
The good news? Most of the Jetpack Compose libraries we were already using had multiplatform support. And with our own open-source library, pale-blue-kmp-core
, we had a big head start in sharing logic across platforms.
The architecture behind Billin
Billin is built on a service-based architecture , which has been key in making multiplatform development efficient. Here’s how:
- We use a
SystemService
layer to abstract system-level interactions (like file storage or sharing). - With Kotlin’s
expect/actual
mechanism, we implement platform-specific code while keeping most of the logic shared. - Our monorepo pattern allows us to share models, business logic, and even some backend code between Android, iOS, and our server.
Speaking of the server, our backend is also written in Kotlin (Spring). That means Billin isn’t just multiplatform on mobile - it’s part of a unified Kotlin ecosystem that runs across frontend, backend, and shared libraries.
Delivering a cross-platform design that feels right
For Billin, we made a deliberate design choice: the app should look clean and modern , but not be tightly coupled to either Android’s Material Design or iOS’s Human Interface Guidelines. Instead, we aimed for a neutral yet elegant visual style that feels familiar on both platforms.
That said, we didn’t ignore platform conventions. We still rely on native UI elements and animations where they make sense—for example, transitions on iOS behave like iOS users expect, and system dialogs follow platform guidelines. By combining a shared, platform-agnostic design language with native touches, Billin feels consistent and professional while still feeling at home on whichever device you’re using.
Simplifying In-App Purchases with RevenueCat
One area where we were particularly impressed was in-app purchases. Setting up IAPs in the Play Console and App Store Connect is still required, but managing them across both platforms can be a real pain - keeping product IDs consistent, making sure pricing aligns, and then wiring everything up in the client. RevenueCat solved much of that complexity by providing a dynamic, configurable paywall that pulls in the right products at runtime.
This not only meant we didn’t have to hard-code product lists, but also gave us the ability to A/B test marketing messages and pricing strategies with just a few clicks. On top of that, the unified reporting across platforms makes it far easier to get a single picture of subscriptions and revenue. And the fact that RevenueCat ships with a Kotlin Multiplatform (KMP) SDK was a huge bonus - integrating it into our existing architecture was smooth and developer-friendly.
What Billin offers
At its core, Billin is an invoicing tool designed to help freelancers and small businesses manage their billing process on mobile. The app supports:
- Invoice creation – generate invoices with support for taxes, discounts, and automatic totals.
- Tracking and Reporting – keep a record of payments, overdue items, expenses, and client summaries.
- Entities Management – maintain items and services you offer for quick invoice creation.
- Cross-Platform Access – preview invoices as PDFs or images, share them securely, and access data from multiple devices.
This functionality is wrapped in a design that is clean and modern, intentionally not tied too closely to either Android or iOS design systems, while still using native UI elements and animations to feel natural on each platform.
The power of CMP
For us, Compose Multiplatform (CMP) wasn't just about writing less code. They were about creating a seamless cross-platform experience while keeping our development process lean and scalable.
Billin today is both a native Android app and a wonderful iOS app , built from the same shared foundation. With thoughtful adjustments, it feels fully integrated into each ecosystem while maintaining a consistent experience across platforms. Combined with our Kotlin/Spring backend and monorepo approach, we’ve created a unified stack where models, logic, and services can be reused everywhere.
This approach has allowed us to move faster, reduce duplication, and deliver apps that feel at home on both Android and iOS while staying true to a clean, platform-agnostic design.
Top comments (0)