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.
- Retrofit + OkHttp preconfigured with Moshi JSON serialization
ApiResponse<T>sealed class — wraps every API call intoSuccess,Error, orNetworkErrorwith utilities likemap,onSuccess,onErrorsafeApiCall— one-liner to safely execute any Retrofit call with automatic error body parsingApiErrormodel — parses standard JSON error bodies (message,error,code, field-levelerrors[])NetworkErrorKind— classifies network failures intoNO_CONNECTION,TIMEOUT,DNS_FAILURE,SSL_ERROR, orUNKNOWNfor 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 safetyHeaderInterceptor— default headers on every request (Accept, User-Agent, X-Platform, etc.) with per-request override supportRedactingInterceptor— masks sensitive headers (Authorization, Cookie, etc.) and JSON body fields (password, ssn, etc.) in log output without modifying actual requestsMultipartHelper— file and byte array upload helpers with auto MIME type detection for 30+ file extensionsDeduplicatingInterceptor— 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
Add the networking module to your app's dependencies:
// app/build.gradle.kts
dependencies {
implementation(project(":networking"))
}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>
}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()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
}
}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)
}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}")
}// 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 }// 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")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()
}
}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()
},
)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)
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)
| 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 |
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
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 |
- Android minSdk 30
- Kotlin (bundled with AGP 9.x)
| 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 |
MIT