Container is a library for simplifying state management and data loading in Android applications. It provides a small set of building blocks that cover the most common reactive patterns: wrapping async results in a typed status, managing derived state from multiple flows, and lazily loading data on demand.
The full documentation is available here.
Add the following line to your build.gradle file:
implementation "com.elveum:container:3.6.0"
Store is a companion library built on top of Container.
It is the higher-level, easier-to-use option: instead of assembling
LazyFlowSubject, LazyCache and pageLoader yourself, you describe where
the data comes from and the store takes care of loading it.
Start with Store if it fits your use case, and drop down to the Container
building blocks described below when you need full control over loading. Both
libraries interoperate: StoreResult and Container convert into each other,
and stores accept the same metadata, pagination and LoaderDecorator
machinery.
See the Store documentation for the full guide.
The library is built around three building blocks:
Container<T>- a sealed type that represents the state of an async operation asPending,Success<T>, orErrorReducer<State>- converts one or more KotlinFlows into aStateFlow<State>, with support for manual state updatesLazyFlowSubject<T>- wraps a loader function in a lazily-startedFlow<Container<T>>with built-in caching, reloading, andContainerstatus handling
Container<T> is a sealed class that represents the current status of an
asynchronous load or operation. It has three possible states:
Container.Pending- the operation is still in progressContainer.Success<T>- the operation completed successfully and holds avalue: TContainer.Error- the operation failed and holds anexception: Exception
Use the Kotlin when keyword or the fold call to handle all three states in one place:
val container: Container<String> = successContainer("Hello")
container.fold(
onPending = { /* show a progress spinner */ },
onError = { exception -> /* show an error message */ },
onSuccess = { value -> /* render the data */ },
)Containers can be created with factory functions:
val pending = pendingContainer()
val success = successContainer("data")
val error = errorContainer(IOException("network error"))For a complete guide (including value extraction, transformations, combining flows, and more), see Container Type.
Reducer<State> converts any Kotlin Flow into a StateFlow<State> while
also allowing manual state updates. This makes it easy to drive a screen's
UI state from one or more reactive sources, with the ability to apply local
changes on top.
@HiltViewModel
class MyViewModel @Inject constructor(
private val getItems: GetItemsUseCase,
) : ViewModel() {
data class State(
val items: List<String> = emptyList(),
val filter: String = "",
)
private val reducer = getItems() // Flow<List<String>>
.toReducer(
initialState = ::State,
nextState = State::copy,
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
)
val stateFlow: StateFlow<State> = reducer.stateFlow
fun applyFilter(filter: String) {
reducer.update { it.copy(filter = filter) }
}
}ContainerReducer<State> is the container-aware variant. It exposes a
StateFlow<Container<State>> so the UI automatically sees Pending,
Error, and Success states without any manual bookkeeping:
private val reducer: ContainerReducer<State> = getItems()
.toContainerReducer(
initialState = ::State,
nextState = State::copy,
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
)
val stateFlow: StateFlow<Container<State>> = reducer.stateFlowFor the full API (combining multiple flows, the ReducerOwner interface,
and the public-interface / private-implementation state pattern), see
Reducer Pattern.
LazyFlowSubject<T> converts a loader function into a Flow<Container<T>>.
The loader runs lazily (only when at least one subscriber is active) and its
latest result is cached so that new subscribers do not re-trigger loading:
LazyFlowSubject is more powerful and leads to simpler code than the built-in
stateIn / shareIn operators: it does not require a CoroutineScope, automatically
wraps results in Container<T> to handle loading and error states, supports reloading
out of the box, and is compatible with any caching strategy.
class ProductRepository(
private val localDataSource: ProductsLocalDataSource,
private val remoteDataSource: ProductsRemoteDataSource,
) {
private val productsSubject = LazyFlowSubject.create {
val local = localDataSource.getProducts()
if (local != null) emit(local)
val remote = remoteDataSource.getProducts()
localDataSource.save(remote)
emit(remote)
}
// ListContainerFlow<T> is an alias for Flow<Container<List<T>>>
fun listenProducts(): ListContainerFlow<Product> = productsSubject.listen()
fun reload() = productsSubject.reloadAsync()
}Key behaviours:
- Instead of
listen(), you can uselistenReloadable()call, which attaches a reload function to every emitted container, enabling pull-to-refresh patterns out of the box (no need to write a separatereload()function). - The loader is cancelled when the last subscriber stops collecting (after a configurable timeout, default 1 s).
- After the timeout the cached value is cleared, so the next subscriber triggers a fresh load
- You can replace the loader at any time with
newLoad/newSimpleLoad - You can push a value directly with
updateWith - A
LoaderDecoratorcan wrap every loader of a subject or a cache, which keeps cross-cutting logic (session checks, logging, retries) out of the individual loaders. It can also finish a load on its own - dropping any cached value or forcing an error through a silent load configuration
For advanced usage (load triggers, source types, flow dependencies,
LoaderDecorator and SubjectFactory for testability) see
Subjects & Cache.
pageLoader turns any key-based data source into a ValueLoader that can be
passed directly to LazyFlowSubject.create. Pages are fetched on demand as
the user scrolls, and the results are concatenated automatically into a single
List<T>:
private val subject = LazyFlowSubject.create(
valueLoader = pageLoader<Int, Order>(
initialKey = 0,
itemId = Order::id,
) { pageKey ->
val page = ordersDataSource.fetchPage(pageKey)
emitPage(page.orders)
if (page.nextKey != null) emitNextKey(page.nextKey)
}
)
fun listenOrders(): Flow<Container<List<Order>>> = subject.listenReloadable()In the UI, call metadata.onItemRendered(index) inside your LazyColumn
to trigger next-page loads as the user scrolls, and read
metadata.nextPageState to show a footer spinner or retry button:
container.fold(
onPending = { CircularProgressIndicator() },
onError = { exception -> /* full-screen error */ },
onSuccess = { orders ->
LazyColumn {
itemsIndexed(orders, key = { _, o -> o.id }) { index, order ->
LaunchedEffect(index) { metadata.onItemRendered(index) }
OrderItem(order)
}
item {
when (val state = metadata.nextPageState) {
PageState.Pending -> CircularProgressIndicator()
is PageState.Error -> Button(onClick = { state.retry() }) { Text("Retry") }
else -> {}
}
}
}
},
)For the full guide (pull-to-refresh, error handling, flow dependencies, and per-item updates), see Pagination.
| Topic | Description |
|---|---|
| Container Type | States, value extraction, transformations, flow extensions, combining flows |
| Reducer Pattern | Reducer, ContainerReducer, combining flows, ReducerOwner |
| Subjects | LazyFlowSubject, metadata, source types |
| Pagination | PageLoader, next-page states, pull-to-refresh, flow dependencies, per-item updates |
| LLM Agent Skill | Installable Agent Skill for AI coding agents: setup, API reference, architecture patterns, testing |