Modeling asynchronous results with AsyncResult

Modeling asynchronous results with AsyncResult
NotStarted, Loading, Success, Error - but in a super cute way?

I have been working on a small Kotlin Multiplatform library that I finally decided to open source: AsyncResult.

GitHub - mrmans0n/asyncresult: AsyncResult helps model asynchronous operations, and provides an ecosystem of operands on top of it
AsyncResult helps model asynchronous operations, and provides an ecosystem of operands on top of it - mrmans0n/asyncresult

The problem

If you have worked with async operations in Android/KMP in whatever state holder / VM / Presenter you use, you have probably written something like this a million times:

sealed class LoadContentError<out T> {
    object Loading : UiState<Nothing>()
    data class Content<T>(val data: T) : UiState<T>()
    data class Error(val exception: Throwable) : UiState<Nothing>()
}

Every. Single. Project.

And then you end up writing the same helpers over and over. I got tired of copy-pasting this between projects, so I made a proper library out of it.

What is it?

AsyncResult is a sealed hierarchy with four states:

  • NotStarted - The operation has not begun
  • Loading - In progress
  • Success<T> - Completed with a value
  • Error - Failed, with optional throwable and typed metadata

The twist is it comes with a bunch of operators inspired by Rust Result and functional programming patterns. Think mapSuccess, flatMap, recover, fold, zip, and more.

Quick example

userRepository.observeUser()
    .asAsyncResult()
    .collect { result ->
        when (result) {
            is NotStarted -> { }
            is Loading -> showLoading()
            is Success -> showUser(result.value)
            is Error -> showError(result.throwable)
        }
    }

The asAsyncResult() extension converts any Flow<T> into a Flow<AsyncResult<T>>, handling exceptions and optionally emitting Loading at the start.

Operators

A little bit of everything! In no particular order...

Transformations

  • mapSuccess — Transform the success value
  • mapError — Transform the error
  • bimap — Transform both success and error
  • flatMap — Chain operations that return AsyncResult
  • flatten — Flatten nested AsyncResult<AsyncResult<T>>
  • fold — Generate a value from any state
  • cast — Safe cast to a subtype

Validation & Filtering

  • filterOrError — Convert to error if predicate fails
  • orError — Convert null success to error
  • toErrorIf — Convert to error if condition is true
  • toErrorUnless — Convert to error unless condition is true

Value Extraction

  • getOrNull — Get value or null
  • getOrDefault — Get value or a default
  • getOrElse — Get value or compute fallback
  • getOrThrow — Get value or throw
  • getOrEmpty — Get collection or empty list
  • errorOrNull — Get error if present
  • throwableOrNull — Get throwable if present

Unwrapping (Rust-style)

  • unwrap — Extract value, throw if not Success
  • expect — Like unwrap with custom error message

Recovery

  • recover — Recover from any error
  • recoverIf — Recover only if predicate matches
  • or — Return fallback result if error
  • orElse — Compute fallback result if error

Combining

  • zip — Combine up to 4 results into one
  • combine — Combine a list of results
  • sequence — List<AsyncResult<T>> to AsyncResult<List<T>>
  • and / andThen — Chain results sequentially
  • spread — Split Pair/Triple result into multiple results

Side Effects

  • onSuccess — Run action on success
  • onError — Run action on error
  • onLoading — Run action on loading
  • onNotStarted — Run action on not started

Flow Extensions

  • asAsyncResult — Convert Flow<T> to Flow<AsyncResult<T>>
  • skipWhileLoading — Filter out Loading emissions
  • cacheLatestSuccess — Keep emitting cached Success during Loading
  • timeoutToError — Emit Error if no result within timeout
  • retryOnError — Retry flow on error with backoff

Either

There are a bunch of operands specific for codebases that are using Arrow, specifically Arrow's Either.

  • Either.toAsyncResult — Convert Either to AsyncResult
  • AsyncResult<Either>.bind — Flatten nested Either inside AsyncResult
  • Flow<Either>.asAsyncResult — Convert Flow of Either to Flow of AsyncResult
  • Flow<AsyncResult>.toEither — Convert Flow of AsyncResult to Either

Testing

And for the extra mark, I also added a bunch of testing goodies, for assertk.

  • isNotStarted — Assert result is NotStarted
  • isLoading — Assert result is Loading
  • isIncomplete — Assert result is NotStarted or Loading
  • isSuccess — Assert result is Success
  • isSuccessEqualTo — Assert Success with specific value
  • isError — Assert result is Error
  • isErrorWithMetadata — Assert Error with typed metadata
  • assertSuccess / assertError — Flow assertions for testing emissions

Anyway...

Give it a try if you see value in it! 😄

Nacho Lopez

Nacho Lopez

Software Engineer - Kotlin / Kotlin Multiplatform / Android / Compose