Handling remote requests in applications with UI is about displaying progress indicators, handling errors and populating data. This library provides you with a model of such request that help you to avoid common errors and streamlining state consumption.
Ported from Elm. See the original blog post for the motivation behind it.
The structure in Kotlin is pretty straightforward. It's a sealed interface
hierarchy parametrized by Error
and Data
type variables and consists of four
states:
sealed interface RemoteData<out Error, out Data> {
object NotAsked : RemoteData<Nothing, Nothing>
object Loading : RemoteData<Nothing, Nothing>
data class Failure<T>(val error: T) : RemoteData<T, Nothing>
data class Success<T>(val data: T) : RemoteData<Nothing, T>
}
You can use factory extension functions to instantiate Success
and Failure
objects.
val success: RemoteData<Nothing, String> = "stuff".success()
val failure: RemoteData<String, Nothing> = "error".failure()
Modify RemoteData
content with mapping functions:
"RemoteData"
.success() // Success("RemoteData")
.map { "Hello $it!" } // Success("Hello RemoteData!")
Throwable("What a Terrible Failure")
.failure() // Failure(Throwable("What a Terrible Failure"))
.mapFailure { it.description } // Failure("What a Terrible Failure")
Folding RemoteData
is also possible:
fun toString(rd: RemoteData<Throwable, List<String>>) {
rd.fold(
ifNotAsked = { "not asked" },
ifLoading = { "loading" },
ifFailure = { it.description },
ifSuccess = { it.joinToString() },
)
}
You can utilize bind()
extension to render your UI.
It accepts setters for your UI as parameters.
fun updateUi(data: RemoteData<Throwable, Stuff>) {
data.bind(
loading = { loadingIndicator.visible = it },
error = { errorView.error = it },
data = { stuffView.stuff = it },
)
}
There is a RemoteDataView()
composable for easy consuming of RemoteData
from
Compose. It invokes corresponding parameters depending on its concrete type
(similarly to fold()
method).
@Composable
fun RemoteStuffView(rd: RemoteData<Throwable, Stuff>) {
RemoteDataView(
remoteData = rd,
notAsked = { Text("Press the button to load stuff.") },
loading = { ProgressIndicator() },
failure = { error -> FailureView(error) },
success = { stuff -> StuffView(stuff) },
)
}
This is more of an example than a useful extension, because in real world you'll have different types for your views.
In order to use RemoteData<Throwable, *>
with it, you should first use
mapFailure()
to have a RemoteData<String, *>
.
fun updateUi(rd: RemoteData<String, Stuff>) {
rd.bind(
progressIndicator = progressBar,
errorTextView = errorTextView,
bindData = { stuff -> updateStuffView(stuff) },
)
}
Often times you have a ready to use API returning a stream – be it a RxJava Single, or Kotlin Flow. Consuming such stream isn't trivial. Most of the time you want to set loading state on stream start and update it when it terminates. You should also handle errors and data when request finishes successfully.
api.requestStuff()
.remotify()
.collect { data: RemoteData<Throwable, Stuff> ->
updateUi(data)
}
There are also mapSuccess()
and mapFailure()
functions.
Kotlin Flow, RxJava2, and RxJava3 are supported in remotedata-flow
,
remotedata-rx2
, and remotedata-rx3
artifacts respectively.
This project uses Bill of Materials. You should specify a BOM dependency with a version, and then add required artifacts omitting version.
// Specify BOM version
implementation(platform("com.carelesscoyotes.remotedata:remotedata-bom:$version"))
// Core artifact
implementation("com.carelesscoyotes.remotedata:remotedata")
// Compose RemoteDataView() support
implementation("com.carelesscoyotes.remotedata:remotedata-compose")
// Streams support
implementation("com.carelesscoyotes.remotedata:remotedata-flow")
implementation("com.carelesscoyotes.remotedata:remotedata-rx2")
implementation("com.carelesscoyotes.remotedata:remotedata-rx3")
You can also use version catalog adding the following to the libs.versions.toml
file:
remotedata-bom = "com.carelesscoyotes.remotedata:remotedata-bom:0.5"
remotedata = { module = "com.carelesscoyotes.remotedata:remotedata" }
remotedata-compose = { module = "com.carelesscoyotes.remotedata:remotedata-compose" }
remotedata-flow = { module = "com.carelesscoyotes.remotedata:remotedata-flow" }
remotedata-rx2 = { module = "com.carelesscoyotes.remotedata:remotedata-rx2" }
remotedata-rx3 = { module = "com.carelesscoyotes.remotedata:remotedata-rx3" }
And then adding dependencies in build.gradle
:
implementation(platform(libs.remotedata.bom))
implementation(libs.remotedata)
implementation(libs.remotedata.flow)
// ...