Elegantly Handling Asynchronous Results in Jetpack Compose

Ruby Lichtenstein
2 min readOct 4, 2023

In modern applications, dealing with asynchronous data fetches, network calls, or other delayed computations is a regular occurrence. It becomes essential to have a systematic way to represent different states of such asynchronous operations, which include the initial loading state, a potential success with the desired data, or an error in case of failures.

The AsyncResult class provides such a mechanism, neatly wrapping different asynchronous states.

Structure

The AsyncResult class is a sealed interface that can represent three distinct states:

  1. Loading: Represents the initial state when the asynchronous operation is ongoing.
  2. Success: Contains the resulting data if the operation was successful.
  3. Error: Captures any exception or error that might occur during the operation.
sealed interface AsyncResult<out T> {
data class Success<T>(val data: T) : AsyncResult<T>
data class Error(val exception: Throwable? = null) : AsyncResult<Nothing>
data object Loading : AsyncResult<Nothing>
}

Extending Functionality

We can extend the functionality of AsyncResult by adding extension functions:

  1. mapSuccess: This function allows transforming the data inside a successful result without having to manually handle other states.
fun <T, R> AsyncResult<T>.mapSuccess(transform: (T) -> R): AsyncResult<R> {
return when (this) {
is AsyncResult.Loading -> AsyncResult.Loading
is AsyncResult.Success -> AsyncResult.Success(transform(data))
is AsyncResult.Error -> AsyncResult.Error(exception)
}
}

asAsyncResult: This function is designed to convert any Flow<T> into a Flow<AsyncResult<T>>, effectively wrapping emitted items into Success state and capturing errors or initial emissions as needed.

fun <T> Flow<T>.asAsyncResult(): Flow<AsyncResult<T>> {
return this
.map<T, AsyncResult<T>> {
AsyncResult.Success(it)
}
.onStart { emit(AsyncResult.Loading) }
.catch { emit(AsyncResult.Error(it)) }
}

Handling Asynchronous Results in Jetpack Compose

With Jetpack Compose becoming the go-to solution for modern Android UI development, seamlessly integrating asynchronous operations within the Composable functions is of paramount importance. One of the frequent challenges developers face is appropriately handling different states of asynchronous tasks (like Loading, Success, and Error) within the UI.

Below, we introduce a utility function that acts as a bridge between AsyncResult and Composables. It offers an elegant way to handle each state:

@Composable
fun <T> AsyncResultHandler(
asyncResult: AsyncResult<T>,
onError: @Composable (String) -> Unit = { message ->
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
text = message,
modifier = Modifier.padding(16.dp),
textAlign = TextAlign.Center
)
}
},
onLoading: @Composable () -> Unit = {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
},
onSuccess: @Composable (T) -> Unit
) {
when (asyncResult) {
is AsyncResult.Loading -> onLoading()
is AsyncResult.Success -> onSuccess(asyncResult.data)
is AsyncResult.Error -> onError(asyncResult.exception?.message ?: "Unknown error")
}
}

--

--