Asynchronicity Guide in Android: From AsyncTask (Legacy) to Coroutines in Jetpack Compose

- 👤 Andrés Cruz

🇪🇸 En español

Asynchronicity Guide in Android: From AsyncTask (Legacy) to Coroutines in Jetpack Compose

When you want to perform an asynchronous call in web environments, we usually resort to AJAX or Fetch. In Android, although the concept is similar, execution is more complex due to thread management and the device lifecycle.

Modern Approach: Kotlin Coroutines and Jetpack Compose

Nowadays, the recommended way to handle asynchrony is through Kotlin Coroutines. Unlike traditional threads, coroutines are lightweight and, most importantly, they are lifecycle-aware.

Using ViewModel and viewModelScope

Instead of declaring the task inside the view, the logic is moved to a ViewModel. This allows the request to survive screen rotation.

// Modern approach in the ViewModel
class MyViewModel : ViewModel() {
    var uiState by mutableStateOf<String?>(null)
        private set
    fun fetchData() {
        viewModelScope.launch {
            // This runs in the background without blocking the UI
            val result = repository.makeNetworkCall() 
            uiState = result // State is updated automatically
        }
    }
}

Implementation in Jetpack Compose

In Jetpack Compose, the interface reacts automatically to state changes. We no longer need response interfaces (delegates); the UI simply "re-composes" when the data arrives.

@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
    val state = viewModel.uiState
    Column {
        if (state == null) {
            CircularProgressIndicator() // Loading...
            LaunchedEffect(Unit) { viewModel.fetchData() }
        } else {
            Text(text = "Result: $state")
        }
    }
}

Simulating the "Undo" option

Gmail's "Undo" use case is a perfect example for coroutines. Instead of complex sleeping threads, we use Kotlin's delay() function, which does not block the main thread.

fun deleteItemWithUndo(item: Item) {
    viewModelScope.launch {
        showUndoSnackbar = true
        try {
            // We wait 5 seconds asynchronously
            withTimeout(5000) {
                // If the user does not cancel in this time...
                repository.deletePermanently(item)
            }
        } catch (e: CancellationException) {
            // If the user presses "Undo", we cancel the coroutine
            showUndoSnackbar = false
        }
    }
}

Legacy Approach: AsyncTask and Manual Threads

Historically, Android used manual Thread or the AsyncTask class (now deprecated) to execute background tasks within an Activity or Fragment:

// Legacy way with Threads
new Thread(new Runnable() {
    public void run() {
        // Expensive tasks here
    }
}).start();

Problems with the Legacy approach

The biggest problem is the Lifecycle. If the user rotates the screen (causing an activity recreate) or navigates back while the request is in progress, memory leaks occur or the request simply "gets lost" when trying to update an interface that no longer exists.

To mitigate this in the Legacy model, interfaces like AsyncResponse were often implemented to delegate the result, but code complexity increased exponentially when trying to handle configuration changes.

public class MyFragment extends Fragment implements
		AsyncResponse {
...
myAsyncTask = new MyAsyncTask();
…
	public void processFinish(String output) {
//The processFinish() method is invoked when the 
//asynchronous class called MyAsyncTask executes the onPostExecute(result) method.
	}
}

Conclusion

While the Legacy approach forced us to fight against the Activity lifecycle, the Compose and Coroutines approach allows us to write asynchronous code that looks sequential, making it much more robust and easier to maintain.

I agree to receive announcements of interest about this Blog.

Master asynchronous programming in Android: from threads and AsyncTask (Legacy) to the powerful Kotlin Coroutines and Jetpack Compose.

| 👤 Andrés Cruz

🇪🇸 En español