Smooth UI – Avoid UI Blocking and Lag in Android Apps - Textnotes

Smooth UI – Avoid UI Blocking and Lag in Android Apps


Learn how to handle background tasks like network requests efficiently in Android to avoid UI blocking and lag. This tutorial explores using AsyncTask (for older Android versions) and Coroutines (for modern Android development) to run network tasks asynchronously.

A smooth and responsive UI is critical for creating a good user experience in Android apps. If your app’s UI thread is blocked, the app becomes unresponsive, leading to lag or ANR (Application Not Responding) errors. One of the most common causes of UI blocking is performing long-running tasks, like network operations or database queries, on the main UI thread.

This tutorial will guide you on how to avoid UI blocking by offloading such tasks to background threads using AsyncTask or Coroutines.

i) Avoiding UI Blocking by Offloading Tasks to Background

1. Understanding UI Thread and Background Threads

Android apps have a main UI thread, responsible for rendering UI elements and handling user interactions. Any long-running operation (like fetching data from a server or performing a complex computation) performed on the UI thread will block the user interface, leading to unresponsiveness.

To prevent this, you should offload heavy operations to a background thread. This ensures the UI remains responsive, allowing users to continue interacting with the app while the background task completes.

ii) Handling Network Tasks with AsyncTask (For Older Versions of Android)

Before Kotlin and Coroutines, AsyncTask was commonly used to manage background operations in Android. While AsyncTask is deprecated in modern Android versions (API level 30+), it is still used in older apps.

Step 1: Using AsyncTask to Handle Background Tasks

AsyncTask allows you to perform background operations and then update the UI when the task is complete. The doInBackground() method runs the task in the background, while onPostExecute() updates the UI after the task finishes.


// Example using AsyncTask to perform a network operation
class MyTask : AsyncTask<String, Void, String>() {

override fun doInBackground(vararg params: String?): String {
// Simulate a network operation
return "Fetched Data"
}

override fun onPostExecute(result: String?) {
super.onPostExecute(result)
// Update the UI with the result
textView.text = result
}
}

// To execute the AsyncTask:
val task = MyTask()
task.execute("https://example.com/data")

In the above example:

  1. doInBackground() handles the background operation (e.g., a network call).
  2. onPostExecute() runs on the main UI thread and updates the UI with the result from the background task.
Limitations of AsyncTask:
  1. AsyncTask is deprecated in Android API level 30+ and no longer recommended for new projects.
  2. It doesn’t provide great support for handling multiple background tasks concurrently.
  3. Managing background tasks with AsyncTask can become difficult as your app grows in complexity.

iii) Handling Network Tasks with Kotlin Coroutines (Recommended for Modern Apps)

In modern Android development, Coroutines provide a more powerful and flexible way to handle background tasks without blocking the UI thread. Coroutines make it easy to write asynchronous code in a sequential manner, making it much more readable than using AsyncTask.

Step 1: Setting Up Coroutines in Your Project

To use Coroutines, add the necessary dependencies in your app-level build.gradle file:


dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2" // Check for the latest version
}

Step 2: Using Coroutines for Background Tasks

Kotlin Coroutines allow you to write asynchronous code as if it’s sequential, making it easier to manage background operations. You can use the Dispatchers.IO context for I/O operations like network requests, and Dispatchers.Main for updating the UI on the main thread.

Example: Making a Network Request with Coroutines

import kotlinx.coroutines.*
import android.widget.TextView

class MyActivity : AppCompatActivity() {

private lateinit var textView: TextView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

textView = findViewById(R.id.textView)

// Run network request on a background thread
GlobalScope.launch(Dispatchers.Main) {
val result = withContext(Dispatchers.IO) {
// Simulate network operation
fetchDataFromNetwork()
}
// Update the UI on the main thread
textView.text = result
}
}

private fun fetchDataFromNetwork(): String {
// Simulate a network call
Thread.sleep(2000)
return "Fetched Data"
}
}

In this example:

  1. The launch function starts a coroutine on the Main thread.
  2. The withContext(Dispatchers.IO) block runs the network task on the background thread (IO dispatcher).
  3. Once the task is complete, the result is returned to the main thread, and the UI is updated.

Step 3: Using ViewModel and LiveData with Coroutines

For a more lifecycle-aware approach, use ViewModel to manage UI-related data and LiveData to observe data changes.


class MyViewModel : ViewModel() {

private val _data = MutableLiveData<String>()
val data: LiveData<String> get() = _data

fun fetchData() {
viewModelScope.launch {
val result = withContext(Dispatchers.IO) {
fetchDataFromNetwork()
}
_data.postValue(result)
}
}

private fun fetchDataFromNetwork(): String {
Thread.sleep(2000) // Simulate a network call
return "Fetched Data"
}
}

In this approach:

  1. viewModelScope ensures that the coroutine is automatically canceled when the ViewModel is cleared (avoiding memory leaks).
  2. LiveData observes the result, and UI components can react to changes automatically.

Step 4: Properly Handling Errors with Coroutines

When using coroutines for network tasks, it’s essential to handle exceptions gracefully. You can use try-catch blocks to manage exceptions that may occur during background operations.


GlobalScope.launch(Dispatchers.Main) {
try {
val result = withContext(Dispatchers.IO) {
fetchDataFromNetwork()
}
textView.text = result
} catch (e: Exception) {
textView.text = "Error: ${e.message}"
}
}

iv) Comparison of AsyncTask and Coroutines

FeatureAsyncTaskCoroutines
Asynchronous CodeCallback-basedSequential-style code (suspend functions)
PerformanceSlower, with overheadFaster and more efficient
Ease of UseHarder to manage for complex tasksSimple, clean, and readable
Background Task HandlingLimited (doesn't support advanced concurrency)Supports advanced concurrency and cancellation
DeprecationDeprecated (API level 30+)Actively supported in modern Android development

v) Conclusion

For smooth UI performance in Android, it’s essential to offload time-consuming tasks like network operations to background threads. While AsyncTask was once the go-to solution, Coroutines offer a more efficient and readable way to handle background tasks in modern Android apps. By using Coroutines, you can ensure that your app stays responsive and free from UI blocking or lag, delivering a seamless experience to users.

For the best user experience, consider:

  1. Using Coroutines for background tasks instead of deprecated AsyncTask.
  2. Leveraging ViewModel and LiveData for lifecycle-aware task management.
  3. Handling errors gracefully with proper exception handling in coroutines.