I wrote the network layer of my Pokedex app, and then I sat there staring at it, genuinely confused.
interface PokeApi {
@GET("pokemon")
suspend fun getPokemonList(
@Query("limit") limit: Int,
@Query("offset") offset: Int
): PokemonListResponse
@GET("pokemon/{name}")
suspend fun getPokemonDetail(
@Path("name") name: String
): PokemonDetail
}
The functions are empty. There's no code inside them. getPokemonList doesn't open a connection, doesn't send anything, doesn't parse anything. It's just a name and a return type. And yet — this is the thing that fetches every Pokemon in the app.
My first guess was wrong, so let me tell you the wrong version first, because you might be thinking it too. I thought: "maybe the data models fetch the data themselves." Then I looked at a data model:
data class PokemonResult(
val name: String,
val url: String
)
There's no networking code in there either. It's two empty slots. A data model can't open the internet — it's a plate, not a cook. It just holds food that someone else made.
So the puzzle stood: the interface has no body, the models can't fetch — who writes the actual networking code?
The answer: Retrofit writes it for you
Here's the mental model that fixed it for me. Think of a restaurant.
Your interface is the menu. It lists what you can order — "list of Pokemon," "one Pokemon's details." A menu doesn't contain recipes and it doesn't cook anything. It just declares what's orderable.
Retrofit is the kitchen. And this one line (which lives in your Hilt module) is where the magic happens:
val api = retrofit.create(PokeApi::class.java)
create() reads your menu and generates a hidden class that implements your interface — a real cook. That generated class is where all the actual code lives: build the URL, attach the query parameters, hand it to OkHttp, wait for the response, pass the JSON to Gson. That generated implementation is the "body" that looked missing.
So the full chain is:
- Interface — the menu. What you can request. No code.
-
retrofit.create()— builds the cook. This writes the real networking code, at runtime. - Gson — plates the JSON response onto your data model.
- Data model — the plate. Holds the result. Never fetches anything.
You declare what to fetch. Retrofit generates the how. The models only hold the result.
How one function becomes one URL
Your baseUrl (set in Hilt) is:
https://pokeapi.co/api/v2/
Retrofit glues the @GET("...") path onto the end. Three annotations do the work:
-
@GET("pokemon")— the path stuck onto the base →.../pokemon -
@Query("limit") limit: Int— adds?limit=...to the URL -
@Path("name") name: String— fills a{name}blank inside the path
And suspend is there because a network call takes time. suspend lets it run on a coroutine, so it never freezes the screen while it waits.
@path vs @Query — the part everyone mixes up
This confused me for a while, so here's the analogy that finally made it stick: a library.
A @Path is a book's shelf address. Every book sits at a fixed spot. If you know the spot, you walk straight to that one book. Every book has its own unique address.
pokemon/bulbasaur ← bulbasaur is the address of one exact Pokemon
bulbasaur is baked into the path with a /. It identifies one specific thing.
A @Query is a word you hand to the search desk. There's one search desk. You fill a slip — "topic: headphones" — and the librarian searches. Tomorrow someone hands the same desk a different word. One desk, many words.
pokemon?limit=20&offset=0 ← options handed to the list endpoint
limit and offset come after the ?. They don't change which endpoint — they just tune the same list.
The rule I use now:
Does the value have its own address? →
@Path(which exact thing).
Is it a word/option you hand to a shared endpoint, after a?? →@Query.
Interview questions on the network layer
Q: Why is the Retrofit API an interface and not a class?
Because you only declare what requests exist. You don't implement them. Retrofit generates the implementation at runtime via retrofit.create().
Q: The functions have no body — where does the networking code come from?
Retrofit's create() builds a hidden class that implements the interface. That generated class contains the real code: URL building, OkHttp call, Gson parsing. The interface is just the menu.
Q: Difference between @path and @Query?
@Path fills a {placeholder} inside the URL path — it identifies one specific resource (pokemon/bulbasaur). @Query adds a ?key=value option to the URL — it tunes a request (pokemon?limit=20). Identity vs option.
Q: Why is the function suspend?
A network call is slow. suspend makes it a coroutine-friendly call that runs off the main thread, so the UI doesn't freeze while waiting. Without it, you'd block the main thread (or need a Call<T> and manual callbacks).
Q: You rename @Path("name") to @Path("pokemonName") but leave the path as pokemon/{name}. Compile error or runtime error?
Runtime. The compiler sees valid Kotlin and valid strings — it never checks that the @Path label matches the {name} blank. Retrofit checks that at runtime and throws because it can't find a placeholder named pokemonName.
Q: @GET("Pokemon") with a capital P compiles fine but returns nothing. Why didn't the compiler catch it?
Because the path is a string, and URLs are case-sensitive. The compiler doesn't validate URL strings. Pokemon ≠ pokemon, so the server returns a 404 — but only at runtime.
None of this was about memorizing Retrofit annotations. It was about one shift: you're not writing the networking code — you're writing a description that Retrofit turns into networking code. The interface is a menu. Retrofit is the kitchen. And the compiler is only checking your grammar, not your order.
Top comments (0)