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
Add the following line to your build.gradle file:
implementation "com.elveum:container:2.0.0-beta15"
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.
LazyCache
LazyCache<Arg, T> is a collection of LazyFlowSubject instances keyed by
an argument. Use it whenever you need to load and cache data for multiple
distinct identifiers (e.g. user IDs, product IDs):
private val usersCache = LazyCache.create<Long, User> { id ->
val local = localDataSource.getUserById(id)
if (local != null) emit(local)
emit(remoteDataSource.getUserById(id))
}
fun getUser(id: Long): Flow<Container<User>> = usersCache.listen(id)
fun reloadUser(id: Long) = usersCache.reloadAsync(id)
Each key has its own independent loading lifecycle, caching behaviour, and subscriber count. See Subjects & Cache for the complete API.
Detailed Documentation
| Topic | Description |
|---|---|
| Container Type | States, value extraction, transformations, flow extensions, combining flows |
| Reducer Pattern | Reducer, ContainerReducer, combining flows, ReducerOwner |
| Subjects & Cache | LazyFlowSubject, LazyCache, FlowSubject, metadata, source types |