Skip to content

Commit e61f193

Browse files
committed
docs: add README.md file
1 parent 3b9ca76 commit e61f193

1 file changed

Lines changed: 239 additions & 0 deletions

File tree

README.md

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
# Container
2+
3+
[![Maven Central](https://img.shields.io/maven-central/v/com.elveum/container.svg?label=Maven%20Central)](https://uandcode.com/sh/container)
4+
![API](https://img.shields.io/badge/API-23%2B-brightgreen.svg?style=flat)
5+
[![License: Apache 2](https://img.shields.io/github/license/romychab/container)](LICENSE)
6+
[![PR Check](https://github.com/romychab/container/actions/workflows/pr-check.yml/badge.svg)](https://github.com/romychab/container/actions/workflows/pr-check.yml)
7+
[![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/romychab/90ef83eacc2f4ce17e4c53c8bb255295/raw/container-coverage.json)](https://github.com/romychab/hilt-autobind/actions/workflows/publish.yml)
8+
[![Publish](https://github.com/romychab/container/actions/workflows/publish.yml/badge.svg)](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

Comments
 (0)