Networking with Retrofit

Introduction
Retrofit is a type-safe HTTP client for Android and Java, developed by Square. It's the most popular networking library in the Android ecosystem, providing a clean and efficient way to consume REST APIs. Retrofit turns your HTTP API into a Java/Kotlin interface, making network calls as simple as calling a method.
This comprehensive guide will take you from basic Retrofit setup to advanced features like custom interceptors, authentication, caching, and error handling. Whether you're building a simple app that fetches data from a public API or a complex application with multiple endpoints and authentication, Retrofit provides the tools you need for robust networking.
Why Use Retrofit?
Before diving into implementation, let's understand the benefits of using Retrofit:
- Type Safety: Compile-time verification of API contracts
- Clean API: Declarative interface-based approach
- Extensible: Easy to add custom converters, interceptors, and call adapters
- Coroutines Support: Native support for Kotlin coroutines
- Multiple Converters: Support for JSON, XML, Protocol Buffers, and more
- Interceptors: Easy to add logging, authentication, and caching
- Testing: Easy to mock and test network calls
- Performance: Efficient HTTP client with connection pooling
Setting Up Retrofit
Let's start by setting up Retrofit in your Android project.
Dependencies
// app/build.gradle.kts
dependencies {
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.retrofit2:converter-scalars:2.9.0")
// OkHttp (HTTP client used by Retrofit)
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
// Optional: For testing
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
}
Basic Project Structure
app/src/main/java/com/example/app/
├── network/
│ ├── api/
│ │ ├── ApiService.kt
│ │ └── AuthApiService.kt
│ ├── model/
│ │ ├── User.kt
│ │ ├── Post.kt
│ │ └── ApiResponse.kt
│ ├── interceptor/
│ │ ├── AuthInterceptor.kt
│ │ └── LoggingInterceptor.kt
│ └── RetrofitClient.kt
├── repository/
│ └── UserRepository.kt
└── ui/
└── MainActivity.kt
Data Models
Define your data models to represent API responses and requests.
Basic Data Models
data class User(
val id: Int,
val name: String,
val email: String,
val username: String,
val phone: String?,
val website: String?,
val company: Company?
)
data class Company(
val name: String,
val catchPhrase: String,
val bs: String
)
data class Post(
val id: Int,
val userId: Int,
val title: String,
val body: String
)
data class CreatePostRequest(
val title: String,
val body: String,
val userId: Int
)
API Response Wrapper
sealed class ApiResponse {
data class Success(val data: T) : ApiResponse()
data class Error(val message: String, val code: Int? = null) : ApiResponse()
class Loading : ApiResponse()
}
// For APIs that return wrapped responses
data class ApiWrapper(
val data: T,
val message: String,
val success: Boolean,
val code: Int
)
API Service Interface
Define your API endpoints using Retrofit annotations.
Basic API Service
interface ApiService {
// GET requests
@GET("users")
suspend fun getUsers(): List
@GET("users/{id}")
suspend fun getUserById(@Path("id") userId: Int): User
@GET("users/{id}/posts")
suspend fun getUserPosts(@Path("id") userId: Int): List
// Query parameters
@GET("posts")
suspend fun getPosts(
@Query("userId") userId: Int? = null,
@Query("_limit") limit: Int = 10,
@Query("_start") start: Int = 0
): List
// POST requests
@POST("posts")
suspend fun createPost(@Body post: CreatePostRequest): Post
// PUT requests
@PUT("posts/{id}")
suspend fun updatePost(
@Path("id") postId: Int,
@Body post: CreatePostRequest
): Post
// DELETE requests
@DELETE("posts/{id}")
suspend fun deletePost(@Path("id") postId: Int): Response
// File upload
@Multipart
@POST("upload")
suspend fun uploadFile(
@Part file: MultipartBody.Part,
@Part("description") description: RequestBody
): Response
// Form data
@FormUrlEncoded
@POST("login")
suspend fun login(
@Field("username") username: String,
@Field("password") password: String
): Response
}
Advanced API Service
interface AdvancedApiService {
// Headers
@Headers(
"Content-Type: application/json",
"Accept: application/json"
)
@GET("protected/users")
suspend fun getProtectedUsers(): List
// Dynamic headers
@GET("users")
suspend fun getUsersWithHeader(
@Header("Authorization") token: String,
@Header("User-Agent") userAgent: String
): List
// Multiple path parameters
@GET("users/{userId}/posts/{postId}/comments")
suspend fun getComments(
@Path("userId") userId: Int,
@Path("postId") postId: Int
): List
// Query map for dynamic parameters
@GET("search")
suspend fun search(
@QueryMap queryMap: Map
): SearchResponse
// URL with query parameters
@GET
suspend fun getDataFromUrl(@Url url: String): Response
// Streaming response
@Streaming
@GET("large-file")
suspend fun downloadLargeFile(): Response
}
Retrofit Client Setup
Configure Retrofit with custom settings, interceptors, and converters.
Basic Retrofit Setup
object RetrofitClient {
private const val BASE_URL = "https://jsonplaceholder.typicode.com/"
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
val apiService: ApiService = retrofit.create(ApiService::class.java)
}
Advanced Retrofit Setup with Interceptors
object RetrofitClient {
private const val BASE_URL = "https://api.example.com/"
// Authentication interceptor
private val authInterceptor = Interceptor { chain ->
val originalRequest = chain.request()
val newRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer ${getAuthToken()}")
.build()
chain.proceed(newRequest)
}
// Logging interceptor
private val loggingInterceptor = HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
}
// Cache interceptor
private val cacheInterceptor = Interceptor { chain ->
val response = chain.proceed(chain.request())
response.newBuilder()
.header("Cache-Control", "public, max-age=3600")
.build()
}
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.addInterceptor(loggingInterceptor)
.addInterceptor(cacheInterceptor)
.addNetworkInterceptor(cacheInterceptor)
.cache(Cache(File(context.cacheDir, "http_cache"), 10 * 1024 * 1024)) // 10MB cache
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build()
val apiService: ApiService = retrofit.create(ApiService::class.java)
private fun getAuthToken(): String {
// Get token from SharedPreferences or secure storage
return "your_auth_token"
}
}
Custom Interceptors
Interceptors allow you to modify requests and responses globally.
Authentication Interceptor
class AuthInterceptor(private val tokenManager: TokenManager) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
// Skip authentication for login/register endpoints
if (originalRequest.url.encodedPath.contains("/auth/")) {
return chain.proceed(originalRequest)
}
val token = tokenManager.getToken()
if (token == null) {
// Handle no token case - redirect to login
throw UnauthorizedException("No authentication token")
}
val newRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer $token")
.build()
val response = chain.proceed(newRequest)
// Handle token refresh
if (response.code == 401) {
tokenManager.refreshToken()
val newToken = tokenManager.getToken()
val retryRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
return chain.proceed(retryRequest)
}
return response
}
}
class TokenManager(private val context: Context) {
private val prefs = context.getSharedPreferences("auth_prefs", Context.MODE_PRIVATE)
fun getToken(): String? = prefs.getString("auth_token", null)
fun saveToken(token: String) {
prefs.edit().putString("auth_token", token).apply()
}
fun refreshToken() {
// Implement token refresh logic
}
fun clearToken() {
prefs.edit().remove("auth_token").apply()
}
}
Error Handling Interceptor
class ErrorHandlingInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
when (response.code) {
200, 201, 204 -> return response
400 -> throw BadRequestException("Bad request")
401 -> throw UnauthorizedException("Unauthorized")
403 -> throw ForbiddenException("Forbidden")
404 -> throw NotFoundException("Resource not found")
500 -> throw ServerException("Internal server error")
else -> throw NetworkException("Network error: ${response.code}")
}
}
}
sealed class NetworkException(message: String) : Exception(message)
class BadRequestException(message: String) : NetworkException(message)
class UnauthorizedException(message: String) : NetworkException(message)
class ForbiddenException(message: String) : NetworkException(message)
class NotFoundException(message: String) : NetworkException(message)
class ServerException(message: String) : NetworkException(message)
Repository Pattern
Use the Repository pattern to abstract network operations and provide a clean API.
Basic Repository
class UserRepository(private val apiService: ApiService) {
suspend fun getUsers(): Result> {
return try {
val users = apiService.getUsers()
Result.success(users)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun getUserById(userId: Int): Result {
return try {
val user = apiService.getUserById(userId)
Result.success(user)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun createUser(user: CreateUserRequest): Result {
return try {
val createdUser = apiService.createUser(user)
Result.success(createdUser)
} catch (e: Exception) {
Result.failure(e)
}
}
}
Advanced Repository with Caching
class UserRepository(
private val apiService: ApiService,
private val userDao: UserDao,
private val networkBoundResource: NetworkBoundResource
) {
fun getUsers(): Flow>> = networkBoundResource.asFlow(
query = {
userDao.getAllUsers()
},
fetch = {
apiService.getUsers()
},
saveFetchResult = { users ->
userDao.insertUsers(users)
},
shouldFetch = { cachedUsers ->
cachedUsers.isEmpty() || isStale(cachedUsers)
},
onFetchError = { error ->
Log.e("UserRepository", "Error fetching users", error)
}
)
suspend fun getUserById(userId: Int): Resource {
return try {
// Try cache first
val cachedUser = userDao.getUserById(userId)
if (cachedUser != null) {
Resource.success(cachedUser)
} else {
// Fetch from network
val user = apiService.getUserById(userId)
userDao.insertUser(user)
Resource.success(user)
}
} catch (e: Exception) {
Resource.error(e.message ?: "Unknown error", null)
}
}
private fun isStale(users: List): Boolean {
// Check if data is older than 5 minutes
val oldestUser = users.minByOrNull { it.lastUpdated }
return oldestUser?.lastUpdated?.let {
System.currentTimeMillis() - it > 5 * 60 * 1000
} ?: true
}
}
sealed class Resource {
class Success(val data: T) : Resource()
class Error(val message: String, val data: T? = null) : Resource()
class Loading(val data: T? = null) : Resource()
}
ViewModel Integration
Integrate Retrofit with ViewModels for reactive UI updates.
Basic ViewModel
class UserViewModel(
private val userRepository: UserRepository
) : ViewModel() {
private val _users = MutableStateFlow>>(Resource.Loading())
val users: StateFlow>> = _users.asStateFlow()
private val _selectedUser = MutableStateFlow(null)
val selectedUser: StateFlow = _selectedUser.asStateFlow()
init {
loadUsers()
}
fun loadUsers() {
viewModelScope.launch {
_users.value = Resource.Loading()
try {
val result = userRepository.getUsers()
_users.value = if (result.isSuccess) {
Resource.Success(result.getOrNull() ?: emptyList())
} else {
Resource.Error(result.exceptionOrNull()?.message ?: "Unknown error")
}
} catch (e: Exception) {
_users.value = Resource.Error(e.message ?: "Unknown error")
}
}
}
fun selectUser(user: User) {
_selectedUser.value = user
}
fun getUserById(userId: Int) {
viewModelScope.launch {
try {
val result = userRepository.getUserById(userId)
_selectedUser.value = result.data
} catch (e: Exception) {
// Handle error
}
}
}
}
Advanced ViewModel with Flow
class UserViewModel(
private val userRepository: UserRepository
) : ViewModel() {
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow = _searchQuery.asStateFlow()
val users: StateFlow>> = combine(
userRepository.getUsers(),
searchQuery
) { usersResource, query ->
when (usersResource) {
is Resource.Success -> {
val filteredUsers = if (query.isEmpty()) {
usersResource.data
} else {
usersResource.data.filter { user ->
user.name.contains(query, ignoreCase = true) ||
user.email.contains(query, ignoreCase = true)
}
}
Resource.Success(filteredUsers)
}
is Resource.Error -> usersResource
is Resource.Loading -> usersResource
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = Resource.Loading()
)
fun setSearchQuery(query: String) {
_searchQuery.value = query
}
fun refreshUsers() {
viewModelScope.launch {
userRepository.refreshUsers()
}
}
}
Advanced Features
Retrofit provides several advanced features for complex use cases.
Custom Converters
class DateConverter : Converter.Factory() {
override fun responseBodyConverter(
type: Type,
annotations: Array,
retrofit: Retrofit
): Converter? {
if (type == Date::class.java) {
return Converter { value ->
val dateString = value.string()
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
.parse(dateString)
}
}
return null
}
override fun requestBodyConverter(
type: Type,
parameterAnnotations: Array,
methodAnnotations: Array,
retrofit: Retrofit
): Converter<*, RequestBody>? {
if (type == Date::class.java) {
return Converter { value ->
val dateString = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
.format(value as Date)
RequestBody.create(MediaType.parse("text/plain"), dateString)
}
}
return null
}
}
// Use custom converter
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(DateConverter())
.addConverterFactory(GsonConverterFactory.create())
.build()
File Upload
class FileUploadService {
suspend fun uploadImage(imageFile: File): Result {
return try {
val requestBody = RequestBody.create(
MediaType.parse("image/*"),
imageFile
)
val multipartBody = MultipartBody.Part.createFormData(
"image",
imageFile.name,
requestBody
)
val description = RequestBody.create(
MediaType.parse("text/plain"),
"Uploaded image"
)
val response = apiService.uploadFile(multipartBody, description)
if (response.isSuccessful) {
Result.success(response.body()?.url ?: "")
} else {
Result.failure(Exception("Upload failed"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun uploadMultipleFiles(files: List): Result> {
return try {
val multipartBodies = files.map { file ->
val requestBody = RequestBody.create(
MediaType.parse("image/*"),
file
)
MultipartBody.Part.createFormData("images", file.name, requestBody)
}
val response = apiService.uploadMultipleFiles(multipartBodies)
if (response.isSuccessful) {
Result.success(response.body()?.urls ?: emptyList())
} else {
Result.failure(Exception("Upload failed"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
}
Download Files
class FileDownloadService {
suspend fun downloadFile(url: String, destinationFile: File): Result {
return try {
val response = apiService.downloadFile(url)
if (response.isSuccessful) {
response.body()?.let { responseBody ->
responseBody.byteStream().use { inputStream ->
destinationFile.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
}
Result.success(destinationFile)
} ?: Result.failure(Exception("Empty response body"))
} else {
Result.failure(Exception("Download failed: ${response.code()}"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun downloadFileWithProgress(
url: String,
destinationFile: File,
onProgress: (Float) -> Unit
): Result {
return try {
val response = apiService.downloadFile(url)
if (response.isSuccessful) {
response.body()?.let { responseBody ->
val contentLength = responseBody.contentLength()
var bytesRead = 0L
responseBody.byteStream().use { inputStream ->
destinationFile.outputStream().use { outputStream ->
val buffer = ByteArray(8192)
var bytes: Int
while (inputStream.read(buffer).also { bytes = it } != -1) {
outputStream.write(buffer, 0, bytes)
bytesRead += bytes
if (contentLength > 0) {
val progress = (bytesRead.toFloat() / contentLength) * 100
onProgress(progress)
}
}
}
}
Result.success(destinationFile)
} ?: Result.failure(Exception("Empty response body"))
} else {
Result.failure(Exception("Download failed: ${response.code()}"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
}
Testing
Testing Retrofit implementations is crucial for reliable networking code.
Unit Testing with MockWebServer
@RunWith(AndroidJUnit4::class)
class ApiServiceTest {
private lateinit var mockWebServer: MockWebServer
private lateinit var apiService: ApiService
@Before
fun setup() {
mockWebServer = MockWebServer()
mockWebServer.start()
val retrofit = Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
.addConverterFactory(GsonConverterFactory.create())
.build()
apiService = retrofit.create(ApiService::class.java)
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@Test
fun `getUsers returns list of users`() = runTest {
// Given
val mockResponse = """
[
{
"id": 1,
"name": "John Doe",
"email": "john@example.com"
}
]
""".trimIndent()
mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setBody(mockResponse)
)
// When
val users = apiService.getUsers()
// Then
assertThat(users).hasSize(1)
assertThat(users[0].name).isEqualTo("John Doe")
assertThat(users[0].email).isEqualTo("john@example.com")
}
@Test
fun `getUserById returns user`() = runTest {
// Given
val mockResponse = """
{
"id": 1,
"name": "John Doe",
"email": "john@example.com"
}
""".trimIndent()
mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setBody(mockResponse)
)
// When
val user = apiService.getUserById(1)
// Then
assertThat(user.id).isEqualTo(1)
assertThat(user.name).isEqualTo("John Doe")
}
@Test
fun `createUser returns created user`() = runTest {
// Given
val request = CreateUserRequest("Jane Doe", "jane@example.com")
val mockResponse = """
{
"id": 2,
"name": "Jane Doe",
"email": "jane@example.com"
}
""".trimIndent()
mockWebServer.enqueue(
MockResponse()
.setResponseCode(201)
.setBody(mockResponse)
)
// When
val user = apiService.createUser(request)
// Then
assertThat(user.id).isEqualTo(2)
assertThat(user.name).isEqualTo("Jane Doe")
}
}
Repository Testing
@RunWith(MockitoJUnitRunner::class)
class UserRepositoryTest {
@Mock
private lateinit var apiService: ApiService
@Mock
private lateinit var userDao: UserDao
private lateinit var userRepository: UserRepository
@Before
fun setup() {
userRepository = UserRepository(apiService, userDao)
}
@Test
fun `getUsers returns success with data`() = runTest {
// Given
val users = listOf(
User(1, "John Doe", "john@example.com"),
User(2, "Jane Smith", "jane@example.com")
)
whenever(apiService.getUsers()).thenReturn(users)
// When
val result = userRepository.getUsers()
// Then
assertThat(result.isSuccess).isTrue()
assertThat(result.getOrNull()).isEqualTo(users)
}
@Test
fun `getUsers returns failure on exception`() = runTest {
// Given
val exception = IOException("Network error")
whenever(apiService.getUsers()).thenThrow(exception)
// When
val result = userRepository.getUsers()
// Then
assertThat(result.isFailure).isTrue()
assertThat(result.exceptionOrNull()).isEqualTo(exception)
}
}
Best Practices
API Design
- Use meaningful endpoint names: Clear and descriptive API paths
- Consistent response formats: Standardize API responses
- Proper HTTP status codes: Use appropriate status codes
- Version your APIs: Include versioning in URLs or headers
Error Handling
- Handle network errors: Catch and handle exceptions properly
- Provide meaningful error messages: User-friendly error descriptions
- Implement retry logic: For transient failures
- Log errors appropriately: For debugging and monitoring
Performance
- Use connection pooling: Configure OkHttp properly
- Implement caching: Reduce unnecessary network calls
- Compress responses: Use gzip compression
- Optimize image loading: Use appropriate image libraries
Common Pitfalls
Avoiding Common Mistakes
- Don't block the main thread: Always use coroutines or background threads
- Don't ignore errors: Always handle network failures
- Don't hardcode URLs: Use configuration files or build variants
- Don't forget timeouts: Set appropriate connection and read timeouts
Debugging Tips
- Use logging interceptors: Monitor network requests and responses
- Enable network inspection: Use tools like Charles Proxy
- Test with mock servers: Use MockWebServer for testing
- Monitor performance: Use Android Studio profiler
Practice Exercises
Try these exercises to reinforce your Retrofit knowledge:
Exercise 1: Weather App
// Create a weather app with:
// - Fetch current weather by location
// - 5-day forecast
// - Weather alerts
// - Offline caching
// - Location-based weather
Exercise 2: News App
// Build a news app with:
// - Fetch news articles by category
// - Search functionality
// - Bookmark articles
// - Share articles
// - Push notifications
Exercise 3: Social Media App
// Create a social media app with:
// - User authentication
// - Post creation and sharing
// - Image upload
// - Real-time messaging
// - Push notifications
Next Steps
Now that you have a solid foundation in Retrofit, explore these advanced topics:
- GraphQL: Modern API query language
- gRPC: High-performance RPC framework
- WebSocket: Real-time bidirectional communication
- Server-Sent Events: Real-time updates from server
- API Versioning: Managing API evolution
- Rate Limiting: Implementing request throttling
Resources
Summary
Retrofit is a powerful and flexible HTTP client that makes networking in Android applications straightforward and efficient. Its type-safe approach, extensive customization options, and seamless integration with modern Android development patterns make it the preferred choice for consuming REST APIs.
You've learned about API service interfaces, Retrofit configuration, interceptors, error handling, testing, and best practices. Remember to always consider performance, error handling, and user experience when implementing networking in your applications.
Practice regularly with different APIs, experiment with advanced features, and stay updated with the latest networking patterns and libraries. The more you work with Retrofit, the more you'll appreciate its power and flexibility for building robust, network-enabled applications.