|
| 1 | +# Container |
| 2 | + |
| 3 | +[](https://uandcode.com/sh/container) |
| 4 | + |
| 5 | +[](LICENSE) |
| 6 | +[](https://github.com/romychab/container/actions/workflows/pr-check.yml) |
| 7 | +[](https://github.com/romychab/hilt-autobind/actions/workflows/publish.yml) |
| 8 | +[](https://github.com/romychab/container/actions/workflows/publish.yml) |
| 9 | + |
| 10 | +Container is a library for simplifying state management and data loading in |
| 11 | +Android applications. It provides a small set of building blocks |
| 12 | +that cover the most common reactive patterns: wrapping async results in a |
| 13 | +typed status, managing derived state from multiple flows, and lazily loading |
| 14 | +data on demand. |
| 15 | + |
| 16 | +<!-- docs-exclude-start --> |
| 17 | +## Documentation |
| 18 | + |
| 19 | +The full documentation is available [here](https://docs.uandcode.com/container). |
| 20 | +<!-- docs-exclude-end --> |
| 21 | + |
| 22 | +## Table of Contents |
| 23 | + |
| 24 | +- [Installation](#installation) |
| 25 | +- [Core Concepts](#core-concepts) |
| 26 | + - [Container Type](#container-type) |
| 27 | + - [Reducer Pattern](#reducer-pattern) |
| 28 | + - [LazyFlowSubject](#lazyflowsubject) |
| 29 | + - [Pagination](#pagination) |
| 30 | +- [Detailed Documentation](#detailed-documentation) |
| 31 | + |
| 32 | +## Installation |
| 33 | + |
| 34 | +Add the following line to your `build.gradle` file: |
| 35 | + |
| 36 | +``` |
| 37 | +implementation "com.elveum:container:2.1.0-beta03" |
| 38 | +``` |
| 39 | + |
| 40 | +## Core Concepts |
| 41 | + |
| 42 | +The library is built around three building blocks: |
| 43 | + |
| 44 | +- **`Container<T>`** - a sealed type that represents the state of an async operation as `Pending`, `Success<T>`, or `Error` |
| 45 | +- **`Reducer<State>`** - converts one or more Kotlin `Flow`s into a `StateFlow<State>`, with support for manual state updates |
| 46 | +- **`LazyFlowSubject<T>`** - wraps a loader function in a lazily-started `Flow<Container<T>>` with built-in caching, reloading, and `Container` status handling |
| 47 | + |
| 48 | +### Container Type |
| 49 | + |
| 50 | +`Container<T>` is a sealed class that represents the current status of an |
| 51 | +asynchronous load or operation. It has three possible states: |
| 52 | + |
| 53 | +- `Container.Pending` - the operation is still in progress |
| 54 | +- `Container.Success<T>` - the operation completed successfully and holds a `value: T` |
| 55 | +- `Container.Error` - the operation failed and holds an `exception: Exception` |
| 56 | + |
| 57 | +Use the Kotlin `when` keyword or the `fold` call to handle all three states in one place: |
| 58 | + |
| 59 | +```kotlin |
| 60 | +val container: Container<String> = successContainer("Hello") |
| 61 | + |
| 62 | +container.fold( |
| 63 | + onPending = { /* show a progress spinner */ }, |
| 64 | + onError = { exception -> /* show an error message */ }, |
| 65 | + onSuccess = { value -> /* render the data */ }, |
| 66 | +) |
| 67 | +``` |
| 68 | + |
| 69 | +Containers can be created with factory functions: |
| 70 | + |
| 71 | +```kotlin |
| 72 | +val pending = pendingContainer() |
| 73 | +val success = successContainer("data") |
| 74 | +val error = errorContainer(IOException("network error")) |
| 75 | +``` |
| 76 | + |
| 77 | +For a complete guide (including value extraction, transformations, |
| 78 | +combining flows, and more), see [Container Type](docs/container-type.md). |
| 79 | + |
| 80 | +### Reducer Pattern |
| 81 | + |
| 82 | +`Reducer<State>` converts any Kotlin `Flow` into a `StateFlow<State>` while |
| 83 | +also allowing manual state updates. This makes it easy to drive a screen's |
| 84 | +UI state from one or more reactive sources, with the ability to apply local |
| 85 | +changes on top. |
| 86 | + |
| 87 | +```kotlin |
| 88 | +@HiltViewModel |
| 89 | +class MyViewModel @Inject constructor( |
| 90 | + private val getItems: GetItemsUseCase, |
| 91 | +) : ViewModel() { |
| 92 | + |
| 93 | + data class State( |
| 94 | + val items: List<String> = emptyList(), |
| 95 | + val filter: String = "", |
| 96 | + ) |
| 97 | + |
| 98 | + private val reducer = getItems() // Flow<List<String>> |
| 99 | + .toReducer( |
| 100 | + initialState = State(), |
| 101 | + nextState = State::copy, |
| 102 | + scope = viewModelScope, |
| 103 | + started = SharingStarted.WhileSubscribed(5000), |
| 104 | + ) |
| 105 | + |
| 106 | + val stateFlow: StateFlow<State> = reducer.stateFlow |
| 107 | + |
| 108 | + fun applyFilter(filter: String) { |
| 109 | + reducer.update { it.copy(filter = filter) } |
| 110 | + } |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +`ContainerReducer<State>` is the container-aware variant. It exposes a |
| 115 | +`StateFlow<Container<State>>` so the UI automatically sees `Pending`, |
| 116 | +`Error`, and `Success` states without any manual bookkeeping: |
| 117 | + |
| 118 | +```kotlin |
| 119 | +private val reducer: ContainerReducer<State> = getItems() |
| 120 | + .toContainerReducer( |
| 121 | + initialState = ::State, |
| 122 | + nextState = State::copy, |
| 123 | + scope = viewModelScope, |
| 124 | + started = SharingStarted.WhileSubscribed(5000), |
| 125 | + ) |
| 126 | + |
| 127 | +val stateFlow: StateFlow<Container<State>> = reducer.stateFlow |
| 128 | +``` |
| 129 | + |
| 130 | +For the full API (combining multiple flows, the `ReducerOwner` interface, |
| 131 | +and the public-interface / private-implementation state pattern), see |
| 132 | +[Reducer Pattern](docs/reducer-pattern.md). |
| 133 | + |
| 134 | +### LazyFlowSubject |
| 135 | + |
| 136 | +`LazyFlowSubject<T>` converts a loader function into a `Flow<Container<T>>`. |
| 137 | +The loader runs lazily (only when at least one subscriber is active) and its |
| 138 | +latest result is cached so that new subscribers do not re-trigger loading: |
| 139 | + |
| 140 | +`LazyFlowSubject` is more powerful and leads to simpler code than the built-in |
| 141 | +`stateIn` / `shareIn` operators: it does not require a `CoroutineScope`, automatically |
| 142 | +wraps results in `Container<T>` to handle loading and error states, supports reloading |
| 143 | +out of the box, and is compatible with any caching strategy. |
| 144 | + |
| 145 | +```kotlin |
| 146 | +class ProductRepository( |
| 147 | + private val localDataSource: ProductsLocalDataSource, |
| 148 | + private val remoteDataSource: ProductsRemoteDataSource, |
| 149 | +) { |
| 150 | + |
| 151 | + private val productsSubject = LazyFlowSubject.create { |
| 152 | + val local = localDataSource.getProducts() |
| 153 | + if (local != null) emit(local) |
| 154 | + val remote = remoteDataSource.getProducts() |
| 155 | + localDataSource.save(remote) |
| 156 | + emit(remote) |
| 157 | + } |
| 158 | + |
| 159 | + // ListContainerFlow<T> is an alias for Flow<Container<List<T>>> |
| 160 | + fun listenProducts(): ListContainerFlow<Product> = productsSubject.listen() |
| 161 | + |
| 162 | + fun reload() = productsSubject.reloadAsync() |
| 163 | +} |
| 164 | +``` |
| 165 | + |
| 166 | +Key behaviours: |
| 167 | + |
| 168 | +- Instead of `listen()`, you can use `listenReloadable()` call, which attaches |
| 169 | + a reload function to every emitted container, enabling pull-to-refresh patterns |
| 170 | + out of the box (no need to write a separate `reload()` function). |
| 171 | +- The loader is cancelled when the last subscriber stops collecting (after |
| 172 | + a configurable timeout, default 1 s). |
| 173 | +- After the timeout the cached value is cleared, so the next subscriber |
| 174 | + triggers a fresh load |
| 175 | +- You can replace the loader at any time with `newLoad` / `newSimpleLoad` |
| 176 | +- You can push a value directly with `updateWith` |
| 177 | + |
| 178 | +For advanced usage (load triggers, source types, flow dependencies, and |
| 179 | +`SubjectFactory` for testability) see [Subjects & Cache](docs/subjects.md). |
| 180 | + |
| 181 | +### Pagination |
| 182 | + |
| 183 | +`pageLoader` turns any key-based data source into a `ValueLoader` that can be |
| 184 | +passed directly to `LazyFlowSubject.create`. Pages are fetched on demand as |
| 185 | +the user scrolls, and the results are concatenated automatically into a single |
| 186 | +`List<T>`: |
| 187 | + |
| 188 | +```kotlin |
| 189 | +private val subject = LazyFlowSubject.create( |
| 190 | + valueLoader = pageLoader<Int, Order>( |
| 191 | + initialKey = 0, |
| 192 | + itemId = Order::id, |
| 193 | + ) { pageKey -> |
| 194 | + val page = ordersDataSource.fetchPage(pageKey) |
| 195 | + emitPage(page.orders) |
| 196 | + if (page.nextKey != null) emitNextKey(page.nextKey) |
| 197 | + } |
| 198 | +) |
| 199 | + |
| 200 | +fun listenOrders(): Flow<Container<List<Order>>> = subject.listenReloadable() |
| 201 | +``` |
| 202 | + |
| 203 | +In the UI, call `metadata.onItemRendered(index)` inside your `LazyColumn` |
| 204 | +to trigger next-page loads as the user scrolls, and read |
| 205 | +`metadata.nextPageState` to show a footer spinner or retry button: |
| 206 | + |
| 207 | +```kotlin |
| 208 | +container.fold( |
| 209 | + onPending = { CircularProgressIndicator() }, |
| 210 | + onError = { exception -> /* full-screen error */ }, |
| 211 | + onSuccess = { orders -> |
| 212 | + LazyColumn { |
| 213 | + itemsIndexed(orders, key = { _, o -> o.id }) { index, order -> |
| 214 | + LaunchedEffect(index) { metadata.onItemRendered(index) } |
| 215 | + OrderItem(order) |
| 216 | + } |
| 217 | + item { |
| 218 | + when (val state = metadata.nextPageState) { |
| 219 | + PageState.Pending -> CircularProgressIndicator() |
| 220 | + is PageState.Error -> Button(onClick = { state.retry() }) { Text("Retry") } |
| 221 | + else -> {} |
| 222 | + } |
| 223 | + } |
| 224 | + } |
| 225 | + }, |
| 226 | +) |
| 227 | +``` |
| 228 | + |
| 229 | +For the full guide (pull-to-refresh, error handling, flow dependencies, and |
| 230 | +per-item updates), see [Pagination](docs/paging.md). |
| 231 | + |
| 232 | +## Detailed Documentation |
| 233 | + |
| 234 | +| Topic | Description | |
| 235 | +|-------|-------------| |
| 236 | +| [Container Type](docs/container-type.md) | States, value extraction, transformations, flow extensions, combining flows | |
| 237 | +| [Reducer Pattern](docs/reducer-pattern.md) | `Reducer`, `ContainerReducer`, combining flows, `ReducerOwner` | |
| 238 | +| [Subjects](docs/subjects.md) | `LazyFlowSubject`, metadata, source types | |
| 239 | +| [Pagination](docs/paging.md) | `PageLoader`, next-page states, pull-to-refresh, flow dependencies, per-item updates | |
0 commit comments