Originally published on Medium
🚀 Introduction
Let’s be honest: networking in Android always feels routine. Fetch from an API. Store in Room. Wire it up with Flow. Repeat — and repeat.
That boilerplate? I got tired of it.
NetFlow was my answer.
Originally, NetFlow started as a simple Android library with a clean, fluent DSL. Multiplatform wasn’t even on my radar. But after seeing how much code sharing helps in real projects — and how much duplicated logic I was writing for iOS — curiosity took over. Maybe a touch of stubbornness, too.
Before I dove in, I tried rewriting the library in Swift using URLSession. It worked, but something was missing. The code lacked cohesion, and maintenance was hard. The Swift DSL just felt like “Kotlin with an accent” — not idiomatic or satisfying.
That’s when it clicked: I didn’t want to duplicate.
I wanted to share.
Kotlin Multiplatform gave me just that — native networking on each platform, with unified business logic above.
In this series, I’ll walk you through why I built NetFlow, what made me embrace KMP, and the technical steps along the way. If this sounds useful, follow along — there’s more to come!
💡 Why I Built NetFlow
NetFlow started life as a library called Communication, designed specifically for Android. It helped simplify network requests and local data updates — but as I began working with Kotlin Multiplatform, I realized I wanted more than an Android-only solution.
When I started building what would become NetFlow (back when it was just Communication on Android), I chose OkHttp as the network client — and for good reasons:
- Performance: OkHttp is generally faster than Retrofit because it’s a lower-level HTTP client. Retrofit builds on top of OkHttp but adds extra abstraction layers like converters and adapters. Those layers add overhead, which can impact performance, especially on complex or frequent network calls.
- Flexibility: OkHttp gives me more control over requests, interceptors, caching, and connection management.
- Simplicity: Since NetFlow is designed to be a lightweight and customizable network library, OkHttp’s simplicity fits perfectly.
- Future-proof: When I started, Ktor wasn’t mature or even stable enough to be a real option for production-ready networking.
By building on OkHttp directly, I was able to craft a lean, efficient networking layer that fits the needs of both Android and Multiplatform targets — now paired with URLSession on iOS for the native experience.
🔧 The Pain Points NetFlow Solves (on Android and Beyond)
As an Android developer, I got tired of:
- Writing the same repository logic over and over.
- Managing
Result
wrappers manually. - Syncing network updates with Room.
NetFlow wraps all of that into a single, declarative API:
val usersFlow = client.call {
path = "/users"
method = HttpMethod.Get
}.responseFlow<List<User>> {
onNetworkSuccess { users ->
userDao.insertAll(users)
}
local({
observe {
userDao.getAllUsers()
}
}
}
The KMP version of NetFlow builds on the same ideas as Communication but takes things further: it supports Android and iOS, while maintaining the same clean, expressive API.
🤔 Why Kotlin Multiplatform?
Once Communication was stable and working nicely for Android, I started to wonder: could I use the same logic in iOS apps?
I had actually tried for several months to get this architecture working with Kotlin Multiplatform, but without success. There were too many moving parts: networking, serialization, platform dependencies, and shared logic structure.
Still, the idea stuck with me. I was already working on KMP projects using shared logic with Kotlin. And something clicked: Communication could be a great candidate for Multiplatform, not just for reuse, but because:
- The networking + caching + observer pattern is universal.
- iOS apps face the same boilerplate problems.
- I was already writing duplicate logic on both platforms.
I had already implemented OkHttp on Android and wanted to reuse that. The goal was to keep it native per platform: OkHttp for Android and URLSession for iOS. With NetFlow, that’s now exactly how it works — native clients behind the scenes, shared logic on top.
Migration wasn’t all rainbows (details coming in Part 2), but KMP let me keep what I loved about the library while sharing the logic and respecting each platform’s strengths.
✨ What Sets Kotlin Multiplatform Apart
When I first heard about Kotlin Multiplatform, I thought:
“Cool, I can share code between Android and iOS.”
But as I dove deeper, I realized it’s a completely different paradigm — not just a convenience, but a powerful new way to build apps.
With KMP, you get the best of both worlds:
- Share all your core logic in
commonMain
— one place for your business rules, network layers, and more. - Call platform APIs natively — thanks to seamless interoperability, Kotlin code can directly use Objective-C/Swift APIs on iOS and Java APIs on Android without awkward wrappers.
- Keep full native performance and feel — you’re not abstracting away platform differences; you’re embracing them.
This approach is a game-changer compared to frameworks like Flutter or React Native, which wrap native APIs behind an engine or a bridge. With KMP, your shared code is true native code, running natively on each platform, even within your common modules.
For me, this meant I could keep using OkHttp on Android and URLSession on iOS, native HTTP clients, while sharing the higher-level networking and state management logic seamlessly.
🔌 Basic Setup
Add NetFlow to your build.gradle.kts
:
implementation("io.github.kmpbits:netflow-core:<latest_version>")
Initialize the client:
val client = netFlowClient {
baseUrl = "<https://api.example.com>"
// Optionally, you can add headers
header(Header(HttpHeader.CONTENT_TYPE), "application/json")
}
🔄 Traditional Approach vs NetFlow
Let’s compare how you usually build something like “fetch all users and keep the UI updated” using Ktor + Room.
🛠️ Traditional Ktor + Room Setup
suspend fun fetchUsers(): Result<List<UserDto>> {
return try {
val response = client.get("<https://api.example.com/users>")
val users = json.decodeFromString<List<UserDto>>(response.bodyAsText())
userDao.insertAll(users)
Result.success(users)
} catch (e: Exception) {
Result.failure(e)
}
}
fun observeUsers(): Flow<List<User>> {
return userDao.getAllUsers().map { it.map { entity -> entity.toModel() } }
}
In the ViewModel:
- Launch one coroutine to fetch.
- Another to observe.
✅ NetFlow Way
val usersFlow = client.call {
path = "/users"
method = HttpMethod.Get
}.responseFlow<List<User>> {
onNetworkSuccess { usersDto ->
// Convert DTO to Entity table
userDao.insertAll(users.map(UserDto::toEntity))
}
local({
observe {
userDao.getAllUsers()
}
// Convert Entity to DTO, if the database object is different than the network
// because the return type from your database must match the network DTO
}, transform = { it.map(UserEntity::ToDto) })
}.map {
// Convert all of the response to models as it is the return type of the function
it.map { it.map(UserDto::toModel) }
}
And on ViewModel:
viewModelScope.launch {
usersFlow.collectLatest { result ->
when(result) {
is ResultState.Loading -> showLoading()
is ResultState.Success -> showUsers(result.data)
is ResultState.Error -> showError(result.exception.message)
ResultState.Empty -> showEmptyState()
}
}
}
💡 One flow to rule them all: fetch, store, observe, and emit full UI state — no second function, no manual merging, no duplicated logic. By the time the result reaches
ResultState.Success
, the items have already been inserted into the database, thanks to theonNetworkSuccess
builder.
🧱 Preparing the Migration
Once I decided to make NetFlow Multiplatform, I had to start thinking modularly:
- ✅ Separate platform-specific logic (OkHttp vs URLSession)
- ✅ Replace Android-only types on commonMain
- ✅ Keep the DSL and developer experience as close to the original as possible
In Part 2, I’ll explain how I handled all those technical challenges — including how I abstracted the HTTP layer to work the same way on both Android and iOS.
Spoiler: it was smoother than I expected, but it took a few sharp corners to get there.
🧪 What’s Next
This was just the beginning. In Part 2, I’ll dive into the real technical challenges: abstracting the HTTP logic, working with OkHttp and URLSession, and keeping the developer experience seamless across Android and iOS.
Until then — feel free to drop questions or thoughts and follow if you’re curious about where this road leads.
Stay tuned! 🛠️
(P.S. If you want to see the code, check out NetFlow on GitHub — PRs and stars always welcome!)