Skip to content

mindobix/CCKotlinNetworking

Repository files navigation

CCKotlinNetworking

An 100% written in Claude Code (CC) Android Kotlin networking library built on top of Retrofit and OkHttp. Provides everything an Android app needs to talk to REST APIs — OAuth token management, generic response handling, automatic retry, certificate pinning, logging redaction, multipart uploads, and request deduplication.

Features

  • Retrofit + OkHttp preconfigured with Moshi JSON serialization
  • ApiResponse<T> sealed class — wraps every API call into Success, Error, or NetworkError with utilities like map, onSuccess, onError
  • safeApiCall — one-liner to safely execute any Retrofit call with automatic error body parsing
  • ApiError model — parses standard JSON error bodies (message, error, code, field-level errors[])
  • NetworkErrorKind — classifies network failures into NO_CONNECTION, TIMEOUT, DNS_FAILURE, SSL_ERROR, or UNKNOWN for user-facing messages
  • Automatic Bearer token injection on every request via AuthInterceptor
  • Built-in token refresh on 401 via TokenAuthenticator �� only one refresh at a time, all other requests block and retry with the new token
  • Authentication failure callback — get notified when the refresh token expires so you can redirect to login
  • Certificate pinning — optional SHA-256 pin validation to prevent MITM attacks, with multi-pin backup support
  • RetryInterceptor — automatic retry on transient failures (5xx, 408, 429, network errors) with configurable backoff and idempotency safety
  • HeaderInterceptor — default headers on every request (Accept, User-Agent, X-Platform, etc.) with per-request override support
  • RedactingInterceptor — masks sensitive headers (Authorization, Cookie, etc.) and JSON body fields (password, ssn, etc.) in log output without modifying actual requests
  • MultipartHelper — file and byte array upload helpers with auto MIME type detection for 30+ file extensions
  • DeduplicatingInterceptor — collapses concurrent identical GET requests into a single HTTP call, all callers get independent response clones
  • Builder pattern — configure everything through a single fluent builder
  • Works with or without auth — skip tokenProvider() for public APIs

Setup

Add the networking module to your app's dependencies:

// app/build.gradle.kts
dependencies {
    implementation(project(":networking"))
}

Usage

1. Define your API service

interface UserApi {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: Int): Response<User>

    @GET("users")
    suspend fun listUsers(): Response<List<User>>

    @POST("users")
    suspend fun createUser(@Body request: CreateUserRequest): Response<User>

    @PUT("users/{id}")
    suspend fun updateUser(@Path("id") id: Int, @Body request: CreateUserRequest): Response<User>

    @DELETE("users/{id}")
    suspend fun deleteUser(@Path("id") id: Int): Response<Unit>

    @Multipart
    @POST("users/me/avatar")
    suspend fun uploadAvatar(@Part image: MultipartBody.Part): Response<User>
}

2. Create the networking client

Minimal setup (no auth, no retry):

val client = NetworkingClient.Builder()
    .baseUrl("https://api.example.com/")
    .build()

val userApi = client.create<UserApi>()

Full setup with all features:

val client = NetworkingClient.Builder()
    .baseUrl("https://api.example.com/")
    .tokenProvider(myTokenProvider)
    .refreshTokenConfig(refreshConfig)
    .defaultHeaders(mapOf(
        "Accept" to "application/json",
        "X-App-Version" to BuildConfig.VERSION_NAME,
        "X-Platform" to "android",
    ))
    .enableRetry(
        maxRetries = 2,
        initialDelayMs = 500,
        backoffMultiplier = 2.0,
    )
    .enableDeduplication()
    .certificatePinner(
        "api.example.com",
        "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
        "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=",  // backup pin
    )
    .enableLogging(BuildConfig.DEBUG)
    .redactHeaders("X-Custom-Secret")
    .redactBodyFields("password", "ssn", "credit_card")
    .connectTimeout(30)
    .readTimeout(30)
    .writeTimeout(30)
    .build()

3. Make API calls with safeApiCall

Every call returns an ApiResponse<T> — no uncaught exceptions, no boilerplate try/catch:

// GET a single resource
val result = safeApiCall { userApi.getUser(42) }

when (result) {
    is ApiResponse.Success -> {
        val user = result.data          // User object
        val statusCode = result.code    // 200
        val headers = result.headers    // Map<String, String>
    }
    is ApiResponse.Error -> {
        val code = result.code          // 404, 422, 500, etc.
        val message = result.error?.displayMessage  // "User not found"
        val fieldErrors = result.error?.errors      // [{field:"email", message:"invalid"}]
        val rawBody = result.rawBody    // raw error body string
    }
    is ApiResponse.NetworkError -> {
        val exception = result.exception  // IOException, timeout, DNS failure
        val kind = result.kind            // NetworkErrorKind enum
    }
}

4. Show specific error messages with NetworkErrorKind

is ApiResponse.NetworkError -> {
    val message = when (result.kind) {
        NetworkErrorKind.NO_CONNECTION -> "Check your internet connection"
        NetworkErrorKind.TIMEOUT -> "Server is slow, try again"
        NetworkErrorKind.DNS_FAILURE -> "Cannot reach server"
        NetworkErrorKind.SSL_ERROR -> "Security error — update the app"
        NetworkErrorKind.UNKNOWN -> "Something went wrong"
    }
    showError(message)
}

5. Use callback chaining

val result = safeApiCall {
    userApi.createUser(CreateUserRequest(name = "Alice", email = "alice@test.com"))
}
result.onSuccess { user ->
    println("Created user ${user.id}")
}.onError { error ->
    println("Failed: ${error.code} - ${error.error?.displayMessage}")
}.onNetworkError { networkError ->
    println("No connection: ${networkError.exception.message}")
}

6. Transform and extract data

// UPDATE a resource
val result = safeApiCall {
    userApi.updateUser(1, CreateUserRequest(name = "Alice Updated", email = "alice@new.com"))
}
val updatedName = result.dataOrNull()?.name  // "Alice Updated" or null

// LIST resources
val users = safeApiCall { userApi.listUsers() }.dataOrNull() ?: emptyList()

// Transform response data
val nameResult: ApiResponse<String> = safeApiCall { userApi.getUser(1) }.map { it.name }

7. Upload files with MultipartHelper

// Upload a file (MIME type auto-detected from extension)
val part = MultipartHelper.fromFile(file, "image")
val result = safeApiCall { userApi.uploadAvatar(part) }

// Upload from byte array (e.g., camera capture)
val part = MultipartHelper.fromBytes(bytes, "photo", "camera.jpg", MimeType.IMAGE_JPEG)

// Upload multiple files
val parts = MultipartHelper.fromFiles(files, "images")

// Include text fields alongside files
val titlePart = MultipartHelper.createFormData("title", "My Post")

8. Implement TokenProvider (for authenticated APIs)

You only need to handle token storage — the library makes the refresh call for you:

class MyTokenProvider(private val prefs: SharedPreferences) : TokenProvider {

    override fun getAccessToken(): String? =
        prefs.getString("access_token", null)

    override fun getRefreshToken(): String? =
        prefs.getString("refresh_token", null)

    override fun onTokensUpdated(accessToken: String, refreshToken: String) {
        prefs.edit()
            .putString("access_token", accessToken)
            .putString("refresh_token", refreshToken)
            .apply()
    }

    override fun onAuthenticationFailed() {
        // Clear tokens and navigate to login
        prefs.edit().clear().apply()
    }
}

9. Configure the refresh endpoint

Tell the library where to refresh and which response headers contain the new tokens:

val refreshConfig = RefreshTokenConfig(
    refreshUrl = "https://api.example.com/auth/refresh",
    accessTokenHeader = "X-Access-Token",       // header with new access token
    refreshTokenHeader = "X-Refresh-Token",     // header with new refresh token (optional)
)

For a web page that returns the token in the standard Authorization header:

val refreshConfig = RefreshTokenConfig(
    refreshUrl = "https://auth.example.com/token/refresh",
    method = "GET",
    accessTokenHeader = "Authorization",  // reads "Bearer <token>" and strips prefix
    buildRequestBody = null,              // no body for GET
)

You can also add extra headers (e.g., client ID) and customize the request body:

val refreshConfig = RefreshTokenConfig(
    refreshUrl = "https://api.example.com/oauth/token",
    accessTokenHeader = "X-Access-Token",
    additionalHeaders = mapOf("X-Client-Id" to "my-app"),
    buildRequestBody = { refreshToken ->
        FormBody.Builder()
            .add("refresh_token", refreshToken)
            .add("grant_type", "refresh_token")
            .add("client_id", "my-app")
            .build()
    },
)

How token refresh works

Request A ──401──┐
Request B ──401──┤
Request C ──401──┘
                 │
          ┌──────▼───────┐
          │  Lock acquired│
          │  by Request A │
          │               │
          │  Refresh token │
          │  ───────────► │
          │  New token ◄── │
          │               │
          │  Store new     │
          │  tokens        │
          └──────┬────────┘
                 │
Request A ◄──retry with new token
Request B ◄──retry with new token (no refresh, token already updated)
Request C ◄──retry with new token (no refresh, token already updated)

How request deduplication works

Screen A ──GET /users/me──┐
Screen B ──GET /users/me──┤
Screen C ──GET /users/me──┘
                          │
                   ┌──────▼──────┐
                   │ Screen A is  │
                   │ the "owner"  │
                   │              │
                   │  HTTP GET ──►│
                   │  Response ◄──│
                   │              │
                   │ Buffer body  │
                   └──────┬───────┘
                          │
Screen A ◄── clone of response
Screen B ◄── clone of response (no HTTP call)
Screen C ◄── clone of response (no HTTP call)

Builder options

Method Description Default
baseUrl(url) API base URL (required) --
tokenProvider(provider) OAuth token storage null (no auth)
refreshTokenConfig(config) Refresh endpoint + header config null (no refresh)
defaultHeaders(headers) Map of headers added to every request empty
defaultHeader(name, value) Single header added to every request --
enableRetry(maxRetries, initialDelayMs, backoffMultiplier, retryNonIdempotent) Automatic retry on transient failures disabled
enableDeduplication() Collapse concurrent identical GETs into one HTTP call disabled
certificatePinner(hostname, pins) SHA-256 certificate pinning for a hostname none (standard HTTPS)
certificatePinner(pinner) Pre-built CertificatePinner for full control none
enableLogging(bool) Enable HTTP logging false
loggingLevel(level) OkHttp logging level BODY
redactHeaders(headers) Mask sensitive headers in logs none (+ defaults when any redaction configured)
redactBodyFields(fields) Mask sensitive JSON fields in logs none
connectTimeout(seconds) Connection timeout 30
readTimeout(seconds) Read timeout 30
writeTimeout(seconds) Write timeout 30
maxAuthRetries(count) Max 401 retry attempts 1
addInterceptor(interceptor) Add custom OkHttp interceptor --
moshi(moshi) Custom Moshi instance default with KotlinJsonAdapterFactory

Architecture

networking/
└── src/main/java/com/mindobix/networking/
    ├── NetworkingClient.kt              # Builder for Retrofit + OkHttp
    ├── ApiResponse.kt                   # Sealed class: Success / Error / NetworkError
    ├── ApiError.kt                      # Standard error body model with JSON parsing
    ├── NetworkErrorKind.kt              # Classifies network errors (timeout, DNS, SSL, etc.)
    ├── SafeApiCall.kt                   # safeApiCall {} suspend wrapper
    ├── RetryInterceptor.kt              # Automatic retry on 5xx/408/429/IOException
    ├── HeaderInterceptor.kt             # Default headers on every request
    ├── RedactingInterceptor.kt          # Masks sensitive headers/fields in logs
    ├── DeduplicatingInterceptor.kt      # Collapses concurrent identical GETs
    ├── MultipartHelper.kt               # File/byte upload helpers + MimeType constants
    └── auth/
        ├── TokenProvider.kt             # Interface to implement (storage only)
        ├── RefreshTokenConfig.kt        # Refresh URL + header config
        ├── AuthInterceptor.kt           # Injects Bearer token
        └── TokenAuthenticator.kt        # Handles 401 → refresh → block → replay

Tests

199 unit tests using MockWebServer cover the full library. Run them with:

./gradlew :networking:testDebugUnitTest
Test Suite Tests Docs
ApiResponse, ApiError & safeApiCall 29 View tests
TokenAuthenticator & AuthInterceptor 27 View tests
NetworkErrorKind 28 View tests
MultipartHelper & MimeType 26 View tests
RetryInterceptor 23 View tests
RedactingInterceptor 20 View tests
DeduplicatingInterceptor 15 View tests
Certificate Pinning 12 View tests
NetworkingClient.Builder 12 View tests
HeaderInterceptor 7 View tests
Total 199

Requirements

  • Android minSdk 30
  • Kotlin (bundled with AGP 9.x)

Related Libraries

Library Description
CCKotlinAPICache File-based API response caching with CACHE_FIRST/NETWORK_FIRST strategies, LRU eviction, and X-Cache headers
CCKotlinFeatureConfig Feature flag management with targeting rules, A/B variants, and bundled defaults — built on CCKotlinNetworking
CCKotlinLibsDemoApp Jetpack Compose demo app showcasing all 3 libraries working together

License

MIT

About

Android Kotlin networking library with Retrofit + OAuth token management (refresh, block, replay)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages