agentsclimarketplace

Retrofit

Skill piyushverma0/android-agent-skills/skills/retrofit

27 Android skills for AI agents (Claude Code, Codex, Cursor). Fixes Supabase auth, Hilt errors, design inconsistency, kapt→ksp, missing UiState states. Reduced my token bills 5×. FitGenZ AI shipped in 18 days.

Install
npx -y skills add piyushverma0/android-agent-skills --skill retrofit

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 14 stars14 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Retrofit HTTP networking for Android AI agents. Use this skill whenever implementing REST API calls, Retrofit service interfaces, OkHttp client, interceptors, authentication headers, API key injection, JWT tokens, @GET @POST @PUT @DELETE @PATCH annotations, @Query @Path @Body @Header @Field @Multipart, kotlinx.serialization, JSON parsing, error handling, network result wrapping, timeout configuration, certificate pinning, logging interceptor, mock API for testing, Hilt network module, base URL configuration, or any HTTP client networking in Android. Always apply before writing any Retrofit interface.

SKILL.md

9.4 KB, as published. Nobody here has run it

Retrofit HTTP Networking

These rules cover the complete Retrofit setup — from interface definition to error handling.

Setup

[versions]
retrofit = "2.11.0"
okhttp = "4.12.0"
kotlinxSerialization = "1.7.3"

[libraries]
retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-kotlin-serialization = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" }

[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

Rule 1: Service interface — correct annotations

// ✅ Complete service interface
interface ItemApiService {
    // GET with path parameter
    @GET("items/{id}")
    suspend fun getItem(@Path("id") id: String): ItemDto

    // GET with query parameters
    @GET("items")
    suspend fun getItems(
        @Query("page") page: Int = 1,
        @Query("limit") limit: Int = 20,
        @Query("sort") sort: String = "created_at",
        @Query("order") order: String = "desc"
    ): PagedResponse<ItemDto>

    // POST with JSON body
    @POST("items")
    suspend fun createItem(@Body request: CreateItemRequest): ItemDto

    // PUT for full update
    @PUT("items/{id}")
    suspend fun updateItem(@Path("id") id: String, @Body request: UpdateItemRequest): ItemDto

    // PATCH for partial update
    @PATCH("items/{id}")
    suspend fun patchItem(
        @Path("id") id: String,
        @Body fields: Map<String, @JvmSuppressWildcards Any>
    ): ItemDto

    // DELETE
    @DELETE("items/{id}")
    suspend fun deleteItem(@Path("id") id: String): Unit

    // File upload
    @Multipart
    @POST("items/{id}/image")
    suspend fun uploadImage(
        @Path("id") id: String,
        @Part image: MultipartBody.Part
    ): ImageDto

    // Dynamic header
    @GET("user/profile")
    suspend fun getProfile(@Header("Authorization") token: String): UserDto
}

Rule 2: DTOs with kotlinx.serialization

// ✅ @Serializable DTOs — never expose to domain layer
@Serializable
data class ItemDto(
    val id: String,
    val title: String,
    val description: String,
    @SerialName("is_favorite") val isFavorite: Boolean = false,
    @SerialName("created_at") val createdAt: String,    // ISO string from API
    @SerialName("updated_at") val updatedAt: String,
    val user: UserDto? = null
)

@Serializable
data class CreateItemRequest(
    val title: String,
    val description: String,
    @SerialName("user_id") val userId: String
)

@Serializable
data class PagedResponse<T>(
    val data: List<T>,
    val page: Int,
    val limit: Int,
    val total: Int,
    @SerialName("has_next") val hasNext: Boolean
)

// ✅ Mapper from DTO to domain model
fun ItemDto.toDomain() = Item(
    id = id,
    title = title,
    description = description,
    isFavorite = isFavorite,
    createdAt = Instant.parse(createdAt)
)

Rule 3: OkHttp + Retrofit Hilt module

// ✅ Complete network module
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    fun provideJson(): Json = Json {
        ignoreUnknownKeys = true     // API can add new fields without breaking app
        coerceInputValues = true     // null → default value for non-nullable fields
        isLenient = true             // handle minor JSON formatting issues
    }

    @Provides
    @Singleton
    fun provideAuthInterceptor(tokenProvider: TokenProvider): Interceptor =
        Interceptor { chain ->
            val token = tokenProvider.getToken()
            val request = if (token != null) {
                chain.request().newBuilder()
                    .addHeader("Authorization", "Bearer $token")
                    .build()
            } else {
                chain.request()
            }
            chain.proceed(request)
        }

    @Provides
    @Singleton
    fun provideOkHttpClient(authInterceptor: Interceptor): OkHttpClient =
        OkHttpClient.Builder()
            .addInterceptor(authInterceptor)
            .addInterceptor(
                HttpLoggingInterceptor().apply {
                    level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
                            else HttpLoggingInterceptor.Level.NONE
                }
            )
            .connectTimeout(30, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .writeTimeout(30, TimeUnit.SECONDS)
            .build()

    @Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient, json: Json): Retrofit =
        Retrofit.Builder()
            .baseUrl(BuildConfig.BASE_URL)
            .client(okHttpClient)
            .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
            .build()

    @Provides
    @Singleton
    fun provideItemApiService(retrofit: Retrofit): ItemApiService =
        retrofit.create(ItemApiService::class.java)
}

Rule 4: Network result — wrap errors uniformly

// ✅ Sealed result for all API calls
sealed interface NetworkResult<out T> {
    data class Success<T>(val data: T) : NetworkResult<T>
    data class Error(val code: Int, val message: String) : NetworkResult<Nothing>
    data object NetworkError : NetworkResult<Nothing>      // no connection
    data object Timeout : NetworkResult<Nothing>
}

// ✅ Extension to safely call any suspend API function
suspend fun <T> safeApiCall(apiCall: suspend () -> T): NetworkResult<T> = try {
    NetworkResult.Success(apiCall())
} catch (e: HttpException) {
    NetworkResult.Error(
        code = e.code(),
        message = e.response()?.errorBody()?.string() ?: e.message()
    )
} catch (e: IOException) {
    NetworkResult.NetworkError
} catch (e: SocketTimeoutException) {
    NetworkResult.Timeout
}

// ✅ Usage in Repository
override suspend fun createItem(request: CreateItemRequest): Result<Item> = withContext(ioDispatcher) {
    when (val result = safeApiCall { api.createItem(request) }) {
        is NetworkResult.Success -> Result.success(result.data.toDomain())
        is NetworkResult.Error -> Result.failure(ApiException(result.code, result.message))
        is NetworkResult.NetworkError -> Result.failure(NoNetworkException())
        is NetworkResult.Timeout -> Result.failure(TimeoutException())
    }
}

Rule 5: Token refresh — automatic re-authentication

// ✅ Authenticator for automatic 401 token refresh
class TokenAuthenticator @Inject constructor(
    private val tokenRepository: TokenRepository
) : Authenticator {
    override fun authenticate(route: Route?, response: Response): Request? {
        // Don't retry if it's already a refresh request
        if (response.request.url.pathSegments.last() == "refresh") return null

        // Refresh token synchronously (Authenticator is blocking)
        val newToken = runBlocking { tokenRepository.refreshToken() } ?: return null

        return response.request.newBuilder()
            .header("Authorization", "Bearer $newToken")
            .build()
    }
}

// Register in OkHttpClient
OkHttpClient.Builder()
    .authenticator(tokenAuthenticator)
    .build()

Rule 6: File upload with progress

// ✅ Multipart upload
fun File.toMultipartPart(partName: String): MultipartBody.Part {
    val requestBody = asRequestBody(getMimeType().toMediaTypeOrNull())
    return MultipartBody.Part.createFormData(partName, name, requestBody)
}

// In Repository
suspend fun uploadImage(itemId: String, file: File): Result<String> = withContext(ioDispatcher) {
    runCatching {
        val imagePart = file.toMultipartPart("image")
        api.uploadImage(itemId, imagePart).url
    }
}

Common Mistakes

❌ Non-suspend service functions — all Retrofit functions must be suspend ❌ Catching generic Exception without type — always catch HttpException + IOException ❌ Exposing Retrofit exceptions to ViewModel — wrap in domain exceptions ❌ Hardcoded base URL — use BuildConfig.BASE_URL ❌ Logging enabled in release build — check BuildConfig.DEBUG before setting log level ❌ No ignoreUnknownKeys = true — crashes when API adds new fields ❌ Raw Response types — always use typed response bodies ❌ Missing timeout configuration — default OkHttp timeouts are too long

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.