Modeling asynchronous results with AsyncResult
I have been working on a small Kotlin Multiplatform library that I finally decided to open source: 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 begunLoading- In progressSuccess<T>- Completed with a valueError- 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 valuemapError— Transform the errorbimap— Transform both success and errorflatMap— Chain operations that return AsyncResultflatten— Flatten nested AsyncResult<AsyncResult<T>>fold— Generate a value from any statecast— Safe cast to a subtype
Validation & Filtering
filterOrError— Convert to error if predicate failsorError— Convert null success to errortoErrorIf— Convert to error if condition is truetoErrorUnless— Convert to error unless condition is true
Value Extraction
getOrNull— Get value or nullgetOrDefault— Get value or a defaultgetOrElse— Get value or compute fallbackgetOrThrow— Get value or throwgetOrEmpty— Get collection or empty listerrorOrNull— Get error if presentthrowableOrNull— Get throwable if present
Unwrapping (Rust-style)
unwrap— Extract value, throw if not Successexpect— Like unwrap with custom error message
Recovery
recover— Recover from any errorrecoverIf— Recover only if predicate matchesor— Return fallback result if errororElse— Compute fallback result if error
Combining
zip— Combine up to 4 results into onecombine— Combine a list of resultssequence— List<AsyncResult<T>> to AsyncResult<List<T>>and / andThen— Chain results sequentiallyspread— Split Pair/Triple result into multiple results
Side Effects
onSuccess— Run action on successonError— Run action on erroronLoading— Run action on loadingonNotStarted— Run action on not started
Flow Extensions
asAsyncResult— Convert Flow<T> to Flow<AsyncResult<T>>skipWhileLoading— Filter out Loading emissionscacheLatestSuccess— Keep emitting cached Success during LoadingtimeoutToError— Emit Error if no result within timeoutretryOnError— 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 AsyncResultAsyncResult<Either>.bind— Flatten nested Either inside AsyncResultFlow<Either>.asAsyncResult— Convert Flow of Either to Flow of AsyncResultFlow<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 NotStartedisLoading— Assert result is LoadingisIncomplete— Assert result is NotStarted or LoadingisSuccess— Assert result is SuccessisSuccessEqualTo— Assert Success with specific valueisError— Assert result is ErrorisErrorWithMetadata— Assert Error with typed metadataassertSuccess / assertError— Flow assertions for testing emissions
Anyway...
Give it a try if you see value in it! 😄