Back to Course
Networking

Networking in Android — Making API Calls

In the modern mobile landscape, almost every app communicates with a backend server. Whether you're fetching user data, uploading images, or syncing content, understanding networking in Android is essential. This guide covers everything from making HTTP requests to parsing JSON responses using Retrofit — the industry-standard networking library for Android.

Production Reality: Networking is one of the most critical parts of any modern app. Poorly implemented networking leads to app crashes, slow performance, and frustrated users. Mastering Retrofit and handling errors gracefully is essential for building professional Android apps.

1. Networking Libraries — Why Retrofit?

There are several networking libraries available for Android:

LibraryDescriptionProsCons
RetrofitType-safe HTTP client for Android✅ Type-safe, ✅ Coroutine support, ✅ Interceptors, ✅ Industry standardRequires setup
VolleyGoogle's networking library✅ Simple, ✅ Built-in caching❌ Older, ❌ Less flexible
OkHttpLow-level HTTP client✅ High performance, ✅ WebSocket support❌ More manual work
KtorKotlin-based networking✅ Kotlin-first, ✅ Cross-platform❌ Newer, ❌ Smaller community
Pro Tip: Retrofit is the most widely used networking library in Android development. It's type-safe, works seamlessly with Kotlin coroutines, and is used by Google in many of their apps. Master Retrofit, and you'll be able to build any networking feature in Android.

2. Setting Up Retrofit

Step 1: Add Dependencies

Add the following to your build.gradle (Module) file:

dependencies {
    // Retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    
    // Converter (Gson for JSON parsing)
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    
    // Logging Interceptor (for debugging)
    implementation 'com.squareup.okhttp3:logging-interceptor:4.11.0'
    
    // OkHttp (already included with Retrofit)
    implementation 'com.squareup.okhttp3:okhttp:4.11.0'
}

Step 2: Add Internet Permission

Add this to your AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

3. Creating the Data Models

Create Kotlin data classes that match the JSON response structure from your API.

Example API Response

// Sample JSON response from a user API
{
    "id": 1,
    "name": "John Doe",
    "email": "john@example.com",
    "username": "johndoe",
    "phone": "+1-555-123-4567",
    "website": "johndoe.com",
    "address": {
        "street": "123 Main St",
        "city": "New York",
        "zipcode": "10001"
    }
}

Kotlin Data Classes

// User.kt
data class User(
    val id: Int,
    val name: String,
    val email: String,
    val username: String,
    val phone: String,
    val website: String,
    val address: Address
)

// Address.kt (nested object)
data class Address(
    val street: String,
    val city: String,
    val zipcode: String
)

// ApiResponse.kt (for responses with metadata)
data class ApiResponse<T>(
    val status: String,
    val message: String?,
    val data: T?
)

// ErrorResponse.kt (for error handling)
data class ErrorResponse(
    val statusCode: Int,
    val message: String,
    val timestamp: String
)

4. Creating the API Interface

The API interface defines all endpoints using Retrofit annotations.

// ApiService.kt
import retrofit2.Call
import retrofit2.http.*
import retrofit2.Response
import kotlinx.coroutines.flow.Flow

interface ApiService {
    
    // GET — Fetch all users
    @GET("users")
    suspend fun getUsers(): Response<List<User>>
    
    // GET — Fetch user by ID
    @GET("users/{id}")
    suspend fun getUserById(@Path("id") userId: Int): Response<User>
    
    // GET — With query parameters
    @GET("users")
    suspend fun searchUsers(
        @Query("name") name: String,
        @Query("limit") limit: Int = 10
    ): Response<List<User>>
    
    // POST — Create a new user
    @POST("users")
    suspend fun createUser(@Body user: User): Response<User>
    
    // PUT — Update an existing user
    @PUT("users/{id}")
    suspend fun updateUser(
        @Path("id") userId: Int,
        @Body user: User
    ): Response<User>
    
    // PATCH — Partial update
    @PATCH("users/{id}")
    suspend fun patchUser(
        @Path("id") userId: Int,
        @Body user: User
    ): Response<User>
    
    // DELETE — Delete a user
    @DELETE("users/{id}")
    suspend fun deleteUser(@Path("id") userId: Int): Response<Unit>
    
    // Upload a file (multipart)
    @Multipart
    @POST("upload")
    suspend fun uploadFile(
        @Part file: MultipartBody.Part
    ): Response<User>
    
    // Download a file
    @Streaming
    @GET("download/{fileName}")
    suspend fun downloadFile(
        @Path("fileName") fileName: String
    ): Response<ResponseBody>
}

5. Building the Retrofit Client

// ApiClient.kt
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit

object ApiClient {
    
    private const val BASE_URL = "https://jsonplaceholder.typicode.com/"
    
    // Logging interceptor (for development)
    private val loggingInterceptor = HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY // Shows request/response body
    }
    
    // OkHttp Client with interceptors
    private val okHttpClient = OkHttpClient.Builder()
        .addInterceptor(loggingInterceptor)
        .addInterceptor { chain ->
            val original = chain.request()
            val request = original.newBuilder()
                .header("Authorization", "Bearer YOUR_TOKEN_HERE")
                .header("Accept", "application/json")
                .header("Content-Type", "application/json")
                .method(original.method, original.body)
                .build()
            chain.proceed(request)
        }
        .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 instance
    val apiService: ApiService = retrofit.create(ApiService::class.java)
}

6. Making API Calls in ViewModel

// UserViewModel.kt
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

sealed class UserState {
    object Loading : UserState()
    data class Success(val users: List<User>) : UserState()
    data class Error(val message: String) : UserState()
}

class UserViewModel : ViewModel() {
    
    private val _uiState = MutableLiveData<UserState>(UserState.Loading)
    val uiState: LiveData<UserState> = _uiState
    
    private val _users = MutableLiveData<List<User>>()
    val users: LiveData<List<User>> = _users
    
    private val _isLoading = MutableLiveData<Boolean>(false)
    val isLoading: LiveData<Boolean> = _isLoading
    
    private val _error = MutableLiveData<String?>(null)
    val error: LiveData<String?> = _error
    
    // Fetch all users
    fun fetchUsers() {
        _isLoading.value = true
        _error.value = null
        
        viewModelScope.launch(Dispatchers.IO) {
            try {
                val response = ApiClient.apiService.getUsers()
                withContext(Dispatchers.Main) {
                    _isLoading.value = false
                    if (response.isSuccessful) {
                        val userList = response.body()
                        if (userList != null) {
                            _users.value = userList
                            _uiState.value = UserState.Success(userList)
                        } else {
                            _error.value = "No data received"
                            _uiState.value = UserState.Error("No data received")
                        }
                    } else {
                        val errorMessage = "Error: ${response.code()} - ${response.message()}"
                        _error.value = errorMessage
                        _uiState.value = UserState.Error(errorMessage)
                    }
                }
            } catch (e: Exception) {
                withContext(Dispatchers.Main) {
                    _isLoading.value = false
                    val errorMessage = "Network error: ${e.message}"
                    _error.value = errorMessage
                    _uiState.value = UserState.Error(errorMessage)
                }
            }
        }
    }
    
    // Create a new user
    fun createUser(user: User) {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                val response = ApiClient.apiService.createUser(user)
                withContext(Dispatchers.Main) {
                    if (response.isSuccessful) {
                        val newUser = response.body()
                        // Add to existing list or refresh
                        _users.value = _users.value?.plus(newUser) ?: listOf(newUser)
                    } else {
                        _error.value = "Failed to create user: ${response.code()}"
                    }
                }
            } catch (e: Exception) {
                withContext(Dispatchers.Main) {
                    _error.value = "Network error: ${e.message}"
                }
            }
        }
    }
    
    // Delete a user
    fun deleteUser(userId: Int) {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                val response = ApiClient.apiService.deleteUser(userId)
                withContext(Dispatchers.Main) {
                    if (response.isSuccessful) {
                        _users.value = _users.value?.filter { it.id != userId }
                    } else {
                        _error.value = "Failed to delete user: ${response.code()}"
                    }
                }
            } catch (e: Exception) {
                withContext(Dispatchers.Main) {
                    _error.value = "Network error: ${e.message}"
                }
            }
        }
    }
}

7. Using in Activity/Fragment

// MainActivity.kt
class MainActivity : AppCompatActivity() {
    
    private lateinit var viewModel: UserViewModel
    private lateinit var adapter: UserAdapter
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // Initialize ViewModel
        viewModel = ViewModelProvider(this).get(UserViewModel::class.java)
        
        // Setup RecyclerView
        setupRecyclerView()
        
        // Observe users
        viewModel.users.observe(this) { users ->
            adapter.submitList(users)
        }
        
        // Observe loading state
        viewModel.isLoading.observe(this) { isLoading ->
            // Show/hide progress bar
            findViewById<ProgressBar>(R.id.progressBar).visibility = 
                if (isLoading) View.VISIBLE else View.GONE
        }
        
        // Observe errors
        viewModel.error.observe(this) { error ->
            error?.let {
                Toast.makeText(this, it, Toast.LENGTH_LONG).show()
            }
        }
        
        // Observe UI state
        viewModel.uiState.observe(this) { state ->
            when (state) {
                is UserState.Loading -> {
                    // Show loading
                }
                is UserState.Success -> {
                    // Show data
                }
                is UserState.Error -> {
                    // Show error
                }
            }
        }
        
        // Fetch users on launch
        viewModel.fetchUsers()
    }
    
    private fun setupRecyclerView() {
        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
        adapter = UserAdapter { user ->
            // Handle item click
            viewModel.deleteUser(user.id)
        }
        recyclerView.adapter = adapter
        recyclerView.layoutManager = LinearLayoutManager(this)
    }
}

8. Error Handling — Best Practices

Sealed Class for Network State

// NetworkState.kt
sealed class NetworkState<out T> {
    object Loading : NetworkState<Nothing>()
    data class Success<T>(val data: T) : NetworkState<T>()
    data class Error(val message: String, val code: Int = -1) : NetworkState<Nothing>()
}

// Usage in ViewModel
fun fetchUsers(): Flow<NetworkState<List<User>>> = flow {
    emit(NetworkState.Loading)
    try {
        val response = ApiClient.apiService.getUsers()
        if (response.isSuccessful) {
            val users = response.body()
            if (users != null) {
                emit(NetworkState.Success(users))
            } else {
                emit(NetworkState.Error("Empty response"))
            }
        } else {
            emit(NetworkState.Error(
                message = response.message(),
                code = response.code()
            ))
        }
    } catch (e: Exception) {
        emit(NetworkState.Error(e.message ?: "Unknown error"))
    }
}

9. Interceptors — Advanced Use Cases

Authentication Interceptor

// AuthInterceptor.kt
class AuthInterceptor(private val tokenProvider: () -> String?) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        val token = tokenProvider()
        
        if (token.isNullOrEmpty()) {
            return chain.proceed(originalRequest)
        }
        
        val request = originalRequest.newBuilder()
            .header("Authorization", "Bearer $token")
            .build()
        
        return chain.proceed(request)
    }
}

Retry Interceptor

// RetryInterceptor.kt
class RetryInterceptor(private val maxRetries: Int = 3) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var retryCount = 0
        var response: Response
        
        do {
            response = chain.proceed(chain.request())
            retryCount++
        } while (!response.isSuccessful && retryCount < maxRetries)
        
        return response
    }
}

Adding Interceptors to OkHttp Client

val okHttpClient = OkHttpClient.Builder()
    .addInterceptor(AuthInterceptor { getAuthToken() })
    .addInterceptor(RetryInterceptor())
    .addInterceptor(loggingInterceptor)
    .build()

10. Handling Offline Scenarios

// ConnectivityManager.kt
class ConnectivityManager(private val context: Context) {
    fun isConnected(): Boolean {
        val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val networkCapabilities = connectivityManager.activeNetwork ?: return false
        val activeNetwork = connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false
        return activeNetwork.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    }
}

// Usage in ViewModel
fun fetchUsers() {
    viewModelScope.launch(Dispatchers.IO) {
        if (!connectivityManager.isConnected()) {
            _error.postValue("No internet connection")
            _isLoading.postValue(false)
            return@launch
        }
        // Proceed with network call
    }
}

11. Common Pitfalls & Solutions

Common Mistakes to Avoid:

Pitfall 1: Not handling network errors

Problem: Network calls can fail for many reasons — no internet, server errors, timeouts.

Solution: Always wrap network calls in try-catch blocks and handle all error cases gracefully.

Pitfall 2: Blocking the main thread

Problem: Running network calls on the main thread causes the app to freeze (ANR).

Solution: Use coroutines with Dispatchers.IO or viewModelScope.

Pitfall 3: Not using the correct JSON converter

Problem: Retrofit needs a converter to parse JSON. Without it, calls will fail.

Solution: Add addConverterFactory(GsonConverterFactory.create()) when building Retrofit.

Pitfall 4: Not handling token expiration

Problem: If your auth token expires, API calls will fail with 401 Unauthorized.

Solution: Use an interceptor to check for 401 responses and refresh the token automatically.

12. Production-Ready Checklist

  • ✅ Use Retrofit — Industry-standard networking library.
  • ✅ Use coroutines — With Dispatchers.IO for network calls.
  • ✅ Handle errors — Try-catch, HTTP status codes, network errors.
  • ✅ Add interceptors — For auth, logging, retries.
  • ✅ Check connectivity — Before making network calls.
  • ✅ Use sealed classes — For network state (Loading, Success, Error).
  • ✅ Configure timeouts — Connect, read, and write timeouts.
  • ✅ Use HTTPS — Always use secure connections in production.
  • ✅ Cache responses — Use OkHttp cache for offline support.
  • ✅ Log requests — Only in debug builds (not in production).
  • ✅ Use live data / flow — For observable data in ViewModel.
  • ✅ Test API calls — Use MockWebServer for unit tests.
Key Takeaway: Networking is essential for any modern Android app. Retrofit is the industry-standard library that makes API calls simple, type-safe, and reliable. Always use coroutines for background threads, handle errors properly, and use interceptors for authentication and logging. With proper error handling and connectivity checks, you can build apps that work seamlessly even in challenging network conditions.

Ready to master Android Development?

Build real-world Android apps with hands-on projects, mentor-led sessions, and placement support.

Explore Course