Handling Network Requests in the Background


Android: Handling Network Requests in the Background

Performing network requests is a common task in mobile development, but it's crucial to do them on a background thread to avoid blocking the UI thread and causing ANRs. This section covers recommended ways to handle network requests in the background and update the UI safely.

The Problem: Network on Main Thread Exception

Since API level 11 (Honeycomb), Android throws a NetworkOnMainThreadException if you attempt to perform a network operation directly on the UI thread. This is a deliberate measure to prevent developers from creating unresponsive applications.

Recommended Approaches for Background Network Requests

1. Using Kotlin Coroutines (Recommended)

Coroutines, especially with libraries like Retrofit and kotlinx-coroutines-android, provide a clean and efficient way to handle network requests.

Setup:

  • Add necessary dependencies for Coroutines and Retrofit (with a Coroutines adapter).
// build.gradle (app level)
dependencies {
    // Kotlin Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") // Use the latest version
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1") // Use the latest version

    // Retrofit (for network requests)
    implementation("com.squareup.retrofit2:retrofit:2.9.0") // Use the latest version
    implementation("com.squareup.retrofit2:converter-gson:2.9.0") // For JSON parsing
    implementation("com.squareup.retrofit2:converter-scalars:2.9.0") // Optional: for String responses

    // Retrofit Coroutines adapter
    implementation("com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2") // Or use Retrofit's built-in support if using Retrofit 2.6.0+
}

Note: Retrofit 2.6.0 and higher have built-in support for suspend functions, making the separate adapter unnecessary in many cases. Check the Retrofit documentation for the latest usage.

Example with Retrofit and Coroutines:

Define your API service interface using suspend functions:

import retrofit2.Response
import retrofit2.http.GET

interface ApiService {
    @GET("users") // Example endpoint
    suspend fun getUsers(): List<User> // Suspend function
}

data class User(val id: Int, val name: String, val email: String) // Example data class

Perform the network request within a Coroutine scope:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class UserViewModel : ViewModel() {

    private val apiService: ApiService = RetrofitClient.instance.create(ApiService::class.java)

    fun loadUsers() {
        viewModelScope.launch {
            try {
                // Switch to the IO dispatcher for the network call
                val users = withContext(Dispatchers.IO) {
                    apiService.getUsers() // Call the suspend function
                }
                // Automatically back on the Main dispatcher after withContext
                // Update UI with users (e.g., update LiveData)
                _users.value = users
            } catch (e: Exception) {
                // Handle network errors (e.g., update LiveData with error state)
                _error.value = "Failed to load users: ${e.message}"
            }
        }
    }
}

// Example RetrofitClient (adjust base URL as needed)
object RetrofitClient {
    private const val BASE_URL = "https://api.example.com/" // Replace with your API base URL

    val instance: Retrofit by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            // If using older Retrofit & adapter: addCallAdapterFactory(CoroutineCallAdapterFactory())
            .build()
    }
}

Benefits of Coroutines for Network:

  • Simplifies asynchronous code flow.
  • Easy error handling with try-catch.
  • Seamless integration with ViewModel and Lifecycle for automatic cancellation when the UI is destroyed.
  • Efficient resource usage compared to managing raw threads.

2. Using Libraries like Retrofit with Callbacks (Java/Kotlin)

Retrofit is a popular library for making type-safe HTTP requests. It can be used with callbacks to handle responses asynchronously on a background thread and then deliver the result back to the UI thread.

Setup:

  • Add necessary dependencies for Retrofit and a converter (e.g., Gson).
// build.gradle (app level)
dependencies {
    implementation("com.squareup.retrofit2:retrofit:2.9.0") // Use the latest version
    implementation("com.squareup.retrofit2:converter-gson:2.9.0") // For JSON parsing
}

Example with Retrofit and Callbacks:

Define your API service interface:

import retrofit2.Call
import retrofit2.http.GET

interface ApiService {
    @GET("users") // Example endpoint
    fun getUsers(): Call<List<User>> // Returns a Call object
}

Make the network request asynchronously using enqueue():

import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import android.widget.TextView
import android.util.Log

fun fetchUsersWithCallback(dataTextView: TextView) {
    val call = RetrofitClient.instance.create(ApiService::class.java).getUsers()

    call.enqueue(object : Callback<List<User>> {
        override fun onResponse(call: Call<List<User>>, response: Response<List<User>>) {
            // This callback is executed on the UI thread by default by Retrofit
            if (response.isSuccessful) {
                val users = response.body()
                if (users != null) {
                    val userNames = users.joinToString("\n") { it.name }
                    dataTextView.text = userNames
                } else {
                    dataTextView.text = "No users found"
                }
            } else {
                // Handle HTTP errors (e.g., 404, 500)
                dataTextView.text = "Error: ${response.code()}"
                Log.e("Network", "HTTP Error: ${response.code()}")
            }
        }

        override fun onFailure(call: Call<List<User>>, t: Throwable) {
            // This callback is executed on the UI thread by default by Retrofit
            // Handle network errors (e.g., no internet connection)
            dataTextView.text = "Network request failed: ${t.message}"
            Log.e("Network", "Request failed", t)
        }
    })
}

Benefits of Retrofit with Callbacks:

  • Abstracts away the low-level details of network connections.
  • Automatically handles background threading for the request and delivers results back to the UI thread.
  • Easy to parse responses using converters (like Gson).

Drawbacks of Retrofit with Callbacks:

  • Can lead to "callback hell" for chained or complex requests.
  • Manual cancellation can be more involved compared to Coroutines.

3. Using Android Async HTTP Libraries (e.g., AsyncHttpClient - Older)

Older libraries like AsyncHttpClient provided similar functionality to Retrofit but are less commonly used in modern Android development compared to Retrofit or Coroutines.

Avoid using these older libraries in new code.

4. Using Raw Java Threads and Handlers (More Manual)

You can manually create a background thread to perform the network request and then use a Handler to post the result back to the UI thread.

Example (Illustrative - generally prefer Coroutines or Retrofit):

import android.os.Handler
import android.os.Looper
import android.widget.TextView
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL

fun fetchUrlWithThreadAndHandler(dataTextView: TextView) {
    val uiHandler = Handler(Looper.getMainLooper())

    Thread {
        var result: String? = null
        var error: String? = null
        try {
            val url = URL("https://api.example.com/data") // Replace with your URL
            val connection = url.openConnection() as HttpURLConnection
            connection.requestMethod = "GET"
            connection.connectTimeout = 5000 // 5 seconds timeout
            connection.readTimeout = 5000 // 5 seconds timeout

            if (connection.responseCode == HttpURLConnection.HTTP_OK) {
                val reader = BufferedReader(InputStreamReader(connection.inputStream))
                val stringBuilder = StringBuilder()
                var line: String?
                while (reader.readLine().also { line = it } != null) {
                    stringBuilder.append(line)
                }
                result = stringBuilder.toString()
                reader.close()
            } else {
                error = "HTTP error: ${connection.responseCode}"
            }
            connection.disconnect()
        } catch (e: Exception) {
            error = "Network request failed: ${e.message}"
            e.printStackTrace()
        }

        // Post the result back to the UI thread
        uiHandler.post {
            if (result != null) {
                dataTextView.text = result
            } else if (error != null) {
                dataTextView.text = error
            } else {
                dataTextView.text = "Request completed with no data/error"
            }
        }
    }.start()
}

Drawbacks of Raw Threads/Handlers:

  • More verbose and requires manual thread management.
  • More complex to handle cancellation and lifecycle awareness.
  • Error handling can be less structured.

5. Using WorkManager (For Guaranteed Background Work)

WorkManager is an Android Architecture Component for managing deferrable, guaranteed background work. It's suitable for tasks that need to run reliably, even if the app exits or the device restarts (e.g., syncing data, uploading logs).

WorkManager is not typically used for immediate network requests triggered by user interaction where you need to update the UI right away. It's for background tasks that don't require the app to be in the foreground.

When to use WorkManager for Network:

  • Fetching data periodically (e.g., checking for updates).
  • Uploading data in the background.
  • Syncing data when network conditions are met.

Summary and Best Practices

  • Always perform network requests on a background thread.
  • Use Kotlin Coroutines with Retrofit for modern Android development. It's the most recommended approach for its simplicity, readability, and lifecycle awareness.
  • If working with an existing Java codebase or prefer callbacks, use Retrofit with Callbacks.
  • Avoid using raw Java Threads and Handlers for network requests unless you have very specific needs that libraries don't cover.
  • Use WorkManager for network tasks that need to be guaranteed to run in the background, independent of the app's foreground state.
  • Handle network errors gracefully (e.g., display an error message to the user).
  • Consider network conditions (e.g., check for connectivity) before making requests.
  • Implement proper cancellation for long-running requests, especially when the associated UI component is destroyed. Coroutines handle this well when used with appropriate scopes.

By following these guidelines, you can ensure that your Android application remains responsive while efficiently handling network operations in the background.