Retrofit Networking and API - Complete Guide
Table of Contents
Android Networking Fundamentals
Networking in Android refers to the process of sending and receiving data over the internet or a local network.
Common Use Cases:
- Fetching data from a server (weather, news, user profiles)
- Sending data to a server (login credentials, form submissions)
- Downloading/uploading files or images
- Communicating with REST APIs or GraphQL APIs
Popular Networking Libraries:
Library | Description | Use Case |
---|---|---|
HttpURLConnection | Low-level API, part of Android SDK | Basic HTTP requests |
OkHttp | Popular third-party HTTP client | Advanced HTTP features |
Retrofit | Type-safe HTTP client (built on OkHttp) | REST API integration |
Volley | Google's networking library | Image loading, caching |
What is an API?
API stands for Application Programming Interface. In Android, it usually refers to a Web API that your app communicates with over the internet.
Example API Flow:
User Action → Network Request → API Endpoint → Server Response → UI Update
API Communication Components:
- API URLs (endpoints): https://api.openweathermap.org/data/2.5/weather?q=London
- Data format: Usually JSON (sometimes XML)
- HTTP Methods: GET, POST, PUT, DELETE, PATCH
How Networking and APIs Work Together:
- User opens your app
- App makes a network request to a remote API endpoint
- Server sends back a response (usually JSON)
- App parses the JSON and displays it on the UI
Introduction to Retrofit
Retrofit is a type-safe HTTP client for Android and Java developed by Square. It simplifies REST API interactions by providing a clean, type-safe way to define API endpoints and handle responses.
Key Components:
1. Base URL
Retrofit.Builder() .baseUrl("https://api.example.com/")
2. API Interface (Endpoints)
interface ApiService { @GET("users") suspend fun getUsers(): List<User> @POST("users") suspend fun createUser(@Body user: User): User }
3. Data Models
data class User( val id: Int, val name: String, val email: String, val phone: String? = null )
4. Converter Factory
.addConverterFactory(GsonConverterFactory.create()) // or .addConverterFactory(MoshiConverterFactory.create())
Retrofit Implementation Guide
Step 1: Add Dependencies
// build.gradle.kts (Module: app) dependencies { // Retrofit implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") // OkHttp (for logging) implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") // Coroutines implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") }
Step 2: Create Data Models
// data/model/User.kt data class User( val id: Int, val name: String, val username: String, val email: String, val phone: String, val website: String, val address: Address ) data class Address( val street: String, val suite: String, val city: String, val zipcode: String, val geo: Geo ) data class Geo( val lat: String, val lng: String )
Step 3: Create API Interface
// data/api/ApiService.kt interface ApiService { // GET request - Fetch all users @GET("users") suspend fun getUsers(): Response<List<User>> // GET request with path parameter @GET("users/{id}") suspend fun getUserById(@Path("id") userId: Int): Response<User> // GET request with query parameters @GET("posts") suspend fun getPostsByUser( @Query("userId") userId: Int, @Query("_limit") limit: Int = 10 ): Response<List<Post>> // POST request - Create new user @POST("users") suspend fun createUser(@Body user: User): Response<User> // PUT request - Update user @PUT("users/{id}") suspend fun updateUser( @Path("id") userId: Int, @Body user: User ): Response<User> // DELETE request @DELETE("users/{id}") suspend fun deleteUser(@Path("id") userId: Int): Response<Unit> // POST with form data @FormUrlEncoded @POST("login") suspend fun login( @Field("username") username: String, @Field("password") password: String ): Response<LoginResponse> // File upload @Multipart @POST("upload") suspend fun uploadImage( @Part("description") description: RequestBody, @Part image: MultipartBody.Part ): Response<UploadResponse> }
Step 4: Create Retrofit Client
// data/api/RetrofitClient.kt object RetrofitClient { private const val BASE_URL = "https://jsonplaceholder.typicode.com/" // Logging interceptor for debugging private val loggingInterceptor = HttpLoggingInterceptor().apply { level = if (BuildConfig.DEBUG) { HttpLoggingInterceptor.Level.BODY } else { HttpLoggingInterceptor.Level.NONE } } // OkHttp client with interceptors private val okHttpClient = OkHttpClient.Builder() .addInterceptor(loggingInterceptor) .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .build() // Retrofit instance private val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .build() // API service val apiService: ApiService by lazy { retrofit.create(ApiService::class.java) } }
Step 5: Create Repository
// data/repository/UserRepository.kt class UserRepository { private val apiService = RetrofitClient.apiService suspend fun getUsers(): Result<List<User>> { return try { val response = apiService.getUsers() if (response.isSuccessful) { Result.success(response.body() ?: emptyList()) } else { Result.failure(Exception("Error: ${response.code()} ${response.message()}")) } } catch (e: Exception) { Result.failure(e) } } suspend fun getUserById(id: Int): Result<User> { return try { val response = apiService.getUserById(id) if (response.isSuccessful) { response.body()?.let { Result.success(it) } ?: Result.failure(Exception("User not found")) } else { Result.failure(Exception("Error: ${response.code()}")) } } catch (e: Exception) { Result.failure(e) } } suspend fun createUser(user: User): Result<User> { return try { val response = apiService.createUser(user) if (response.isSuccessful) { Result.success(response.body()!!) } else { Result.failure(Exception("Failed to create user")) } } catch (e: Exception) { Result.failure(e) } } }
Step 6: Use in ViewModel
// ui/main/UserViewModel.kt class UserViewModel : ViewModel() { private val repository = UserRepository() private val _users = MutableLiveData<List<User>>() val users: LiveData<List<User>> = _users private val _loading = MutableLiveData<Boolean>() val loading: LiveData<Boolean> = _loading private val _error = MutableLiveData<String?>() val error: LiveData<String?> = _error fun fetchUsers() { viewModelScope.launch { _loading.value = true repository.getUsers().fold( onSuccess = { userList -> _users.value = userList _error.value = null }, onFailure = { exception -> _error.value = exception.message } ) _loading.value = false } } fun createUser(user: User) { viewModelScope.launch { _loading.value = true repository.createUser(user).fold( onSuccess = { newUser -> // Add to current list val currentList = _users.value?.toMutableList() ?: mutableListOf() currentList.add(newUser) _users.value = currentList }, onFailure = { exception -> _error.value = exception.message } ) _loading.value = false } } }
Step 7: Use in Activity/Fragment
// ui/main/MainActivity.kt class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var viewModel: UserViewModel private lateinit var userAdapter: UserAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) viewModel = ViewModelProvider(this)[UserViewModel::class.java] setupRecyclerView() observeViewModel() viewModel.fetchUsers() } private fun setupRecyclerView() { userAdapter = UserAdapter { user -> // Handle user click Toast.makeText(this, "Clicked: ${user.name}", Toast.LENGTH_SHORT).show() } binding.recyclerView.adapter = userAdapter } private fun observeViewModel() { viewModel.users.observe(this) { users -> userAdapter.submitList(users) } viewModel.loading.observe(this) { isLoading -> binding.progressBar.isVisible = isLoading } viewModel.error.observe(this) { error -> error?.let { Toast.makeText(this, it, Toast.LENGTH_LONG).show() } } } }
Advanced Examples
Authentication with Interceptor
class AuthInterceptor(private val tokenProvider: () -> String?) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() val token = tokenProvider() val newRequest = if (token != null) { originalRequest.newBuilder() .header("Authorization", "Bearer $token") .build() } else { originalRequest } return chain.proceed(newRequest) } }
Generic API Response Wrapper
data class ApiResponse<T>( val success: Boolean, val message: String?, val data: T? ) interface ApiService { @GET("users") suspend fun getUsers(): Response<ApiResponse<List<User>>> }
Custom Error Handling
sealed class NetworkResult<T> { data class Success<T>(val data: T) : NetworkResult<T>() data class Error<T>(val message: String, val code: Int? = null) : NetworkResult<T>() data class Loading<T>(val isLoading: Boolean = true) : NetworkResult<T>() } suspend fun <T> safeApiCall(apiCall: suspend () -> Response<T>): NetworkResult<T> { return try { val response = apiCall() if (response.isSuccessful) { response.body()?.let { NetworkResult.Success(it) } ?: NetworkResult.Error("Empty response body") } else { NetworkResult.Error( message = response.message(), code = response.code() ) } } catch (e: Exception) { NetworkResult.Error(e.message ?: "Unknown error occurred") } }
Moshi vs Gson
Key Differences
Feature | Moshi | Gson |
---|---|---|
Library by | Square | |
Kotlin Support | Excellent (native support) | Limited, needs extra adapters |
Performance | Faster and more memory-efficient | Slower in some cases |
Null Safety | Native support for Kotlin nullability | Can silently ignore nulls |
Default Values | Respects Kotlin default parameters | Ignores default values |
When to Use What?
Use Case | Recommended Library |
---|---|
Pure Kotlin project | Moshi |
Legacy Java project | Gson |
Need fast parsing and Kotlin features | Moshi |
Already using Gson successfully | Stick with Gson |
Gson Limitations in Kotlin
data class User( val id: Int, val name: String = "Unknown", // Default value val email: String ) val json = """{ "id": 1, "email": "a@b.com" }""" val user = gson.fromJson(json, User::class.java) // Gson won't use the default value "Unknown"
Moshi Setup
// Dependencies implementation("com.squareup.moshi:moshi:1.15.0") implementation("com.squareup.moshi:moshi-kotlin:1.15.0") implementation("com.squareup.retrofit2:converter-moshi:2.9.0") // Retrofit setup val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build() val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(MoshiConverterFactory.create(moshi)) .build()
CRUD Operations
CRUD represents the four basic operations you can perform on data:
Operation | Description | HTTP Method | Example Endpoint |
---|---|---|---|
Create | Add new data | POST | /users |
Read | Retrieve data | GET | /users or /users/1 |
Update | Modify existing data | PUT/PATCH | /users/1 |
Delete | Remove data | DELETE | /users/1 |
Complete CRUD Implementation
interface ApiService { // CREATE @POST("users") suspend fun createUser(@Body user: CreateUserRequest): Response<User> // READ @GET("users") suspend fun getAllUsers(): Response<List<User>> @GET("users/{id}") suspend fun getUserById(@Path("id") id: Int): Response<User> // UPDATE (full update) @PUT("users/{id}") suspend fun updateUser(@Path("id") id: Int, @Body user: User): Response<User> // UPDATE (partial update) @PATCH("users/{id}") suspend fun partialUpdateUser(@Path("id") id: Int, @Body updates: Map<String, Any>): Response<User> // DELETE @DELETE("users/{id}") suspend fun deleteUser(@Path("id") id: Int): Response<Unit> }
Best Practices
1. Project Structure
π¦com.example.myapp β βββ πdata # Data layer β βββ πapi # Retrofit interfaces & clients β β βββ ApiService.kt β β βββ RetrofitClient.kt β β β βββ πmodel # Data models β β βββ User.kt β β βββ Post.kt β β β βββ πrepository # Repository pattern β βββ UserRepository.kt β βββ πdomain # Business logic (optional) β βββ πusecase β βββ GetUsersUseCase.kt β βββ πui # Presentation layer β βββ πmain β βββ MainActivity.kt β βββ UserViewModel.kt β βββ UserAdapter.kt β βββ πutils # Helper functions β βββ Constants.kt β βββ Extensions.kt β βββ MyApp.kt # Application class
2. Error Handling Best Practices
class ApiException( val code: Int, override val message: String, val errorBody: String? = null ) : Exception(message) suspend fun <T> handleApiCall(apiCall: suspend () -> Response<T>): Result<T> { return try { val response = apiCall() when { response.isSuccessful -> { response.body()?.let { Result.success(it) } ?: Result.failure(ApiException(response.code(), "Empty response")) } response.code() == 401 -> { Result.failure(ApiException(401, "Unauthorized")) } response.code() == 404 -> { Result.failure(ApiException(404, "Resource not found")) } response.code() >= 500 -> { Result.failure(ApiException(response.code(), "Server error")) } else -> { Result.failure(ApiException(response.code(), response.message())) } } } catch (e: IOException) { Result.failure(Exception("Network error: Check your connection")) } catch (e: Exception) { Result.failure(Exception("Unexpected error: ${e.message}")) } }
3. Caching Strategy
class UserRepository( private val apiService: ApiService, private val cacheManager: CacheManager ) { suspend fun getUsers(forceRefresh: Boolean = false): Result<List<User>> { return if (forceRefresh || cacheManager.isExpired("users")) { // Fetch from network handleApiCall { apiService.getUsers() }.also { result -> if (result.isSuccess) { result.getOrNull()?.let { users -> cacheManager.cache("users", users) } } } } else { // Return cached data Result.success(cacheManager.get<List<User>>("users") ?: emptyList()) } } }
Interview Questions & Answers
1. What is Retrofit?
Answer: Retrofit is a type-safe HTTP client for Android and Java developed by Square. It simplifies making REST API calls by abstracting away low-level networking code and automatically converting JSON responses into Kotlin/Java objects.
2. What are the key components of Retrofit?
Answer:
- Base URL: Root endpoint of the API
- Converter: Converts JSON to objects (Gson, Moshi)
- Interface: Defines endpoints with annotations (@GET, @POST, etc.)
- Model Classes: Represent the API response structure
- Retrofit Builder: Initializes Retrofit and binds everything together
3. What HTTP methods does Retrofit support?
Answer:
- @GET- Retrieve data
- @POST- Create new data
- @PUT- Update entire resource
- @PATCH- Update specific fields
- @DELETE- Remove data
- @HEAD- Get metadata (headers only), no body
4. What is a Converter in Retrofit?
Answer: A Converter serializes and deserializes HTTP request/response bodies. Examples:
- GsonConverterFactory - Converts JSON to/from Kotlin/Java objects
- MoshiConverterFactory - Lightweight alternative to Gson with better Kotlin support
5. How do you handle errors in Retrofit?
Answer: Multiple approaches:
- Try-Catch blocks with Coroutines
- Response.isSuccessful() checks
- Custom error handling with sealed classes
- Interceptors for global error handling
6. What is a REST API?
Answer: REST (Representational State Transfer) API is a way for different software systems to communicate over the internet using HTTP. It allows accessing or modifying data on a remote server using standard HTTP methods.
7. Explain @Query, @Path, @Body, and @Header annotations.
Answer:
- @Query- Appends query parameters (?key=value)
- @Path- Replaces URL path segments with values
- @Body- Sends data in request body (usually JSON)
- @Header- Adds custom headers to requests
8. Retrofit vs Volley vs OkHttp - What are the differences?
Answer:
- Retrofit: High-level HTTP client, easy REST API integration, type-safe
- OkHttp: Low-level HTTP client (used internally by Retrofit), more control
- Volley: Google's older networking library, good for image loading but less flexible
9. How do you implement authentication with Retrofit?
Answer: Use interceptors to add authentication headers:
class AuthInterceptor(private val token: String) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request().newBuilder() .addHeader("Authorization", "Bearer $token") .build() return chain.proceed(request) } }
10. What's the difference between PUT and PATCH?
Answer:
- PUT: Updates the entire resource (full replacement)
- PATCH: Updates only specific fields (partial update)
Conclusion
Retrofit is a powerful and flexible library that significantly simplifies network operations in Android applications. By following the patterns and best practices outlined in this guide, you can build robust, maintainable networking layers for your Android apps.
Key takeaways:
- Use proper error handling and response validation
- Implement repository pattern for data management
- Choose appropriate converters (Moshi for Kotlin, Gson for legacy)
- Structure your project logically with clear separation of concerns
- Implement proper authentication and security measures
This guide provides a comprehensive foundation for working with Retrofit, from basic implementation to advanced patterns and best practices.