Making HTTP requests with Retrofit in Android
Making HTTP requests with Retrofit in Android is a practical way to connect an app to a REST API. Retrofit turns an HTTP endpoint into a typed Kotlin method, while a converter turns JSON into objects your interface can display. The result is less manual URL handling and fewer mistakes when data changes.
Each call has three parts: a base URL, an endpoint method, and a response model. A GET request usually reads information, a POST creates or submits something, and headers carry details such as authentication. Understanding that contract makes troubleshooting much easier.
The examples below use Kotlin, coroutines, Gson, and OkHttp. They apply to a small forecast service, but the same structure works for transport data, account records, catalogue items, and booking systems. The network layer stays separate from the Activity or Compose screen.
Australian use cases are familiar: a Sydney commuter checking train times on mobile data, a Melbourne café polling live food orders, or an app displaying whether the “arvo” will be hot. Local copy still matters, even when the API and technical terminology remain in English.
Add Retrofit to an Android project
Add Retrofit, the Gson converter, and a debug-only logging interceptor to the app module. The logging dependency should not reach a production build because request headers and response bodies can contain personal information.
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
debugImplementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
Also declare internet access in the manifest and create one Retrofit instance, normally in a dependency container or repository. The base URL must end with a slash, and a relative endpoint path must not begin with one.
<uses-permission android:name="android.permission.INTERNET" />
val retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
Model JSON responses with Kotlin
Create a Kotlin data class that matches the JSON payload. Keep server field names in mind because Gson uses the field names directly; annotate a property when the API uses a different convention.
data class Forecast(
val city: String,
val temperatureC: Double,
val condition: String,
val updatedAt: String
)
Describe the endpoint in an interface. The suspend function lets callers await the result without blocking the main thread, while the query parameter keeps the city choice separate from the request URL.
interface ForecastApi {
@GET("v1/forecast")
suspend fun forecast(@Query("city") city: String): Forecast
}
Send requests from coroutines
Put the API behind a repository so screens do not build URLs or handle raw exceptions. Launch the request from a lifecycle-aware scope such as viewModelScope; cancelling that scope cancels the associated work when the screen is no longer needed.
viewModelScope.launch {
uiState.value = ForecastUiState.Loading
uiState.value = try {
ForecastUiState.Success(repository.load(city))
} catch (error: HttpException) {
ForecastUiState.Error("Forecast request failed")
} catch (error: IOException) {
ForecastUiState.Error("Unable to reach the forecast service")
} catch (error: JsonParseException) {
ForecastUiState.Error("The forecast response was invalid")
}
}
Retrofit's suspend support already performs the HTTP call safely, so an extra Dispatchers.IO switch is usually unnecessary. Reserve explicit background dispatchers for blocking work outside Retrofit, and expose loading, success, and failure states to the UI rather than showing a bare progress spinner.
Handle errors and response states
A network failure is not the same as a rejected request. Connection drops, timeouts, malformed JSON, and HTTP 400, 401, 404, or 500 responses need different messages. Returning Response<T> exposes the status, headers, and error body instead of turning every failure into an exception.
Different return patterns suit different layers of an application.
| Pattern | Caller receives | Failure visibility | Best fit |
|---|---|---|---|
| Direct model | Data or an exception | Exception-based | Small, controlled clients |
Response<Forecast> |
Status, headers, and body | Explicit HTTP details | General REST endpoints |
| Sealed result | Success or Failure |
Typed domain errors | UI-facing repositories |
An OkHttp interceptor is a good place for a common authorisation header, request identifiers, or controlled retries. Give it a short connect and read timeout, avoid automatic retries for non-idempotent POST requests, and never print access tokens in debug logs.
Cache data for mobile and regional use
Retrofit uses OkHttp underneath, so an OkHttp cache can return stored responses when the network is unavailable. The server must permit caching with suitable Cache-Control headers; otherwise the client has nothing suitable to retain.
Design the screen around freshness rather than treating every request as an all-or-nothing event. Showing the last successful forecast with an “updated” time gives users useful context during a brief signal loss. Save a small timestamp in SharedPreferences or DataStore, and use Room when the cached payload is a relational record set.
Prepare a release-ready Android client
Keep environment-specific base URLs in build configuration, require HTTPS, and disable cleartext traffic. Treat API secrets as exposed if they are packaged in an APK, so obtain sensitive credentials from a backend at runtime. Test R8 or ProGuard rules with the release build because reflection-based JSON mapping can otherwise remove required model members.
Google Play is the normal distribution channel in Australia, where eligible consumer purchases are presented in Australian dollars and developers may need to meet Google's GST registration requirements. If a controlled sideload is needed for internal device testing, an APK download guide can explain the artefact, but an Android App Bundle should remain the production format.
Verify the request flow before release
Use MockWebServer in tests to return predictable JSON, status codes, delays, and malformed responses. This keeps the test repeatable and removes dependence on a live service, while a small instrumented test confirms that the real Retrofit client can parse the selected converter.
Before publishing, check the following:
- Keep staging and production base URLs separate and controlled by build type.
- Redact authorisation headers and personal data from network logs.
- Test valid JSON, missing fields, null values, and changed server field names.
- Exercise timeouts, empty bodies, malformed JSON, and common 4xx and 5xx statuses.
- Confirm loading, cached, empty, and error states survive configuration changes.
- Profile realistic payload sizes on both Wi-Fi and mobile data.
A dependable client comes from a typed contract, explicit failure handling, sensible caching, and release-grade configuration. Those choices keep the Android interface predictable when the network behaves unpredictably.