
Simplifies state management and data loading with typed async status, reducer-based state composition, lazy cached flows with reloads, loader decorators, and on-demand paginated loading.
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.
The full documentation is available here.
Add the following line to your build.gradle file:
implementation "com.elveum:container:3.7.0"
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
root com.elveum:container coordinate to the right per-target artifact
(e.g. container-jvm on the JVM).
Maven users must switch coordinates. Maven does not understand Gradle
module metadata, so the root com.elveum:container artifact is no longer a
usable JVM jar for Maven - it is a Kotlin metadata module. If you consume
this library from a Maven pom.xml, change the artifactId from
container to container-jvm:
<dependency>
<groupId>com.elveum</groupId>
<artifactId>container-jvm</artifactId>
<version>3.7.0</version>
</dependency>linuxX64 users must supply their own CoroutineScopeFactory. The
default CoroutineScopeFactory uses Dispatchers.Main.immediate, which has
no Kotlin/Native implementation on Linux. JVM/Android and iOS are unaffected
since a Main dispatcher is provided there.
: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.
The library is built around three building blocks:
Container<T> - a sealed type that represents the state of an async operation as Pending, Success<T>, or Error
Reducer<State> - converts one or more Kotlin Flows into a StateFlow<State>, with support for manual state updatesLazyFlowSubject<T> - wraps a loader function in a lazily-started Flow<Container<T>> with built-in caching, reloading, and Container status handlingContainer<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 a value: T
Container.Error - the operation failed and holds an exception: 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:
listen(), you can use listenReloadable() call, which attaches
a reload function to every emitted container, enabling pull-to-refresh patterns
out of the box (no need to write a separate reload() function).newLoad / newSimpleLoad
updateWith
LoaderDecorator can 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 configurationFor 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 |
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.
The full documentation is available here.
Add the following line to your build.gradle file:
implementation "com.elveum:container:3.7.0"
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
root com.elveum:container coordinate to the right per-target artifact
(e.g. container-jvm on the JVM).
Maven users must switch coordinates. Maven does not understand Gradle
module metadata, so the root com.elveum:container artifact is no longer a
usable JVM jar for Maven - it is a Kotlin metadata module. If you consume
this library from a Maven pom.xml, change the artifactId from
container to container-jvm:
<dependency>
<groupId>com.elveum</groupId>
<artifactId>container-jvm</artifactId>
<version>3.7.0</version>
</dependency>linuxX64 users must supply their own CoroutineScopeFactory. The
default CoroutineScopeFactory uses Dispatchers.Main.immediate, which has
no Kotlin/Native implementation on Linux. JVM/Android and iOS are unaffected
since a Main dispatcher is provided there.
: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.
The library is built around three building blocks:
Container<T> - a sealed type that represents the state of an async operation as Pending, Success<T>, or Error
Reducer<State> - converts one or more Kotlin Flows into a StateFlow<State>, with support for manual state updatesLazyFlowSubject<T> - wraps a loader function in a lazily-started Flow<Container<T>> with built-in caching, reloading, and Container status handlingContainer<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 a value: T
Container.Error - the operation failed and holds an exception: 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:
listen(), you can use listenReloadable() call, which attaches
a reload function to every emitted container, enabling pull-to-refresh patterns
out of the box (no need to write a separate reload() function).newLoad / newSimpleLoad
updateWith
LoaderDecorator can 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 configurationFor 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 |