Fetching and displaying remote data is one of the most common tasks you may be assigned in programmer’s work. We might think the problem has already been solved, yet I still encounter mistakes causing bad experience to users, or rough, hand-crafted solutions for each remote call. This is far from optimal. Let’s review a process of modeling a remote request state in Kotlin.

The first attempt Link to heading

There aren’t many apps that don’t make some kind of network requests. We’ve all been here. Having coded data view and progress indicator, we just need to set up the initial state so that indicator animates and data view is hidden, then make a request and when data arrives – display it and hide indicator. Simple, right?

progressIndicator.visible = true
api.requestData()
    .collect { data ->
        progressIndicator.visible = false
        dataView.visible = true
        dataView.data = data
    }

It’s simple, can’t argue with that, but this approach has a few flaws. Let’s tackle them one by one.

Supporting lazy scenario Link to heading

If a network request is fired upon a certain condition like a UI event, the progress indicator must stay hidden until the condition is actually met. That’s what onStart() is for – moving progressIndicator.visible = true line guarantees the progress indicator shows when the stream starts.

api.requestData()
    .onStart {
        progressIndicator.visible = true
    }
    // ...

Capturing state Link to heading

Since we want to have a remote call state model, introducing a data structure seems like a good idea. Let’s utilize a data class for that:

data class RemoteData(
    val loading: Boolean,
    val data: Data?,
)

Now we integrate the above model into our code:

api.requestData()
    .map { data ->
        RemoteData(loading = false, data = data)
    }
    .onStart {
        emit(RemoteData(loading = true, data = null))
    }
    .collect { rd ->
        progressIndicator.visible = data.loading
        dataView.visible = !data.loading
        dataView.data = rd.data
    }

This code has an interesting property. It doesn’t touch your views before collection of data. It means you can separate state modeling from displaying it, achieving separation of concerns. Hurray!

Impossible state Link to heading

We’re still far from a robust model though. The current model accepts parameters which aren’t exactly meaningful: RemoteData(loading = true, data = SomeData()) and RemoteData(loading = false, data = null). It’s called an impossible state, and we want to avoid it. See the Making Impossible States Impossible talk by Richard Feldman if you’re interested in hearing more.

There is a sealed class hierarchy feature in Kotlin which tells the compiler that a value is of one of specified types. Applying this technique results in the following:

sealed interface RemoteData {
    object Loading : RemoteData
    data class Success(val data: Data) : RemoteData
}

And this solves the impossible state issue. Now a value of RemoteData type can be either Loading or Success, and nothing else.

What we can be improved further is supporting reuse by abstracting data type. Leveraging generic type parameter lets using RemoteData with virtually any type.

sealed interface RemoteData<out T> {
    object Loading : RemoteData<Nothing>
    data class Success<T>(val data: T) : RemoteData<T>
}

A few notes here:

  • Loading is an object because a single instance is sufficient, as it doesn’t hold any data.
  • Also, Loading is an excellent place to use Nothing type, because it means that it never takes or provides any objects of type variable T.
  • Lastly, the parameter T in RemoteData<out T> has out variance, because it may provide a T subtype. In other words, without it, the following assignment wouldn’t compile because Nothing isn’t String type (it’s a subtype of String, and of every other class in Kotlin):
val rd: RemoteData<String> = RemoteData.Loading

Adding NotAsked state and handling Failures Link to heading

While we don’t always need NotAsked state, it’s useful in case a remote request is fired upon a certain event that doesn’t occur right after entering a particular screen. But we should always handle the error path. Failure is very similar to Success in terms of implementation. A common pitfall here is to specify Throwable as an upper bound for Error type. It doesn’t make much sense. While you could wrap Throwable object with Failure class without such limitation, you might want to pass a simple String message instead – your UI doesn’t necessarily need to know this is Throwable if handling error is about displaying a message on a screen, right?

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>
}

Integration with a stream library should lift the emitted values into RemoteData context, meaning a lifted stream should start its emission with Loading, and then finish with either Success or Failure. We could do it like this:

fun <T> Flow<T>.remotify() = this
    .map { data -> Success(data) }
    .onStart { emit(Loading) }
    .catch { error -> Failure(error) }

And now the usage:

api.requestData()
    .remotify()
    .collect { rd ->
        progressIndicator.visible = rd is Loading

        errorView.visible = rd is Failure
        errorView.message = (rd as? Failure<String>).message

        dataView.visisble = rd is Success
        dataView.data = (rd as? Success<Data>).data
    }

We came to the point where the logic of loading remote data into a view is separated, ensuring consistent behavior throughout the application, maintaining clear semantics and disallowing some common mistakes. I believe this is a step towards better experience – for both programmers and end-users.


RemoteData is the name used in Elm implementation. This article is heavily inspired by both the Elm RemoteData library and the original blog post by Kris Jenkins.

You can use my port of the RemoteData library for Kotlin here.


This post was featured in Kotlin Weekly #344