Using Retrofit For Reliable Android Network Calls
Modern Android apps depend on remote data for everything from account access to parcel tracking. Retrofit provides a clean way to connect an application to REST APIs, convert JSON into Kotlin objects, and handle responses without filling an Activity with networking code.
For Australian developers, network behaviour deserves particular attention. A customer in Sydney may have fast fibre, while someone travelling between Perth and Adelaide can face patchy coverage. A well-designed Retrofit layer should remain predictable across Wi-Fi, mobile data, and slower regional connections.
Why Retrofit Fits Android Projects
Retrofit is a type-safe HTTP client for Android and Kotlin. It turns an interface definition into working network code, allowing developers to describe endpoints with annotations such as @GET, @POST, and @Path.
It commonly works with OkHttp for connections, caching, logging, and timeouts. A converter such as Kotlin serialization, Moshi, or Gson transforms JSON responses into data classes. This separation keeps screens focused on presentation while repositories manage remote data.
Retrofit also supports Kotlin coroutines, making asynchronous calls easier to read. Rather than nesting callbacks, a suspend function can return a result after the request finishes, while the ViewModel controls loading, success, and failure states.
Adding Retrofit To An Android App
With Gradle Version Catalogues, dependencies can be managed centrally. A typical project needs Retrofit, an HTTP converter, and optionally OkHttp logging during development:
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-moshi:2.11.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
The application also needs permission to access the internet:
<uses-permission android:name="android.permission.INTERNET" />
Create a single Retrofit instance rather than constructing a new client for every request. A shared instance reuses connections and gives the application one place to configure the base URL, converters, interceptors, and timeouts.
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(MoshiConverterFactory.create())
.build()
The base URL must end with a slash. Keep it on HTTPS, particularly when transmitting credentials, payment information, or personal details covered by Australian privacy requirements.
Defining Endpoints And Data Models
An API interface describes the available operations:
interface ProductApi {
@GET("products")
suspend fun getProducts(
@Query("page") page: Int
): List<Product>
@GET("products/{id}")
suspend fun getProduct(
@Path("id") id: Long
): Product
}
A matching data class represents the JSON payload:
data class Product(
val id: Long,
val name: String,
val priceCents: Int,
val available: Boolean
)
Using integer cents for Australian prices avoids floating-point rounding issues. The UI can format the value as Australian dollars with NumberFormat.getCurrencyInstance(Locale("en", "AU")), rather than assuming every market uses the same currency or date conventions.
For APIs with different field names, annotate properties or configure the chosen converter. Separating network models from database and UI models is useful when an API changes, such as when a local retailer adds delivery windows or GST-related fields.
Making Calls With Coroutines
A repository can hide the API implementation from the rest of the app:
class ProductRepository(
private val api: ProductApi
) {
suspend fun products(page: Int): Result<List<Product>> =
runCatching { api.getProducts(page) }
}
The ViewModel can expose state through StateFlow:
class ProductViewModel(
private val repository: ProductRepository
) : ViewModel() {
private val _state = MutableStateFlow<ProductState>(ProductState.Loading)
val state: StateFlow<ProductState> = _state
fun loadProducts() {
viewModelScope.launch {
repository.products(1)
.onSuccess { _state.value = ProductState.Success(it) }
.onFailure { _state.value = ProductState.Error(it) }
}
}
}
This approach prevents network work from running on the main thread. It also gives Jetpack Compose or the View system a clear state model for progress indicators, content, empty results, and errors.
An Australian travel app, for example, should retain useful cached information when a user loses reception outside regional towns. A screen that immediately replaces all content with a generic error message can be frustrating when the request fails temporarily.
Choosing A Network Approach
Retrofit is generally the best fit when an app consumes a structured REST service. Other approaches remain useful for different requirements, especially when a project needs generated clients, GraphQL queries, or a very small dependency footprint.
| Approach | Strengths | Limitations | Suitable Use |
|---|---|---|---|
| Retrofit with OkHttp | Clear API interfaces, converters, coroutines, mature ecosystem | Requires setup and model definitions | Most REST-based Android applications |
| Ktor Client | Kotlin-first and multiplatform support | More configuration may be needed on Android | Shared Kotlin Multiplatform projects |
| Volley | Straightforward request queues and image loading | Less natural for coroutine-focused architecture | Smaller apps and legacy codebases |
| Raw HttpURLConnection | Built into the platform | Verbose parsing, threading, and error handling | Rare low-level or dependency-restricted cases |
| GraphQL client | Fetches selected fields through typed queries | Requires a GraphQL backend and different caching concepts | APIs designed around GraphQL |
Retrofit does not automatically solve authentication, offline storage, retries, or pagination. Those concerns should be designed around the service and the user experience rather than added randomly to individual screens.
Handling Errors, Timeouts And Security
HTTP errors and transport failures are different. A 401 may require a refreshed login, a 404 can indicate missing content, and a 500 suggests a server-side problem. A timeout or UnknownHostException usually points to connectivity or DNS conditions.
Use an explicit result model when the UI needs to distinguish these states:
sealed interface ProductState {
data object Loading : ProductState
data class Success(val products: List<Product>) : ProductState
data class Error(val message: String) : ProductState
}
Configure sensible connection, read, and write timeouts through OkHttp. Avoid unlimited retries, which can drain a phone battery or create duplicate orders. For payment and booking operations, use idempotency keys or server-side safeguards before adding automatic retry behaviour.
Never place API secrets in the Android application. APKs can be inspected, so private credentials belong on a secure backend. Use certificate pinning only when the operational risks are understood, and avoid logging tokens, customer names, addresses, or order details in production.
Practical Recommendations For Production Apps
A robust Retrofit implementation benefits from a few consistent conventions. Keep API interfaces small, inject them into repositories, and test conversion and error handling independently from the user interface.
Australian products should also account for local expectations: display dates in the user’s region, handle daylight-saving differences between Melbourne and Brisbane, and avoid assuming every customer is in Sydney time. Services serving remote communities should test slow connections and intermittent access rather than relying only on office Wi-Fi.
- Keep one configured Retrofit and OkHttp instance for the application.
- Use Kotlin coroutines and
viewModelScopefor lifecycle-aware requests. - Model loading, success, empty, and failure states explicitly.
- Add pagination and caching for large catalogues or unreliable connections.
- Treat Australian dollars, GST, addresses, and time zones as domain data.
- Remove sensitive headers and response bodies from release logging.
- Test with airplane mode, throttled mobile data, expired tokens, and server errors.
Testing And Maintaining The API Layer
MockWebServer from the OkHttp project can simulate successful responses, malformed JSON, delays, and HTTP failures. This makes repository tests repeatable without depending on a live service or an internet connection. It is especially valuable when backend changes are released outside Australian business hours.
Contract tests can verify that field names, status codes, and authentication rules remain compatible. When an API evolves, version models carefully and avoid breaking old app releases that may remain installed for months.
For production monitoring, record request duration, endpoint category, and status code without collecting personal information. A spike in failures for users on Telstra, Optus, or a regional provider may reveal a connectivity issue that local emulator testing will not expose.
A clean Retrofit layer gives Android applications dependable access to remote services while keeping screens maintainable. Start with a small interface, define clear data and state models, and expand the repository as authentication, caching, pagination, and offline behaviour become necessary.
Apply these patterns to a sample catalogue or account feature, then test it across Melbourne Wi-Fi, Brisbane mobile data, and a simulated low-connectivity regional route. That practical workflow turns a basic network call into a reliable foundation for an Android product.