Network Errors and Offline Scenarios


Android: Handling Network Errors and Offline Scenarios

In mobile applications, network requests are inherently unreliable due to varying network conditions, server issues, and user connectivity. A robust application must gracefully handle network errors and provide a good user experience even when offline.

1. Handling Network Errors

Network requests can fail for various reasons, including:

  • No internet connection.
  • Server is unreachable.
  • Server returns an error (e.g., 404 Not Found, 500 Internal Server Error).
  • Request timeout.
  • Data parsing errors.
  • DNS lookup failures.

Your application should detect and handle these errors appropriately.

a) Using Callbacks or Coroutine Exception Handling

Network libraries like Retrofit and Volley provide mechanisms to handle both successful responses and errors.

Retrofit with Callbacks (Java/Kotlin):

call.enqueue(object : Callback<WeatherResponse> {
    override fun onResponse(call: Call<WeatherResponse>, response: Response<WeatherResponse>) {
        if (response.isSuccessful) {
            // Handle successful HTTP response (status code 2xx)
            val weather = response.body()
            // ... update UI
        } else {
            // Handle HTTP errors (status codes 4xx, 5xx)
            val errorCode = response.code()
            val errorMessage = response.errorBody()?.string() // Get error body if available
            Log.e("NetworkError", "HTTP Error $errorCode: $errorMessage")
            // Show error message to user
        }
    }

    override fun onFailure(call: Call<WeatherResponse>, t: Throwable) {
        // Handle network errors (no connection, timeout, etc.)
        Log.e("NetworkError", "Network request failed", t)
        // Show network error message to user
    }
})

Retrofit with Coroutines (Kotlin):

suspend fun fetchWeatherData(city: String): WeatherResponse? {
    return withContext(Dispatchers.IO) {
        try {
            // This call will throw an exception on network or HTTP errors
            RetrofitClient.instance.getWeather(city, RetrofitClient.API_KEY)
        } catch (e: retrofit2.HttpException) {
            // Handle HTTP errors
            val errorCode = e.code()
            val errorMessage = e.response()?.errorBody()?.string()
            Log.e("NetworkError", "HTTP Error $errorCode: $errorMessage")
            // You might want to return null or a specific error object
            null
        } catch (e: java.io.IOException) {
            // Handle network errors (no connection, timeout, etc.)
            Log.e("NetworkError", "Network request failed", e)
            // Return null or a specific error object
            null
        } catch (e: Exception) {
            // Handle other potential exceptions (e.g., parsing errors if not handled by converter)
            Log.e("NetworkError", "An unexpected error occurred", e)
            // Return null or a specific error object
            null
        }
    }
}

// In your ViewModel/CoroutineScope:
/*
viewModelScope.launch {
    val weather = fetchWeatherData("London")
    if (weather != null) {
        // Update UI
    } else {
        // Show error message based on the error type (if you returned different error objects)
        // Or just show a generic "Failed to load data"
    }
}
*/

Volley:

val jsonObjectRequest = JsonObjectRequest(Request.Method.GET, url, null,
    { response ->
        // Handle successful response
    },
    { error ->
        // Handle error response
        if (error is com.android.volley.NoConnectionError || error is com.android.volley.TimeoutError) {
            // Handle no connection or timeout
            Log.e("VolleyError", "Network unavailable or timeout")
        } else if (error is com.android.volley.ServerError) {
            // Handle server error (status code 5xx)
            val statusCode = error.networkResponse.statusCode
            Log.e("VolleyError", "Server Error: $statusCode")
        } else if (error is com.android.volley.ClientError) {
            // Handle client error (status code 4xx)
            val statusCode = error.networkResponse.statusCode
            Log.e("VolleyError", "Client Error: $statusCode")
        } else if (error is com.android.volley.ParseError) {
            // Handle parsing error
            Log.e("VolleyError", "Parsing Error")
        } else {
            // Handle other Volley errors
            Log.e("VolleyError", "Unknown Volley error", error)
        }
        // Show error message to user
    }
)

b) Checking Network Connectivity

Before making a network request, it's good practice to check if the device has an active network connection. This prevents unnecessary attempts and provides immediate feedback to the user.

import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities

fun isNetworkAvailable(context: Context): Boolean {
    val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val network = connectivityManager.activeNetwork ?: return false
    val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
    return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
           capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
           capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
}

// Before making a network call:
/*
if (isNetworkAvailable(context)) {
    // Make network request
} else {
    // Show "No internet connection" message
}
*/

Note: Checking connectivity only tells you if a network interface is active. It doesn't guarantee that the internet is actually reachable or that the server is online. You still need robust error handling for the network request itself.

c) User Feedback

Inform the user when a network request fails. This could be a simple Toast message, a Snackbar, or a dedicated error view on the screen.

  • "Failed to load data. Please check your internet connection." (Generic network error)
  • "Could not find the requested item." (404 Not Found)
  • "Something went wrong on the server. Please try again later." (5xx Server Error)

d) Retry Mechanisms

For transient network errors (like temporary network glitches or server overload), you can implement a retry mechanism. Some libraries (like Volley) have built-in retry policies. For Retrofit/Coroutines, you can implement it manually or use libraries like Flow's retryWhen.

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.retryWhen
import android.util.Log

// Example using Kotlin Flow for retrying a network operation
fun <T> Flow<T>.retryNetworkCalls(
    times: Int = 3,
    initialDelay: Long = 1000, // 1 second
    maxDelay: Long = 5000, // 5 seconds
    factor: Double = 2.0
): Flow<T> = retryWhen { cause, attempt ->
    if (cause is java.io.IOException && attempt < times) {
        val delayDuration = (initialDelay * factor.pow(attempt)).coerceAtMost(maxDelay)
        Log.d("Retry", "Retrying network request after ${delayDuration}ms (Attempt ${attempt + 1})")
        delay(delayDuration)
        true // Retry
    } else {
        false // Do not retry
    }
}

// Example usage with Retrofit/Coroutines and Flow
/*
suspend fun fetchWeatherFlow(city: String): Flow<WeatherResponse> = flow {
    val response = RetrofitClient.instance.getWeather(city, RetrofitClient.API_KEY)
    emit(response)
}.retryNetworkCalls(times = 3) // Retry up to 3 times

// In your ViewModel/CoroutineScope:
viewModelScope.launch {
    fetchWeatherFlow("London")
        .catch { e ->
            // Handle final error after retries
            Log.e("NetworkError", "Failed to fetch weather after retries", e)
            // Show error message
        }
        .collect { weather ->
            // Handle successful response
            Log.d("Weather", "Temperature: ${weather.main.temp}")
        }
}
*/

2. Handling Offline Scenarios

Users expect applications to function reasonably well even when offline. This involves providing access to previously loaded data.

a) Data Caching

Store data fetched from the network locally on the device. This allows the app to display data immediately upon launch, even without a network connection.

  • Database (e.g., Room): Excellent for structured data. You can load cached data from the database when offline and update it from the network when online.
  • SharedPreferences: Suitable for small amounts of key-value data.
  • Internal/External Storage: For larger files or data that doesn't fit well in a database.
  • HTTP Caching: Retrofit and OkHttp (its underlying HTTP client) support standard HTTP caching headers. Volley also has built-in caching.

Example using Room for caching:

  1. Define Room entities for your data models.
  2. Create DAO (Data Access Object) methods for inserting, retrieving, and updating data.
  3. In your data layer (e.g., Repository):
    • When online, fetch data from the network.
    • Store the fetched data in the database.
    • Return the data from the database.
    • When offline, fetch data directly from the database.

This is often implemented using a "source of truth" pattern, where the database is the primary source of data, and the network is used to update the database.

Example (Conceptual Repository):

class WeatherRepository(
    private val apiService: ApiService, // Retrofit service
    private val weatherDao: WeatherDao // Room DAO
) {
    suspend fun getWeather(city: String): Flow<Weather?> = flow {
        // First, emit cached data if available
        val cachedWeather = weatherDao.getWeather(city)
        if (cachedWeather != null) {
            emit(cachedWeather)
        }

        // Then, try to fetch from network
        try {
            val networkWeather = apiService.getWeather(city, RetrofitClient.API_KEY)
            // Convert network response to database entity if needed
            val weatherEntity = WeatherEntity(city, networkWeather.name, networkWeather.main.temp, ...)
            weatherDao.insertWeather(weatherEntity)
            // Emit the newly cached data
            emit(weatherEntity.toWeather()) // Convert entity back to domain model
        } catch (e: Exception) {
            // Handle network errors. If cached data was emitted, this error won't
            // leave the user with a blank screen. If no cached data, handle error.
            if (cachedWeather == null) {
                 // Re-throw or emit an error state if no cache was available
                 throw e
            } else {
                 Log.w("Repository", "Failed to update weather from network", e)
                 // Optionally emit a state indicating data is stale or update failed
            }
        }
    }
}

b) Offline UI States

Design your UI to clearly indicate when the app is offline and potentially show stale data or relevant messages.

  • Display cached data with a timestamp indicating when it was last updated.
  • Show a banner or message indicating "Offline mode" or "No internet connection."
  • Disable functionality that requires an active network connection (e.g., submitting new data).

c) Background Sync (WorkManager)

For tasks that need to run even when the app is not active or the device is offline, use WorkManager. WorkManager can queue network requests and execute them when the network becomes available and other constraints are met.

Summary of Best Practices:

  • Check Connectivity: Perform a basic network check before initiating requests.
  • Implement Robust Error Handling: Catch specific network and HTTP errors.
  • Provide User Feedback: Clearly communicate network status and errors to the user.
  • Implement Retries: Use retry mechanisms for transient errors.
  • Cache Data: Store fetched data locally for offline access.
  • Design for Offline: Create UI states that work gracefully without a connection.
  • Use WorkManager: Handle background network tasks reliably.

By implementing these strategies, you can create Android applications that are resilient to network fluctuations and provide a smoother experience for users, regardless of their connectivity status.