Skip to main content

UI State Module

Presentation layer module providing BaseViewModel with MVI-style state management, one-time events, and 5 async helpers.

implementation("com.github.wahidabd.miru-sdk:ui-state:<version>")

BaseViewModel

Abstract base class for all ViewModels. Manages typed state and one-time UI events:

class MyViewModel(
private val useCase: MyUseCase
) : BaseViewModel<MyState, MyEvent>(MyState()) {
// ...
}

State Management

// Read current state
val current = currentState

// Update state with a reducer
setState { copy(isLoading = true) }

// Observe in Composable
val state by viewModel.uiState.collectAsStateWithLifecycle()

Events

One-time events (navigation, snackbar, etc.) that should not survive recomposition:

// Emit event
sendEvent(MyEvent.NavigateToDetail(id))

// Collect in Composable
viewModel.events.collectAsEffect { event ->
when (event) {
is MyEvent.NavigateToDetail -> navController.navigate("detail/${event.id}")
is MyEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.message)
}
}

Async Helpers

BaseViewModel provides 5 helpers for handling async operations. Choose based on how much control you need:

collectResource()

Simplest — pipes a one-shot suspend -> Resource<T> directly into a MutableStateFlow. Zero boilerplate.

private val _users = MutableStateFlow<Resource<List<User>>>(Resource.Loading())
val users = _users.asStateFlow()

fun loadUsers() = collectResource(_users) { getUsersUseCase() }

Best for: screens where the entire UI is driven by a single Resource state. Pair with MiruResourceView for zero-boilerplate screens.

collectFlow()

Pipes a plain Flow<T> into a MutableStateFlow<Resource<T>> — auto-wraps emissions in Resource.Success:

private val _notifications = MutableStateFlow<Resource<List<Notification>>>(Resource.Loading())
val notifications = _notifications.asStateFlow()

fun observe() = collectFlow(_notifications, distinctUntilChanged = true) {
observeNotificationsUseCase() // returns Flow<List<Notification>>
}

Best for: observing Room queries or other plain Flow streams.

collectFlowResource()

Pipes a Flow<Resource<T>> directly into a MutableStateFlow<Resource<T>>:

private val _feed = MutableStateFlow<Resource<List<FeedItem>>>(Resource.Loading())
val feed = _feed.asStateFlow()

fun observe() = collectFlowResource(_feed) {
observeLiveFeedUseCase() // returns Flow<Resource<List<FeedItem>>>
}

Best for: streams that already emit Resource (e.g., network polling with loading/error states).

execute()

One-shot suspend call with state reducers — gives you full control over how loading, success, and error map to your state:

fun loadProducts() = execute(
call = { getProductsUseCase() },
onLoading = { copy(isLoading = true, error = null) },
onSuccess = { copy(products = it, isLoading = false) },
onError = { copy(isLoading = false, error = it.message) },
errorEvent = { MyEvent.ShowError(it.message ?: "Unknown error") }
)

Best for: screens with complex state (multiple fields, loading indicators, error messages).

collect()

Reactive stream (Flow<Resource<T>>) with state reducers:

fun observeBookmarks() = collect(
flow = { observeBookmarksUseCase() },
distinctUntilChanged = true,
onLoading = { copy(isLoading = true) },
onSuccess = { copy(bookmarks = it, isLoading = false) },
onError = { copy(isLoading = false, error = it.message) }
)

Best for: same as execute() but for reactive streams instead of one-shot calls.

Choosing the Right Helper

HelperInputOutputControl Level
collectResourcesuspend -> Resource<T>MutableStateFlow<Resource<T>>Minimal
collectFlowFlow<T>MutableStateFlow<Resource<T>>Minimal
collectFlowResourceFlow<Resource<T>>MutableStateFlow<Resource<T>>Minimal
executesuspend -> Resource<T>State reducersFull
collectFlow<Resource<T>>State reducersFull
tip

See ViewModel Patterns Guide for real-world examples showing all 5 helpers in one ViewModel.

PagingState

Built-in pagination state management:

data class FeedState(
val paging: PagingState<Post> = PagingState()
)

// Append new page
setState { copy(paging = paging.appendItems(newPosts)) }

// Refresh
setState { copy(paging = paging.refresh(freshPosts)) }

// Check state
paging.isLoading
paging.hasMore
paging.items
paging.currentPage

EventFlow

Channel-backed event flow for one-time UI events:

val events = MutableEventFlow<MyEvent>()

// Send
events.send(MyEvent.ShowToast("Done!"))

// Collect (in Composable)
events.collectAsEffect { event -> /* handle */ }