Skip to content

kmpbits/netflow

Repository files navigation

NetFlow KMP 🌐

A lightweight, multiplatform network library for Kotlin – seamless API calls with Flow, LiveData, and native performance.

Maven Central License: MIT


✨ Features

  • 🚀 Kotlin Multiplatform-ready (Android + iOS)
  • 📡 Multiple response strategies:
    • LiveData
    • Flow (with UI state handling)
    • Direct deserialization
  • ⚙️ Customizable requests (headers, parameters, methods)
  • 🧠 Smart local cache integration with auto-observe
  • 🔁 Built-in error handling and retry logic
  • 🔍 Debug logging with multiple levels (None, Basic, Headers, Body)

💡 Why NetFlow?

Managing network calls, caching, and syncing with local databases can become a repetitive mess in every project.

NetFlow KMP was built to solve these common problems:

  • 🔁 No more syncing logic – When your API updates the data, NetFlow can automatically update your local database and trigger observers.
  • 🔍 No more boilerplate – Just define what to do on success and what to observe locally, and you're done.
  • 🧠 Smart caching and local-first approach – Responses are loaded from the local database immediately (if available), while the network updates in the background.
  • 🛠️ Flexible by design – You can work with your domain models using transformations, or skip them entirely and use DTOs.
  • 📱 Multiplatform-ready – Designed for KMP, works with Android/iOS out of the box.

✅ All of this with just a few lines of Kotlin code – no manual list mutations, no state juggling, and no complicated observer logic.


📦 Installation

Add the dependency to your build.gradle.kts:

dependencies { 
    implementation("io.github.kmpbits:netflow-core:<latest_version>")
}

Check the latest version on Maven Central


🚀 Getting Started

Initialize the Client

val client = netFlowClient {
    baseUrl = "https://api.example.com"

    // Optional default headers
    header(Header(HttpHeader.custom("custom-header"), "This is a custom header"))
    header(Header(HttpHeader.CONTENT_TYPE), "application/json")
}

Basic Request

val response = client.call {
    path = "/users"
    method = HttpMethod.Get
}.response()

Deserialize to Model

val user: User = client.call {
    path = "/users/1"
}.responseToModel<User>()

🌊 Working with Flow

val userFlow = client.call {
    path = "/users/1"
}.responseFlow<User>()

Customize the flow behavior:

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

⚠️ Important:

  • The return type from your database must match the network DTO (e.g., UserDto).
  • If you're using a different entity class, use the transform parameter inside local() to convert to the DTO type, otherwise, you will get a ClassCastException.
  • If you only want to fetch local data without a network call, set onlyLocalCall = true inside the local DSL block.
local({
    onlyLocalCall = true
    call {
        userDao.getAllUsers()
    }
}, transform = { it.map { dto -> dto.toModel() } })

All the responses can be mapped at once using the map extension inside ResultState:

.map {
    it.map { dtoList -> dtoList.map { it.toModel() } }
}

Observing Flow

Using lifecycle:

userFlow.observe(viewLifecycleOwner) { state ->
    when(state) {
        is ResultState.Loading -> showLoading()
        is ResultState.Success -> showUsers(state.data)
        is ResultState.Error -> showError(state.exception.message)
        is ResultState.Empty -> showEmptyState()
    }
}

Or with coroutines:

lifecycleScope.launch {
    userFlow.collectLatest { state ->
        // same logic as above
    }
}

⚡ Working with Async

NetFlow also supports suspending requests for simpler APIs where observation is not required.

suspend fun deleteUser(id: Int): AsyncState<User> {
    return client.call {
        path = "users/$id"
        method = HttpMethod.Delete
    }.responseAsync<UserDto> {
        onNetworkSuccess {
            userDao.deleteUser(id)
        }
    }.map(UserDto::toModel)
}

📋 Advanced Configuration

Custom Headers

client.call {
    path = "/secure-endpoint"
    header(Header(HttpHeader.custom("custom-header"), "This is a custom header"))
    header(Header(HttpHeader.CONTENT_TYPE), "application/json")
}.responseFlow<SecureData>()

Query Parameters

client.call {
    path = "/users"
    parameter("role" to "admin")
    parameter("active" to true)
}.responseFlow<List<User>>()

❗ Error Handling

responseToModel is the only extension that needs to be used with try catch.

try {
  val response = client.call {
    path = "/might-fail"
  }.responseToModel<Data>()
} catch (e: StateTalkException) {
  when (e) {
    is NetworkException -> { /* handle network issues */ }
    is SerializationException -> { /* handle parsing errors */ }
    is HttpException -> {
      val code = e.code
      val errorBody = e.errorBody
    }
  }
}

🧰 Using with DI (e.g. Koin)

single {
  netFlowClient {
    baseUrl = "https://api.example.com"
  }
}

🧪 Testing

Will be implemented soon.


📝 License

This project is licensed under the MIT License.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages