How to represent a fetching state: FetchingState
On our Compose Multiplatform applications, we often need to fetch some data and present it on the screen. To represent the state of such data, we like to have a generic type that can represent loading, success, and error. Most people want to represent it with a sealed class:
sealed class FetchingState<out T> {
data object Loading : FetchingState<Nothing>()
data class Success<out T>(val data: T) : FetchingState<T>()
data class Failure(val error: Exception) : FetchingState<Nothing>()
}
You can use a different name, like LoadingState, FetchingResult etc.
It looks nice at first, but it completely misses refreshing. You see, refreshing is different from reloading. As I explained in previous article, when a user refreshes, its already loaded data shouldn't be lost. We could add isRefreshing property to both Success and Failure:
sealed class FetchingState<out T> {
data object Loading : FetchingState<Nothing>()
data class Success<out T>(
val data: T,
val isRefreshing: Boolean,
) : FetchingState<T>()
data class Failure(
val error: Exception,
val isRefreshing: Boolean,
) : FetchingState<Nothing>()
}
This is a good representation, but it requires separate PullToRefreshBox. It is because even if the error state or empty state has some "Retry" button, it is more intuitive for users to be able to pull-to-refresh on all states.
when(state) {
is Loading -> LoadingState()
is Failure -> {
PullToRefreshBox(
isRefreshing = state.isRefreshing,
onRefresh = onRefresh,
) {
FailureState(state.error)
}
}
is Succes if state.data.isEmpty() -> {
PullToRefreshBox(
isRefreshing = state.isRefreshing,
onRefresh = onRefresh,
) {
EmptyState(state.data)
}
}
is Success -> {
PullToRefreshBox(
isRefreshing = state.isRefreshing,
onRefresh = onRefresh,
) {
SuccessState(state.data)
}
}
}
Sealed classes are used for mutually exclusive states. They are used to signal that we either have success or error, but never both. Imagine a situation where we loaded some data, then we refresh it, and we have an error. The most typical way to handle this situation is to show the error as a snackbar, and keep Success with previous state as state (we should not take away data user successfully loaded). Though what if our designers decide to show this error on UI. We could represent it as Error with extra property previousData, but this really complicates both FetchingState and its handling.
sealed class FetchingState<out T> {
data object Loading : FetchingState<Nothing>()
data class Success<out T>(
val data: T,
val isRefreshing: Boolean,
) : FetchingState<T>()
data class Failure<out T>(
val error: Exception,
val isRefreshing: Boolean,
val previousData: T? = null,
) : FetchingState<Nothing>()
}
Those were the issues I needed to face in one project, and then I saw the same issue in many others. Mobile applications do not have "refresh" like websites, so we should implement a pull-to-refresh mechanism, and implementing makes it really complicated to represent FetchingState as a sealed hierarchy. This is why now I prefer FetchingState defined as a data class (I really wanted to use a sealed class, but after many attempts I resigned). This is how you can define it if you want to make success and error mutually exclusive:
data class FetchingState<out T>(
val result: Result<T>? = null,
val isLoading: Boolean = false,
) {
val data = result?.getOrNull()
val error = result?.exceptionOrNull()
val isRefreshing get() = data != null && isLoading
}
This is how you can define it if you want to support both success and error:
data class FetchingState<out T>(
val data: T? = null,
val error: Exception? = null,
val isLoading: Boolean = false,
) {
val isRefreshing get() = data != null && isLoading
}
Yes, I know it doesn't look good, but it is easy to use:
PullToRefreshBox(
isRefreshing = state.isRefreshing,
onRefresh = onRefresh,
) {
when {
state.result == null -> LoadingState()
state.data != null && state.data.isEmpty() -> EmptyState()
state.data != null -> SuccessState(state.data)
state.error != null -> FailureState(state.error) // Add state.data here if they are not mutually exclusive
}
}
If you have standardized ways to represent loading/error/empty states in your application, you can define a dedicated component to show them instead of repeating the above pattern. I will explain that in my next article, so stay tuned! Subscribe to our newsletter or follow me on X and/or LinkedIn.

Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.