Most Retrofit tutorials show you isolated code snippets. This article traces the entire lifecycle of a Retrofit call — from app startup to data appearing on screen — using real code from a Pokedex app built with Kotlin, Hilt, Coroutines, and Jetpack Compose.
By the end, you'll understand every link in the chain!
What Is Retrofit?
Retrofit is a type-safe HTTP client for Android and Java. Instead of writing raw HTTP requests, you describe your API as a Kotlin interface, and Retrofit generates the networking code for you behind the scenes.
interface PokeApi {
@GET("pokemon")
suspend fun getPokemonList(
@Query("limit") limit: Int,
@Query("offset") offset: Int,
): PokemonList
@GET("pokemon/{name}")
suspend fun getPokemonInfo(
@Path("name") name: String,
): Pokemon
}
That's it. No manual HttpURLConnection, no manual JSON parsing. Retrofit handles building the request and Gson (or Moshi) handles converting the JSON response into your Kotlin objects.
The 3 Building Blocks
Every Retrofit setup needs exactly three things:
- The Interface — what requests can be made
- The Builder — where to send them and how to parse responses
- The Response model — what shape the data comes back in
val retrofit = Retrofit.Builder()
.baseUrl("https://pokeapi.co/api/v2/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val api = retrofit.create(PokeApi::class.java)
The Full Lifecycle, Step by Step
Here's the entire request lifecycle traced through a real app architecture: Compose UI → ViewModel → Repository → Retrofit → OkHttp → Server → Gson → back up the chain.
Step 1 — App Starts, Hilt Wakes Up
@HiltAndroidApp
class PokedexApplication : Application() {
override fun onCreate() {
super.onCreate()
Timber.plant(Timber.DebugTree())
}
}
@HiltAndroidApp tells Hilt to start managing dependency injection across the app.
Step 2 — Hilt Builds Retrofit Once (Singleton)
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Singleton
@Provides
fun providePokeApi(): PokeApi {
val okHttpClient = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.header("User-Agent", "Mozilla/5.0 ...")
.header("Accept", "application/json")
.build()
chain.proceed(request)
}
.build()
return Retrofit.Builder()
.baseUrl(Constants.BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(PokeApi::class.java)
}
@Singleton
@Provides
fun providePokemonRepository(api: PokeApi) = PokemonRepository(api)
}
Building a Retrofit instance is expensive — connection pools, thread pools, parsers. @Singleton ensures it's built once and reused everywhere, instead of recreated per screen.
Step 3 — ViewModel Is Created, Requests Data Immediately
@HiltViewModel
class PokemonListViewModel @Inject constructor(
private val repository: PokemonRepository,
) : ViewModel() {
var pokemonList = mutableStateOf<List<PokedexListEntry>>(listOf())
var isLoading = mutableStateOf(false)
init {
loadPokemonPaginated()
}
fun loadPokemonPaginated() {
viewModelScope.launch {
isLoading.value = true
val result = repository.getPokemonList(PAGE_SIZE, curPage * PAGE_SIZE)
// handle result below
}
}
}
Hilt injects the PokemonRepository automatically — no manual instantiation needed. viewModelScope.launch starts a coroutine so the network call doesn't block the main thread.
Step 4 — Repository Calls the API and Handles Failure
@Singleton
class PokemonRepository @Inject constructor(
private val api: PokeApi,
) {
suspend fun getPokemonList(limit: Int, offset: Int): Resource<PokemonList> {
val response = try {
api.getPokemonList(limit, offset)
} catch (e: Exception) {
return Resource.Error("Couldn't load Pokemon")
}
return Resource.Success(response)
}
}
The Repository pattern decouples the ViewModel from the data source. The ViewModel doesn't know — or care — whether data comes from a network call, a cache, or a database.
Step 5 — Retrofit Builds the URL and OkHttp Sends It
Given:
@GET("pokemon")
suspend fun getPokemonList(
@Query("limit") limit: Int,
@Query("offset") offset: Int,
): PokemonList
Calling getPokemonList(20, 0) produces:
https://pokeapi.co/api/v2/pokemon?limit=20&offset=0
OkHttp (the engine underneath Retrofit) opens the connection, attaches any interceptor headers, and sends the request over the wire.
Step 6 — Server Responds With JSON
{
"count": 1302,
"next": "https://pokeapi.co/api/v2/pokemon?offset=20&limit=20",
"previous": null,
"results": [
{ "name": "bulbasaur", "url": "https://pokeapi.co/api/v2/pokemon/1/" }
]
}
Step 7 — Gson Converts JSON Into Kotlin Objects
data class PokemonList(
@SerializedName("count") val count: Int,
@SerializedName("next") val next: String,
@SerializedName("previous") val previous: Any,
@SerializedName("results") val results: List<Result>,
)
@SerializedName maps a JSON key to a Kotlin property — essential when the server uses snake_case and your code uses camelCase.
Step 8 — Result Flows Back Up, Wrapped in a Sealed Class
sealed class Resource<T>(val data: T? = null, val message: String? = null) {
class Success<T>(data: T) : Resource<T>(data)
class Error<T>(message: String, data: T? = null) : Resource<T>(data, message)
class Loading<T>(data: T? = null) : Resource<T>(data)
}
This wrapper lets the ViewModel handle success, error, and loading as explicit states rather than nullable guesswork.
Step 9 — ViewModel Updates State, Compose Redraws
when (result) {
is Resource.Success -> {
pokemonList.value += result.data!!.results.map { /* map to UI model */ }
isLoading.value = false
}
is Resource.Error -> {
loadError.value = result.message!!
isLoading.value = false
}
else -> {}
}
Because pokemonList is a Compose mutableStateOf, the moment it changes, any composable reading it recomposes automatically. No manual UI refresh needed.
The Full Chain, Visualized
App starts → Hilt builds Retrofit + Repository (once)
↓
ViewModel created → Repository injected → init{} fires
↓
viewModelScope.launch → repository.getPokemonList()
↓
api.getPokemonList() → Retrofit builds the URL
↓
OkHttp attaches headers, sends the HTTP request
↓
Server responds with JSON
↓
Gson converts JSON → Kotlin data class
↓
Repository wraps it in Resource.Success / Resource.Error
↓
ViewModel updates mutableStateOf
↓
Compose recomposes → user sees the data

Top comments (0)