Skip to main content

MiruResourceView

Composable that eliminates repetitive when (resource) boilerplate by declaratively rendering loading, error, and success states.

Module: :ui-components

Definition

@Composable
fun <T> MiruResourceView(
resource: Resource<T>,
modifier: Modifier = Modifier,
loadingMessage: String? = null,
onRetry: (() -> Unit)? = null,
onLoading: (@Composable () -> Unit)? = null,
onError: (@Composable (String) -> Unit)? = null,
content: @Composable (T) -> Unit
)

Parameters

ParameterTypeDefaultDescription
resourceResource<T>requiredThe resource state to render
modifierModifierModifierApplied to the root container
loadingMessageString?nullMessage displayed in the default loading view
onRetry(() -> Unit)?nullRetry callback shown in the default error view
onLoading@Composable?nullCustom loading composable (overrides default)
onError@Composable (String)?nullCustom error composable (overrides default)
content@Composable (T)requiredContent rendered when resource is Success

Default Behavior

StateDefault Rendering
Resource.LoadingMiruFullScreenLoading(message = loadingMessage)
Resource.ErrorMiruErrorView(message = ..., onRetry = onRetry)
Resource.SuccessCalls content(data)

Basic Usage

@Composable
fun UserListScreen(viewModel: UserListViewModel = koinViewModel()) {
val usersResource by viewModel.users.collectAsStateWithLifecycle()

MiruResourceView(
resource = usersResource,
loadingMessage = "Loading users...",
onRetry = { viewModel.loadUsers() }
) { users ->
LazyColumn {
items(users, key = { it.id }) { user ->
UserCard(user)
}
}
}
}

Custom Loading & Error

MiruResourceView(
resource = feedResource,
onRetry = { viewModel.refresh() },
onLoading = {
// Custom shimmer placeholder
Column {
repeat(5) { ShimmerCard() }
}
},
onError = { message ->
// Custom error banner
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(Icons.Default.Warning, contentDescription = null)
Text(message)
MiruButton(text = "Try Again", onClick = { viewModel.refresh() })
}
}
) { data ->
FeedContent(data)
}

Data Source Agnostic

MiruResourceView works with any StateFlow<Resource<T>>, regardless of which BaseViewModel helper populated it:

// collectResource — suspend one-shot
fun load() = collectResource(_data) { useCase() }

// collectFlow — plain Flow<T>
fun observe() = collectFlow(_data) { dao.getAll() }

// collectFlowResource — Flow<Resource<T>>
fun stream() = collectFlowResource(_data) { useCase.observe() }

All three produce a StateFlow<Resource<T>> that you collect and pass to MiruResourceView:

val dataResource by viewModel.data.collectAsStateWithLifecycle()
MiruResourceView(resource = dataResource) { data -> /* render */ }

Before vs After

Before (manual handling):

val resource by viewModel.users.collectAsStateWithLifecycle()

when (resource) {
is Resource.Loading -> MiruFullScreenLoading()
is Resource.Error -> MiruErrorView(
message = (resource as Resource.Error).exception.message ?: "Error",
onRetry = { viewModel.loadUsers() }
)
is Resource.Success -> {
val users = (resource as Resource.Success).data
LazyColumn {
items(users) { UserCard(it) }
}
}
}

After (with MiruResourceView):

val resource by viewModel.users.collectAsStateWithLifecycle()

MiruResourceView(
resource = resource,
onRetry = { viewModel.loadUsers() }
) { users ->
LazyColumn {
items(users) { UserCard(it) }
}
}