Making Network Calls with Retrofit and OkHttp in Android
Modern Android apps rarely work in isolation. Whether an application loads weather data for Brisbane, displays public transport updates in Melbourne, or retrieves products from an online shop, it needs a reliable way to communicate with a remote server. Retrofit and OkHttp form a popular combination for sending HTTP requests, reading JSON responses, and handling network problems cleanly.
Retrofit provides a high-level interface for REST APIs, while OkHttp manages the underlying connection, request headers, caching, and interceptors. Together, they reduce repetitive networking code and make an Android project easier to test, maintain, and expand.
Why Retrofit And OkHttp Work Well Together
Retrofit lets you describe an API with a Kotlin interface instead of manually building every request. An annotation such as @GET identifies the endpoint, while function parameters represent path values or query strings. Retrofit then converts the server response into Kotlin objects through a JSON converter.
OkHttp operates underneath Retrofit. It opens connections, follows redirects, applies timeouts, and can log requests during development. This separation is useful: Retrofit concentrates on the API contract, while OkHttp handles transport-level behaviour.
A request should never run on Android’s main thread because a slow server or poor mobile connection could freeze the interface. Retrofit’s suspend functions work naturally with Kotlin coroutines, allowing the call to run from a ViewModel without blocking the screen.
Adding Dependencies And Internet Access
Add the required libraries to the module-level build.gradle.kts file. The exact versions change over time, so check the current stable releases before copying them into a production project.
dependencies {
implementation("com.squareup.retrofit2:retrofit:<version>")
implementation("com.squareup.retrofit2:converter-gson:<version>")
implementation("com.squareup.okhttp3:logging-interceptor:<version>")
}
The application also needs permission to access the network. Place this line in AndroidManifest.xml outside the <application> element:
<uses-permission android:name="android.permission.INTERNET" />
For learners building a complete sample, Android project examples can provide ideas for combining a remote data source with familiar Android components such as forms, lists, and detail screens.
Defining Models And The API Contract
Suppose an API returns a list of posts. A simple Kotlin data class might look like this:
data class Post(
val id: Int,
val title: String,
val body: String
)
The property names should match the JSON keys returned by the service. If the API uses different names, annotate fields or configure a serializer. Real APIs can contain missing values, so nullable properties such as val subtitle: String? are often safer than assuming every field is always present.
The service interface describes available endpoints:
interface PostApi {
@GET("posts")
suspend fun getPosts(): List<Post>
@GET("posts/{id}")
suspend fun getPost(@Path("id") id: Int): Post
}
A base URL must end with a slash, and the endpoint path is relative to that address. Query parameters use @Query, request bodies use @Body, and headers can be supplied with @Header or an OkHttp interceptor.
Building A Configured Retrofit Client
Create the Retrofit instance once rather than constructing it for every button tap or screen refresh. A singleton object is sufficient for a small application:
private val logging = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BASIC
}
private val client = OkHttpClient.Builder()
.addInterceptor(logging)
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.build()
val api: PostApi = Retrofit.Builder()
.baseUrl("https://example.com/api/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(PostApi::class.java)
Use Level.BODY only while debugging and avoid logging passwords, tokens, personal details, or payment information. This matters for Australian applications that handle customer records under the Privacy Act 1988. Sensitive information should never appear in shared logs or crash reports.
An interceptor can add a common authentication header, a request ID, or diagnostic information. Keep secrets out of the Android source code because values embedded in an APK can be extracted.
Calling The API From A ViewModel
A ViewModel provides a suitable place to coordinate the request and expose loading, success, and failure states to the UI. A sealed class keeps these states explicit:
sealed interface PostState {
data object Loading : PostState
data class Success(val posts: List<Post>) : PostState
data class Error(val message: String) : PostState
}
The ViewModel can update a StateFlow while handling expected exceptions:
class PostViewModel(
private val api: PostApi
) : ViewModel() {
private val _state = MutableStateFlow<PostState>(PostState.Loading)
val state: StateFlow<PostState> = _state
fun loadPosts() {
viewModelScope.launch {
_state.value = PostState.Loading
try {
_state.value = PostState.Success(api.getPosts())
} catch (e: IOException) {
_state.value = PostState.Error("Check your internet connection.")
} catch (e: HttpException) {
_state.value = PostState.Error("Server error: ${e.code()}")
}
}
}
}
A timeout can occur on a train between Sydney and Wollongong, while a server may respond with an HTTP 401, 404, or 500 status. Treating connectivity failures differently from HTTP failures helps the interface show useful messages rather than a generic error.
Displaying Results In A RecyclerView
The screen can collect the state and pass successful results to a RecyclerView adapter. Keep the adapter focused on binding data, leaving request logic inside the ViewModel. This produces a clearer architecture and makes rotation or process recreation easier to manage.
If your project needs a refresher on list presentation, this guide to RecyclerView custom adapters demonstrates the adapter and ViewHolder pattern that commonly displays Retrofit results.
Australian users may access an app over variable mobile coverage in regional areas, so show a progress indicator during the request and provide a retry action after failure. A cached list can be more helpful than an empty screen when someone is checking transport times or shopping from a low-bandwidth connection.
Handling Responses, Caching And Security
Retrofit responses can expose status codes and error bodies when you need more control than a direct List<Post> return type. For example, Response<List<Post>> lets the application check isSuccessful, inspect code(), and parse a meaningful server error.
OkHttp supports response caching, but caching rules should reflect the data. A public article list may be cached briefly, while account balances, medical information, and live availability should use stricter policies. Do not place authentication tokens or private responses in an uncontrolled shared cache.
Useful checks before shipping include:
- Verify airplane mode and weak-connection behaviour.
- Test 401, 404, 429, and 500 responses.
- Confirm timeouts show a recoverable error.
- Remove verbose logging from release builds.
In Australia, services that collect names, email addresses, location, or usage information should document what is collected and why. Consider consent, secure transport through HTTPS, deletion processes, and applicable Australian privacy obligations before releasing an app to the local market.
Testing Network Code In Realistic Conditions
A mocked API makes unit tests fast and predictable. With MockWebServer from the OkHttp project, you can return sample JSON, simulate a server error, and verify that the correct endpoint and headers were used. This avoids depending on a live service during every test run.
Test parsing with realistic payloads, including empty arrays, missing optional fields, unexpected characters, and pagination metadata. Also test repeated taps on a refresh button so that a user cannot accidentally launch several identical calls at once.
For production monitoring, record safe diagnostic data such as response duration and endpoint category rather than full request bodies. This helps identify whether problems affect users in Perth, Adelaide, or regional Queensland without exposing private customer content. A carefully designed Retrofit and OkHttp layer gives the rest of the application a stable foundation for future authentication, pagination, offline storage, and background synchronisation.