💾 Data, Network & Architecture · Lesson 33
Retrofit: Call REST APIs
What is a REST API?
A REST API is a web address that returns data (usually JSON) instead of a web page. Apps call it to get products, post orders or log in. Retrofit turns a Kotlin interface into those HTTP calls and Gson converts JSON into Kotlin objects.
The JSON we will read
We created a real JSON endpoint on codingeasily.in for the Student Diary "Tip of the day":
JSON
GET https://codingeasily.in/api/android-tips.json
{
"tips": [
{
"id": 1,
"title": "Use View Binding",
"text": "Replace findViewById with binding.myView – it is null-safe and type-safe.",
"image": "https://codingeasily.in/assets/img/tutorials/android/tip-1.png",
"link": "https://codingeasily.in/tutorial/android/android-view-binding/"
},
…
]
}
Step 1: Permission and dependencies
XML + TOML + Kotlin DSL
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.INTERNET" />
# libs.versions.toml
retrofit = "2.11.0"
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
// app/build.gradle.kts
implementation(libs.retrofit)
implementation(libs.retrofit.converter.gson)Step 2: Model classes
Property names must match the JSON keys (or use @SerializedName("json_key")).
Kotlin – network/Tip.kt
package com.codingeasily.studentdiary.network
/** Matches one object in https://codingeasily.in/api/android-tips.json */
data class Tip(
val id: Int,
val title: String,
val text: String,
val image: String,
val link: String
)
data class TipsResponse(val tips: List<Tip>)Step 3: API interface + Retrofit
Kotlin – network/TipsApi.kt
package com.codingeasily.studentdiary.network
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
/** Retrofit turns this interface into real HTTP code. */
interface TipsApi {
@GET("api/android-tips.json")
suspend fun getTips(): TipsResponse
companion object {
private const val BASE_URL = "https://codingeasily.in/"
val service: TipsApi by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(TipsApi::class.java)
}
}
}Step 4: Call it from a ViewModel
Kotlin
// From Student Diary – NoteViewModel
private val _tip = MutableLiveData<TipState>()
val tip: LiveData<TipState> = _tip
fun loadTip(force: Boolean = false) {
if (!force && _tip.value is TipState.Success) return
_tip.value = TipState.Loading
viewModelScope.launch {
_tip.value = try {
val tips = TipsApi.service.getTips().tips
if (tips.isEmpty()) TipState.Error("No tips yet")
else TipState.Success(tips[(System.currentTimeMillis() / 86_400_000L % tips.size).toInt()])
} catch (e: Exception) {
TipState.Error(e.message ?: "Network error")
}
}
}Query, path, POST and headers
These are exactly the kinds of calls the KartEasy e-commerce app makes to its PHP API:
Kotlin
interface ShopApi {
// GET products.php?category=3&page=1
@GET("products.php")
suspend fun products(@Query("category") categoryId: Int, @Query("page") page: Int = 1): ProductListResponse
// GET product/15
@GET("product/{id}")
suspend fun product(@Path("id") id: Int): ProductResponse
// POST JSON body {"email": "...", "password": "..."}
@POST("auth/login.php")
suspend fun login(@Body body: LoginRequest): LoginResponse
// POST form fields (application/x-www-form-urlencoded)
@FormUrlEncoded
@POST("feedback.php")
suspend fun feedback(@Field("name") name: String, @Field("message") message: String): SimpleResponse
// header for logged-in calls
@GET("orders.php")
suspend fun orders(@Header("Authorization") bearer: String): OrdersResponse
// when you need the status code
@GET("profile.php")
suspend fun profile(): Response<ProfileResponse>
}| Annotation | Result |
|---|---|
| @Query("q") | ?q=value |
| @Path("id") | replaces {id} in the path |
| @Body | object sent as JSON |
| @Field + @FormUrlEncoded | form fields (like an HTML form) |
| @Header | one header for this call |
Errors, timeouts and logging
Kotlin
try {
val res = api.profile() // Response<ProfileResponse>
if (res.isSuccessful) show(res.body()!!)
else if (res.code() == 401) logout()
else showError("Server error ${res.code()}")
} catch (e: IOException) { // no internet, timeout
showError("Check your internet connection")
} catch (e: HttpException) { // non-2xx when return type is not Response<>
showError("HTTP ${e.code()}")
}
// timeouts + logging (add com.squareup.okhttp3:logging-interceptor)
val client = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY })
.build()
Retrofit.Builder().baseUrl(BASE_URL).client(client).addConverterFactory(GsonConverterFactory.create()).build()Common mistakes
- No INTERNET permission →
SecurityException/ "Permission denied". - Calling the API from the main thread without suspend →
NetworkOnMainThreadException. - Field names not matching the JSON → values are null (or 0).
- Creating a new Retrofit instance for every call → slow; create it once (lazy singleton).
- Release build crashes with R8 → keep your model classes (see proguard-rules.pro in the project).
Summary
- INTERNET permission + Retrofit + Gson converter.
- Data classes match JSON; interface methods are
suspendwith @GET/@POST. - Call from viewModelScope, wrap in try/catch, expose a sealed UI state.
✅ Practice
Call https://codingeasily.in/api/android-tips.json and show all tips in a RecyclerView (title + text). Add pull-to-refresh with SwipeRefreshLayout.
Frequently Asked Questions
Why must the base URL end with a slash?
Retrofit joins the base URL and the path from @GET. Without the trailing / it throws "baseUrl must end in /".
Can I call http:// (not https) URLs?
Android blocks clear-text HTTP by default since Android 9. Use HTTPS. For a local test server you can allow it with a network security config, but never ship that.
Gson, Moshi or kotlinx.serialization?
All work with Retrofit. Gson is the simplest to start with and used in this course; Moshi and kotlinx.serialization are more Kotlin-friendly for larger apps.
