Container¶
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.
Table of Contents¶
- Installation
- Core Concepts
- Container Type
- Reducer Pattern
- LazyFlowSubject
- Pagination
- Detailed Documentation
Installation¶
Add the following line to your build.gradle file:
Core Concepts¶
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 Type¶
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 Pattern¶
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.stateFlow
For the full API (combining multiple flows, the ReducerOwner interface,
and the public-interface / private-implementation state pattern), see
Reducer Pattern.
LazyFlowSubject¶
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
For advanced usage (load triggers, source types, flow dependencies, and
SubjectFactory for testability) see Subjects & Cache.
Pagination¶
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.
Detailed Documentation¶
| 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 |