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:

  1. User opens your app
  2. App makes a network request to a remote API endpoint
  3. Server sends back a response (usually JSON)
  4. 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 Google
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.