Container¶
Container is a Kotlin Multiplatform library for simplifying state management and data loading. 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
- Multiplatform Support
- Store: the High-Level Library
- Core Concepts
- Container Type
- Reducer Pattern
- LazyFlowSubject
- Pagination
- Detailed Documentation
Installation¶
Add the following line to your build.gradle file:
Multiplatform Support¶
Container is published as a Kotlin Multiplatform library for the following targets:
jvm(including Android)linuxX64iosArm64,iosSimulatorArm64,iosX64
This changes how the library resolves depending on your build tool:
- Gradle users are unaffected.
implementation("com.elveum:container:3.7.0")keeps working as before - Gradle module metadata transparently resolves the rootcom.elveum:containercoordinate to the right per-target artifact (e.g.container-jvmon the JVM). - Maven users must switch coordinates. Maven does not understand Gradle
module metadata, so the root
com.elveum:containerartifact is no longer a usable JVM jar for Maven - it is a Kotlin metadata module. If you consume this library from a Mavenpom.xml, change theartifactIdfromcontainertocontainer-jvm:
<dependency>
<groupId>com.elveum</groupId>
<artifactId>container-jvm</artifactId>
<version>3.7.0</version>
</dependency>
linuxX64users must supply their ownCoroutineScopeFactory. The defaultCoroutineScopeFactoryusesDispatchers.Main.immediate, which has no Kotlin/Native implementation on Linux. JVM/Android and iOS are unaffected since a Main dispatcher is provided there.
Store: the High-Level Library¶
:store is JVM/Android-only for now and is not published for the other
Container targets (linuxX64, iOS).
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.
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 - 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.
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 |
| LLM Agent Skill | Installable Agent Skill for AI coding agents: setup, API reference, architecture patterns, testing |