plugins/android-skills/skills/android-retrofit/SKILL.md
Use when setting up or working with Retrofit in Android — service interface definitions, coroutines integration, OkHttp configuration, Hilt module setup, and error handling in the repository layer.
npx skillsauth add rcosteira79/android-skills android-retrofitInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Modern Retrofit setup for Android using coroutines, kotlinx.serialization, and Hilt.
Declare all endpoints as suspend functions. Use Response<T> when you need access to status codes or error bodies; use the body type directly when 2xx is the only expected success case.
interface GitHubService {
// Direct body — throws HttpException on non-2xx
@GET("users/{user}/repos")
suspend fun listRepos(@Path("user") user: String): List<Repo>
// Response wrapper — gives access to code, headers, error body
@GET("users/{user}")
suspend fun getUser(@Path("user") user: String): Response<User>
}
interface SearchService {
@GET("search/users")
suspend fun searchUsers(
@Query("q") query: String,
@Query("sort") sort: String? = null,
@QueryMap options: Map<String, String> = emptyMap()
): SearchResult<User>
@GET("orgs/{org}/members")
suspend fun orgMembers(@Path("org") org: String): List<User>
}
interface UserService {
@POST("users")
suspend fun createUser(@Body user: CreateUserRequest): User
@FormUrlEncoded
@POST("user/edit")
suspend fun updateUser(
@Field("first_name") firstName: String,
@Field("last_name") lastName: String
): User
@Multipart
@PUT("user/photo")
suspend fun uploadPhoto(
@Part("description") description: RequestBody,
@Part photo: MultipartBody.Part
): User
}
interface AuthService {
// Static header
@Headers("Cache-Control: no-cache")
@GET("auth/refresh")
suspend fun refreshToken(): TokenResponse
// Dynamic header
@GET("user/profile")
suspend fun getProfile(@Header("Authorization") token: String): Profile
}
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideJson(): Json = Json {
ignoreUnknownKeys = true
coerceInputValues = true
isLenient = true
}
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder()
.addInterceptor(
HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
}
)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient, json: Json): Retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
@Provides
@Singleton
fun provideGitHubService(retrofit: Retrofit): GitHubService =
retrofit.create(GitHubService::class.java)
}
Catch network exceptions at the repository layer. Never let HttpException, IOException, or UnknownHostException leak into the ViewModel. See android-skills:android-data-layer for the full repository pattern including offline-first strategies.
Use the project's existing domain error types. If none exist, use a unified sealed class for all data-layer errors (not separate hierarchies per data source):
class GitHubRepository @Inject constructor(
private val service: GitHubService
) {
suspend fun listRepos(user: String): Result<List<Repo>> = try {
Result.success(service.listRepos(user))
} catch (e: HttpException) {
Result.failure(DataError.Server(e.code(), e.message()))
} catch (e: IOException) {
Result.failure(DataError.Network(e))
}
}
// Reuse the same error hierarchy across the data layer — see android-skills:android-data-layer
sealed class DataError(message: String, cause: Throwable? = null) : Exception(message, cause) {
class Network(cause: Throwable) : DataError("Network error", cause)
class Server(val code: Int, message: String?) : DataError("Server error $code: $message")
class Local(cause: Throwable) : DataError("Local storage error", cause)
}
Add auth tokens via an Interceptor rather than individual @Header parameters:
class AuthInterceptor @Inject constructor(
private val tokenProvider: TokenProvider
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = tokenProvider.getToken() ?: return chain.proceed(chain.request())
val request = chain.request().newBuilder()
.header("Authorization", "Bearer $token")
.build()
return chain.proceed(request)
}
}
Inject it into OkHttpClient via the Hilt module.
Response<T> — use only when needed// WRONG — wrapping every endpoint in Response<T> "just in case"
// Forces callers to check isSuccessful and handle nullable body, even when only the body matters
@GET("users/{user}/repos")
suspend fun listRepos(@Path("user") user: String): Response<List<Repo>>
// In repository — verbose and error-prone:
val response = service.listRepos(user)
if (response.isSuccessful) {
Result.success(response.body()!!) // nullable body needs !! or ?. handling
} else {
Result.failure(DataError.Server(response.code(), response.message()))
}
// RIGHT — direct return; Retrofit throws HttpException on non-2xx automatically
@GET("users/{user}/repos")
suspend fun listRepos(@Path("user") user: String): List<Repo>
// In repository — clean:
try {
Result.success(service.listRepos(user)) // non-null, direct
} catch (e: HttpException) {
Result.failure(DataError.Server(e.code(), e.message()))
}
RIGHT because direct return types are non-null and throw HttpException on non-2xx, giving you a clean try/catch at the repository level. Use Response<T> only when you need the error body content (e.g., validation messages) or response headers.
// WRONG — ViewModel catches HttpException directly; couples UI layer to network internals
class RepoViewModel(private val service: GitHubService) : ViewModel() {
fun loadRepos(user: String) {
viewModelScope.launch {
try {
_uiState.value = UiState.Success(service.listRepos(user))
} catch (e: HttpException) { // ViewModel knows about HTTP
_uiState.value = UiState.Error("Server error: ${e.code()}")
} catch (e: IOException) { // ViewModel knows about IO
_uiState.value = UiState.Error("Network error")
}
}
}
}
// RIGHT — repository maps to domain errors; ViewModel handles domain types only
class RepoViewModel(private val repository: GitHubRepository) : ViewModel() {
fun loadRepos(user: String) {
viewModelScope.launch {
repository.listRepos(user)
.onSuccess { repos -> _uiState.value = UiState.Success(repos) }
.onFailure { error ->
_uiState.value = when (error) {
is DataError.Network -> UiState.Error("Check your connection")
is DataError.Server -> UiState.Error("Something went wrong")
else -> UiState.Error("Unknown error")
}
}
}
}
}
WRONG because the ViewModel directly depends on Retrofit/OkHttp exception types. If you later swap Retrofit for Ktor, or add a cache layer, every ViewModel must change. The repository is the boundary — it maps network exceptions to domain error types that the ViewModel can handle without knowing the network implementation.
@Header// WRONG — token parameter on every endpoint; easy to forget, duplicates logic
@GET("user/profile")
suspend fun getProfile(@Header("Authorization") token: String): Profile
@GET("user/settings")
suspend fun getSettings(@Header("Authorization") token: String): Settings
// Caller must pass token every time — copy-paste prone:
service.getProfile("Bearer $token")
service.getSettings("Bearer $token")
// RIGHT — interceptor adds token automatically to all requests
class AuthInterceptor(private val tokenProvider: TokenProvider) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = tokenProvider.getToken()
?: throw IOException("Auth token unavailable") // fail fast — see note below
val request = chain.request().newBuilder()
.header("Authorization", "Bearer $token")
.build()
return chain.proceed(request)
}
}
// Clean service interface — no auth boilerplate:
@GET("user/profile")
suspend fun getProfile(): Profile
@GET("user/settings")
suspend fun getSettings(): Settings
WRONG because adding @Header("Authorization") to every endpoint is repetitive and fragile — one missing parameter means an unauthenticated request that fails at runtime, not compile time. An OkHttp interceptor applies the token uniformly to all requests.
Throw vs proceed: Throw when all endpoints require auth — a missing token should surface immediately rather than producing a confusing 401. If the
OkHttpClientis shared between authenticated and public endpoints, proceed without the header instead:?: return chain.proceed(chain.request()).
suspendResponse<T> only when specific status code handling is neededOkHttpClient logging is gated behind BuildConfig.DEBUGtesting
Use when implementing paginated lists in Android or Compose with Paging 3 — PagingSource, Pager and PagingConfig setup, RemoteMediator for offline-first lists, LazyPagingItems and itemKey integration in LazyColumn, dynamic filters via flatMapLatest, and unit tests with TestPager and asSnapshot. Triggers include Paging 3, infinite list, infinite scroll, paginated list, LazyPagingItems, collectAsLazyPagingItems, and cachedIn.
development
Use when setting up or working with Koin in Android or KMP projects — module declarations with Classic DSL or KSP annotations, ViewModel injection in Compose, scopes, Nav 3 entry providers, application startup, and compile-time verification via `verify()`. Triggers on Koin, `single`, `factory`, `koinViewModel`, `koinInject`, `parametersOf`, `startKoin`, "KMP DI", "shared DI".
development
Use when persisting key-value preferences or small typed settings on Android or KMP with Jetpack DataStore — Preferences vs Typed (Proto/JSON) selection, KMP factory with per-platform file paths, SharedPreferences migration, serializers with corruption handlers, DI singletons, and repository/MVI integration. Triggers on DataStore, Preferences, PreferenceDataStoreFactory, DataStoreFactory, preferencesDataStore, SharedPreferencesMigration, Serializer, or persistent settings work.
development
Use when writing, fixing, or refactoring Android/KMP code in Kotlin — supplements superpowers:test-driven-development with Android's three-tier test model, fake-first strategy, coroutine testing, and Compose UI testing.