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.
1. Networking Libraries — Why Retrofit?
There are several networking libraries available for Android:
| Library | Description | Pros | Cons |
|---|---|---|---|
| Retrofit | Type-safe HTTP client for Android | ✅ Type-safe, ✅ Coroutine support, ✅ Interceptors, ✅ Industry standard | Requires setup |
| Volley | Google's networking library | ✅ Simple, ✅ Built-in caching | ❌ Older, ❌ Less flexible |
| OkHttp | Low-level HTTP client | ✅ High performance, ✅ WebSocket support | ❌ More manual work |
| Ktor | Kotlin-based networking | ✅ Kotlin-first, ✅ Cross-platform | ❌ Newer, ❌ Smaller community |
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
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.IOfor 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.
Ready to master Android Development?
Build real-world Android apps with hands-on projects, mentor-led sessions, and placement support.
.png)