Skip to content
Go back

NetFlow Part 1: Why I Took the Leap from Android-Only to Kotlin Multiplatform

by KMP Bits

KMP Bits Cover

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:

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:

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:

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:

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:

✅ 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 the onNetworkSuccess builder.


🧱 Preparing the Migration

Once I decided to make NetFlow Multiplatform, I had to start thinking modularly:

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!)


Share this post on:

Previous Post
🧊 How Talking to Myself, Vanilla PHP, and an Ice Cream Led Me to Kotlin Multiplatform
Next Post
🚨 iOS 26’s Liquid Glass Is a Game-Changer for Kotlin Multiplatform — And a Wake-Up Call for Flutter