{"id":"c973eda7-97d8-41b3-8ff6-2de175dfbd9e","shortId":"BvUexx","kind":"skill","title":"retrofit","tagline":"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.","description":"# Retrofit HTTP Networking\r\n\r\nThese rules cover the complete Retrofit setup — from interface definition to error handling.\r\n\r\n## Setup\r\n\r\n```toml\r\n[versions]\r\nretrofit = \"2.11.0\"\r\nokhttp = \"4.12.0\"\r\nkotlinxSerialization = \"1.7.3\"\r\n\r\n[libraries]\r\nretrofit-core = { group = \"com.squareup.retrofit2\", name = \"retrofit\", version.ref = \"retrofit\" }\r\nretrofit-kotlin-serialization = { group = \"com.squareup.retrofit2\", name = \"converter-kotlinx-serialization\", version.ref = \"retrofit\" }\r\nokhttp = { group = \"com.squareup.okhttp3\", name = \"okhttp\", version.ref = \"okhttp\" }\r\nokhttp-logging = { group = \"com.squareup.okhttp3\", name = \"logging-interceptor\", version.ref = \"okhttp\" }\r\nkotlinx-serialization-json = { group = \"org.jetbrains.kotlinx\", name = \"kotlinx-serialization-json\", version.ref = \"kotlinxSerialization\" }\r\n\r\n[plugins]\r\nkotlin-serialization = { id = \"org.jetbrains.kotlin.plugin.serialization\", version.ref = \"kotlin\" }\r\n```\r\n\r\n## Rule 1: Service interface — correct annotations\r\n\r\n```kotlin\r\n// ✅ Complete service interface\r\ninterface ItemApiService {\r\n    // GET with path parameter\r\n    @GET(\"items/{id}\")\r\n    suspend fun getItem(@Path(\"id\") id: String): ItemDto\r\n\r\n    // GET with query parameters\r\n    @GET(\"items\")\r\n    suspend fun getItems(\r\n        @Query(\"page\") page: Int = 1,\r\n        @Query(\"limit\") limit: Int = 20,\r\n        @Query(\"sort\") sort: String = \"created_at\",\r\n        @Query(\"order\") order: String = \"desc\"\r\n    ): PagedResponse<ItemDto>\r\n\r\n    // POST with JSON body\r\n    @POST(\"items\")\r\n    suspend fun createItem(@Body request: CreateItemRequest): ItemDto\r\n\r\n    // PUT for full update\r\n    @PUT(\"items/{id}\")\r\n    suspend fun updateItem(@Path(\"id\") id: String, @Body request: UpdateItemRequest): ItemDto\r\n\r\n    // PATCH for partial update\r\n    @PATCH(\"items/{id}\")\r\n    suspend fun patchItem(\r\n        @Path(\"id\") id: String,\r\n        @Body fields: Map<String, @JvmSuppressWildcards Any>\r\n    ): ItemDto\r\n\r\n    // DELETE\r\n    @DELETE(\"items/{id}\")\r\n    suspend fun deleteItem(@Path(\"id\") id: String): Unit\r\n\r\n    // File upload\r\n    @Multipart\r\n    @POST(\"items/{id}/image\")\r\n    suspend fun uploadImage(\r\n        @Path(\"id\") id: String,\r\n        @Part image: MultipartBody.Part\r\n    ): ImageDto\r\n\r\n    // Dynamic header\r\n    @GET(\"user/profile\")\r\n    suspend fun getProfile(@Header(\"Authorization\") token: String): UserDto\r\n}\r\n```\r\n\r\n## Rule 2: DTOs with kotlinx.serialization\r\n\r\n```kotlin\r\n// ✅ @Serializable DTOs — never expose to domain layer\r\n@Serializable\r\ndata class ItemDto(\r\n    val id: String,\r\n    val title: String,\r\n    val description: String,\r\n    @SerialName(\"is_favorite\") val isFavorite: Boolean = false,\r\n    @SerialName(\"created_at\") val createdAt: String,    // ISO string from API\r\n    @SerialName(\"updated_at\") val updatedAt: String,\r\n    val user: UserDto? = null\r\n)\r\n\r\n@Serializable\r\ndata class CreateItemRequest(\r\n    val title: String,\r\n    val description: String,\r\n    @SerialName(\"user_id\") val userId: String\r\n)\r\n\r\n@Serializable\r\ndata class PagedResponse<T>(\r\n    val data: List<T>,\r\n    val page: Int,\r\n    val limit: Int,\r\n    val total: Int,\r\n    @SerialName(\"has_next\") val hasNext: Boolean\r\n)\r\n\r\n// ✅ Mapper from DTO to domain model\r\nfun ItemDto.toDomain() = Item(\r\n    id = id,\r\n    title = title,\r\n    description = description,\r\n    isFavorite = isFavorite,\r\n    createdAt = Instant.parse(createdAt)\r\n)\r\n```\r\n\r\n## Rule 3: OkHttp + Retrofit Hilt module\r\n\r\n```kotlin\r\n// ✅ Complete network module\r\n@Module\r\n@InstallIn(SingletonComponent::class)\r\nobject NetworkModule {\r\n\r\n    @Provides\r\n    @Singleton\r\n    fun provideJson(): Json = Json {\r\n        ignoreUnknownKeys = true     // API can add new fields without breaking app\r\n        coerceInputValues = true     // null → default value for non-nullable fields\r\n        isLenient = true             // handle minor JSON formatting issues\r\n    }\r\n\r\n    @Provides\r\n    @Singleton\r\n    fun provideAuthInterceptor(tokenProvider: TokenProvider): Interceptor =\r\n        Interceptor { chain ->\r\n            val token = tokenProvider.getToken()\r\n            val request = if (token != null) {\r\n                chain.request().newBuilder()\r\n                    .addHeader(\"Authorization\", \"Bearer $token\")\r\n                    .build()\r\n            } else {\r\n                chain.request()\r\n            }\r\n            chain.proceed(request)\r\n        }\r\n\r\n    @Provides\r\n    @Singleton\r\n    fun provideOkHttpClient(authInterceptor: Interceptor): OkHttpClient =\r\n        OkHttpClient.Builder()\r\n            .addInterceptor(authInterceptor)\r\n            .addInterceptor(\r\n                HttpLoggingInterceptor().apply {\r\n                    level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY\r\n                            else HttpLoggingInterceptor.Level.NONE\r\n                }\r\n            )\r\n            .connectTimeout(30, TimeUnit.SECONDS)\r\n            .readTimeout(30, TimeUnit.SECONDS)\r\n            .writeTimeout(30, TimeUnit.SECONDS)\r\n            .build()\r\n\r\n    @Provides\r\n    @Singleton\r\n    fun provideRetrofit(okHttpClient: OkHttpClient, json: Json): Retrofit =\r\n        Retrofit.Builder()\r\n            .baseUrl(BuildConfig.BASE_URL)\r\n            .client(okHttpClient)\r\n            .addConverterFactory(json.asConverterFactory(\"application/json\".toMediaType()))\r\n            .build()\r\n\r\n    @Provides\r\n    @Singleton\r\n    fun provideItemApiService(retrofit: Retrofit): ItemApiService =\r\n        retrofit.create(ItemApiService::class.java)\r\n}\r\n```\r\n\r\n## Rule 4: Network result — wrap errors uniformly\r\n\r\n```kotlin\r\n// ✅ Sealed result for all API calls\r\nsealed interface NetworkResult<out T> {\r\n    data class Success<T>(val data: T) : NetworkResult<T>\r\n    data class Error(val code: Int, val message: String) : NetworkResult<Nothing>\r\n    data object NetworkError : NetworkResult<Nothing>      // no connection\r\n    data object Timeout : NetworkResult<Nothing>\r\n}\r\n\r\n// ✅ Extension to safely call any suspend API function\r\nsuspend fun <T> safeApiCall(apiCall: suspend () -> T): NetworkResult<T> = try {\r\n    NetworkResult.Success(apiCall())\r\n} catch (e: HttpException) {\r\n    NetworkResult.Error(\r\n        code = e.code(),\r\n        message = e.response()?.errorBody()?.string() ?: e.message()\r\n    )\r\n} catch (e: IOException) {\r\n    NetworkResult.NetworkError\r\n} catch (e: SocketTimeoutException) {\r\n    NetworkResult.Timeout\r\n}\r\n\r\n// ✅ Usage in Repository\r\noverride suspend fun createItem(request: CreateItemRequest): Result<Item> = withContext(ioDispatcher) {\r\n    when (val result = safeApiCall { api.createItem(request) }) {\r\n        is NetworkResult.Success -> Result.success(result.data.toDomain())\r\n        is NetworkResult.Error -> Result.failure(ApiException(result.code, result.message))\r\n        is NetworkResult.NetworkError -> Result.failure(NoNetworkException())\r\n        is NetworkResult.Timeout -> Result.failure(TimeoutException())\r\n    }\r\n}\r\n```\r\n\r\n## Rule 5: Token refresh — automatic re-authentication\r\n\r\n```kotlin\r\n// ✅ Authenticator for automatic 401 token refresh\r\nclass TokenAuthenticator @Inject constructor(\r\n    private val tokenRepository: TokenRepository\r\n) : Authenticator {\r\n    override fun authenticate(route: Route?, response: Response): Request? {\r\n        // Don't retry if it's already a refresh request\r\n        if (response.request.url.pathSegments.last() == \"refresh\") return null\r\n\r\n        // Refresh token synchronously (Authenticator is blocking)\r\n        val newToken = runBlocking { tokenRepository.refreshToken() } ?: return null\r\n\r\n        return response.request.newBuilder()\r\n            .header(\"Authorization\", \"Bearer $newToken\")\r\n            .build()\r\n    }\r\n}\r\n\r\n// Register in OkHttpClient\r\nOkHttpClient.Builder()\r\n    .authenticator(tokenAuthenticator)\r\n    .build()\r\n```\r\n\r\n## Rule 6: File upload with progress\r\n\r\n```kotlin\r\n// ✅ Multipart upload\r\nfun File.toMultipartPart(partName: String): MultipartBody.Part {\r\n    val requestBody = asRequestBody(getMimeType().toMediaTypeOrNull())\r\n    return MultipartBody.Part.createFormData(partName, name, requestBody)\r\n}\r\n\r\n// In Repository\r\nsuspend fun uploadImage(itemId: String, file: File): Result<String> = withContext(ioDispatcher) {\r\n    runCatching {\r\n        val imagePart = file.toMultipartPart(\"image\")\r\n        api.uploadImage(itemId, imagePart).url\r\n    }\r\n}\r\n```\r\n\r\n## Common Mistakes\r\n\r\n❌ Non-suspend service functions — all Retrofit functions must be `suspend`\r\n❌ Catching generic `Exception` without type — always catch `HttpException` + `IOException`\r\n❌ Exposing Retrofit exceptions to ViewModel — wrap in domain exceptions\r\n❌ Hardcoded base URL — use BuildConfig.BASE_URL\r\n❌ Logging enabled in release build — check `BuildConfig.DEBUG` before setting log level\r\n❌ No `ignoreUnknownKeys = true` — crashes when API adds new fields\r\n❌ Raw Response types — always use typed response bodies\r\n❌ Missing timeout configuration — default OkHttp timeouts are too long","tags":["retrofit","android","agent","skills","piyushverma0","agent-skills","ai-agent","antigravity","claude-code","codex","cursor","gemini-cli"],"capabilities":["skill","source-piyushverma0","skill-retrofit","topic-agent-skills","topic-ai-agent","topic-android","topic-antigravity","topic-claude-code","topic-codex","topic-cursor","topic-gemini-cli","topic-hilt","topic-jetpack-compose","topic-kotlin","topic-material3"],"categories":["android-agent-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/piyushverma0/android-agent-skills/retrofit","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add piyushverma0/android-agent-skills","source_repo":"https://github.com/piyushverma0/android-agent-skills","install_from":"skills.sh"}},"qualityScore":"0.454","qualityRationale":"deterministic score 0.45 from registry signals: · indexed on github topic:agent-skills · 8 github stars · SKILL.md body (8,917 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-05-18T19:09:10.736Z","embedding":null,"createdAt":"2026-05-18T13:14:50.558Z","updatedAt":"2026-05-18T19:09:10.736Z","lastSeenAt":"2026-05-18T19:09:10.736Z","tsv":"'/image':254 '1':127,166 '1.7.3':59 '18':33 '2':279 '2.11.0':55 '20':171 '27':2 '3':390 '30':486,489,492 '4':526 '4.12.0':57 '401':654 '5':28,643 '6':716 'add':415,814 'addconverterfactori':510 'addhead':457 'addinterceptor':474,476 'agent':7 'ai':6,30 'alreadi':680 'alway':778,820 'android':3 'annot':131 'api':320,413,537,575,813 'api.createitem':622 'api.uploadimage':756 'apical':580,586 'apiexcept':631 'app':420 'appli':478 'application/json':512 'asrequestbodi':731 'auth':14 'authent':649,651,665,668,692,712 'authinterceptor':470,475 'author':274,458,704 'automat':646,653 'base':792 'baseurl':505 'bearer':459,705 'bill':27 'block':694 'bodi':187,193,211,229,824 'boolean':309,368 'break':419 'build':461,494,514,707,714,801 'buildconfig.base':506,795 'buildconfig.debug':481,803 'call':538,572 'catch':587,598,602,773,779 'chain':446 'chain.proceed':464 'chain.request':455,463 'check':802 'class':293,333,349,402,543,550,657 'class.java':524 'claud':8 'client':508 'code':9,553,591 'codex':10 'coerceinputvalu':421 'com.squareup':65,76,87,97 'common':760 'complet':42,133,396 'configur':827 'connect':564 'connecttimeout':485 'constructor':660 'convert':80 'converter-kotlinx-seri':79 'core':63 'correct':130 'cover':40 'crash':811 'creat':176,312 'createdat':315,386,388 'createitem':192,612 'createitemrequest':195,334,614 'cursor':11 'data':292,332,348,352,542,546,549,559,565 'day':34 'default':424,828 'definit':47 'delet':236,237 'deleteitem':242 'desc':182 'descript':302,339,382,383 'design':17 'domain':289,373,789 'dto':371 'dtos':280,285 'dynam':266 'e':588,599,603 'e.code':592 'e.message':597 'e.response':594 'els':462,483 'enabl':798 'error':16,49,530,551 'errorbodi':595 'except':775,784,790 'expos':287,782 'extens':569 'fals':310 'favorit':306 'field':230,417,430,816 'file':248,717,746,747 'file.tomultipartpart':725,754 'fitgenz':29 'fix':12 'format':436 'full':199 'fun':146,160,191,205,223,241,256,271,375,407,440,468,497,517,578,611,667,724,742 'function':576,766,769 'generic':774 'get':138,142,153,157,268 'getitem':147,161 'getmimetyp':732 'getprofil':272 'group':64,75,86,96,109 'handl':50,433 'hardcod':791 'hasnext':367 'header':267,273,703 'hilt':15,393 'http':36 'httpexcept':589,780 'httplogginginterceptor':477 'httplogginginterceptor.level.body':482 'httplogginginterceptor.level.none':484 'id':122,144,149,150,203,208,209,221,226,227,239,244,245,253,259,260,296,343,378,379 'ignoreunknownkey':411,809 'imag':263,755 'imagedto':265 'imagepart':753,758 'inconsist':18 'inject':659 'installin':400 'instant.parse':387 'int':165,170,356,359,362,554 'interceptor':102,444,445,471 'interfac':46,129,135,136,540 'iodispatch':617,750 'ioexcept':600,781 'isfavorit':308,384,385 'isleni':431 'iso':317 'issu':437 'item':143,158,189,202,220,238,252,377 'itemapiservic':137,521,523 'itemdto':152,196,214,235,294 'itemdto.todomain':376 'itemid':744,757 'json':108,115,186,409,410,435,501,502 'json.asconverterfactory':511 'jvmsuppresswildcard':233 'kapt':19 'kotlin':73,120,125,132,283,395,532,650,721 'kotlin-seri':119 'kotlinx':81,106,113 'kotlinx-serialization-json':105,112 'kotlinx.serialization':282 'kotlinxseri':58,117 'ksp':20 'layer':290 'level':479,807 'librari':60 'limit':168,169,358 'list':353 'log':95,101,797,806 'logging-interceptor':100 'long':833 'map':231 'mapper':369 'messag':556,593 'minor':434 'miss':21,825 'mistak':761 'model':374 'modul':394,398,399 'multipart':250,722 'multipartbody.part':264,728 'multipartbody.part.createformdata':735 'must':770 'name':67,78,89,99,111,737 'network':37,397,527 'networkerror':561 'networkmodul':404 'networkresult':541,548,558,562,568,583 'networkresult.error':590,629 'networkresult.networkerror':601,635 'networkresult.success':585,625 'networkresult.timeout':605,639 'never':286 'new':416,815 'newbuild':456 'newtoken':696,706 'next':365 'non':428,763 'non-nul':427 'non-suspend':762 'nonetworkexcept':637 'null':330,423,454,688,700 'nullabl':429 'object':403,560,566 'okhttp':56,85,90,92,94,104,391,829 'okhttp-log':93 'okhttp3':88,98 'okhttpclient':472,499,500,509,710 'okhttpclient.builder':473,711 'order':179,180 'org.jetbrains.kotlin.plugin.serialization':123 'org.jetbrains.kotlinx':110 'overrid':609,666 'page':163,164,355 'pagedrespons':183,350 'paramet':141,156 'part':262 'partial':217 'partnam':726,736 'patch':215,219 'patchitem':224 'path':140,148,207,225,243,258 'plugin':118 'post':184,188,251 'privat':661 'progress':720 'provid':405,438,466,495,515 'provideauthinterceptor':441 'provideitemapiservic':518 'providejson':408 'provideokhttpcli':469 'provideretrofit':498 'put':197,201 'queri':155,162,167,172,178 'raw':817 're':648 're-authent':647 'readtimeout':488 'reduc':24 'refresh':645,656,682,686,689 'regist':708 'releas':800 'repositori':608,740 'request':194,212,451,465,613,623,673,683 'requestbodi':730,738 'respons':671,672,818,823 'response.request.newbuilder':702 'response.request.url.pathsegments.last':685 'result':528,534,615,620,748 'result.code':632 'result.data.todomain':627 'result.failure':630,636,640 'result.message':633 'result.success':626 'retri':676 'retrofit':1,35,43,54,62,68,70,72,84,392,503,519,520,768,783 'retrofit-cor':61 'retrofit-kotlin-seri':71 'retrofit.builder':504 'retrofit.create':522 'retrofit2':66,77 'return':687,699,701,734 'rout':669,670 'rule':39,126,278,389,525,642,715 'runblock':697 'runcatch':751 'safe':571 'safeapical':579,621 'seal':533,539 'serial':74,82,107,114,121 'serializ':284,291,331,347 'serialnam':304,311,321,341,363 'servic':128,134,765 'set':805 'setup':44,51 'ship':31 'singleton':406,439,467,496,516 'singletoncompon':401 'skill':4 'skill-retrofit' 'sockettimeoutexcept':604 'sort':173,174 'source-piyushverma0' 'state':23 'string':151,175,181,210,228,232,246,261,276,297,300,303,316,318,326,337,340,346,557,596,727,745 'success':544 'supabas':13 'suspend':145,159,190,204,222,240,255,270,574,577,581,610,741,764,772 'synchron':691 'timeout':567,826,830 'timeoutexcept':641 'timeunit.seconds':487,490,493 'titl':299,336,380,381 'token':26,275,448,453,460,644,655,690 'tokenauthent':658,713 'tokenprovid':442,443 'tokenprovider.gettoken':449 'tokenrepositori':663,664 'tokenrepository.refreshtoken':698 'tomediatyp':513 'tomediatypeornul':733 'toml':52 'topic-agent-skills' 'topic-ai-agent' 'topic-android' 'topic-antigravity' 'topic-claude-code' 'topic-codex' 'topic-cursor' 'topic-gemini-cli' 'topic-hilt' 'topic-jetpack-compose' 'topic-kotlin' 'topic-material3' 'total':361 'tri':584 'true':412,422,432,810 'type':777,819,822 'uistat':22 'uniform':531 'unit':247 'updat':200,218,322 'updatedat':325 'updateitem':206 'updateitemrequest':213 'upload':249,718,723 'uploadimag':257,743 'url':507,759,793,796 'usag':606 'use':794,821 'user':328,342 'user/profile':269 'userdto':277,329 'userid':345 'val':295,298,301,307,314,324,327,335,338,344,351,354,357,360,366,447,450,545,552,555,619,662,695,729,752 'valu':425 'version':53 'version.ref':69,83,91,103,116,124 'viewmodel':786 'withcontext':616,749 'without':418,776 'wrap':529,787 'writetimeout':491","prices":[{"id":"6dbb1a0d-d215-44ba-bd6c-7a3bdeb85073","listingId":"c973eda7-97d8-41b3-8ff6-2de175dfbd9e","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"piyushverma0","category":"android-agent-skills","install_from":"skills.sh"},"createdAt":"2026-05-18T13:14:50.558Z"}],"sources":[{"listingId":"c973eda7-97d8-41b3-8ff6-2de175dfbd9e","source":"github","sourceId":"piyushverma0/android-agent-skills/retrofit","sourceUrl":"https://github.com/piyushverma0/android-agent-skills/tree/main/skills/retrofit","isPrimary":false,"firstSeenAt":"2026-05-18T13:14:50.558Z","lastSeenAt":"2026-05-18T19:09:10.736Z"}],"details":{"listingId":"c973eda7-97d8-41b3-8ff6-2de175dfbd9e","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"piyushverma0","slug":"retrofit","github":{"repo":"piyushverma0/android-agent-skills","stars":8,"topics":["agent-skills","ai-agent","android","antigravity","claude-code","codex","cursor","gemini-cli","hilt","jetpack-compose","kotlin","material3","open-source","skills","supabase"],"license":"mit","html_url":"https://github.com/piyushverma0/android-agent-skills","pushed_at":"2026-04-27T09:15:31Z","description":"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.","skill_md_sha":"b0de80b8490a384fb016c7324b112869cdc8df56","skill_md_path":"skills/retrofit/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/piyushverma0/android-agent-skills/tree/main/skills/retrofit"},"layout":"multi","source":"github","category":"android-agent-skills","frontmatter":{},"skills_sh_url":"https://skills.sh/piyushverma0/android-agent-skills/retrofit"},"updatedAt":"2026-05-18T19:09:10.736Z"}}