Networking with Retrofit

Intermediate April 2025 Mohsen Mashkour
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.