Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The full documentation is available [here](https://docs.uandcode.com/container).
Add the following line to your `build.gradle` file:

```
implementation "com.elveum:container:3.3.1"
implementation "com.elveum:container:3.3.2"
```

## Core Concepts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,6 @@ class CatDetailsRepository @Inject constructor(
return catEvents
}

fun invalidate(catId: Long) {
store.invalidateAsync(catId)
}

private suspend fun optimisticUpdate(id: Long, name: String) {
store.optimisticUpdate(id) { old ->
val entity = old.copy(cat = old.cat.copy(name = name))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import com.elveum.container.subject.paging.PageState
import com.elveum.store.load.StoreResult
import com.elveum.store.load.isBackgroundLoading
import com.elveum.store.load.nextPageState
import com.elveum.store.load.onItemRendered

@Composable
fun PaginationArgsScreen() = DemoScaffold(
Expand Down Expand Up @@ -82,7 +83,7 @@ fun PaginationArgsScreen() = DemoScaffold(
key = { _, photo -> photo.id },
) { index, photo ->
LaunchedEffect(index) {
viewModel.onItemRendered(index)
finalResult.onItemRendered(index)
}
PhotoCard(photo = photo)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,4 @@ class PaginationArgsViewModel @Inject constructor(
repository.toggleCategory(category)
}

fun onItemRendered(index: Int) {
repository.onItemRendered(index)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@ class PhotoRepository @Inject constructor(
store.submitQueryAsync(newFilter)
}

fun onItemRendered(index: Int) {
store.onItemRendered(index)
}

data class Photo(
val id: Int,
val title: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import coil.compose.AsyncImage
import com.elveum.container.subject.paging.PageState
import com.elveum.store.demo.feature.examples.store_paged.pagination_basic.PhotoRepository.Photo
import com.elveum.store.demo.ui.components.DemoScaffold
import com.elveum.store.demo.ui.theme.Dimens
import com.elveum.container.subject.paging.PageState
import com.elveum.container.subject.paging.onItemRendered
import com.elveum.store.load.StoreResult
import com.elveum.store.load.nextPageState
import com.elveum.store.load.onItemRendered

@Composable
fun BasicPaginationScreen() = DemoScaffold(
Expand Down Expand Up @@ -74,7 +74,7 @@ fun BasicPaginationScreen() = DemoScaffold(
key = { _, photo -> photo.id },
) { index, photo ->
LaunchedEffect(index) {
viewModel.onItemRendered(index)
finalResult.onItemRendered(index)
}
PhotoCard(photo = photo)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,4 @@ class BasicPaginationViewModel @Inject constructor(
.getPhotos()
.stateIn(StoreResult.Loading)

fun onItemRendered(index: Int) {
repository.onItemRendered(index)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ class PhotoRepository @Inject constructor(

fun getPhotos(): Flow<StoreResult<List<Photo>>> = store.observe()

fun onItemRendered(index: Int) {
store.onItemRendered(index)
}

data class Photo(
val id: Int,
val title: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.elveum.store.demo.feature.examples.store_paged.pagination_statuses

import com.elveum.store.StoreFactory
import com.elveum.store.load.LoadRequest
import com.elveum.store.load.StoreResult
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
Expand All @@ -17,10 +16,6 @@ class BookRepository @Inject constructor(

fun getBooks(): Flow<StoreResult<List<Book>>> = store.observe()

fun onItemRendered(index: Int) {
store.onItemRendered(index)
}

data class Book(
val id: Int,
val title: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import com.elveum.store.load.StoreResult
import com.elveum.store.load.invalidate
import com.elveum.store.load.isBackgroundLoading
import com.elveum.store.load.nextPageState
import com.elveum.store.load.onItemRendered

@Composable
fun PaginationStatusesScreen() {
Expand Down Expand Up @@ -116,7 +117,7 @@ fun PaginationStatusesScreen() {
key = { _, book -> book.id },
) { index, book ->
LaunchedEffect(index) {
viewModel.onItemRendered(index)
finalResult.onItemRendered(index)
}
BookCard(book = book)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.elveum.store.demo.feature.examples.store_paged.pagination_statuses

import com.elveum.container.reducer.stateIn
import com.elveum.store.demo.errors.ErrorFlagRepository
import com.elveum.store.demo.ui.AbstractViewModel
import com.elveum.container.reducer.stateIn
import com.elveum.store.load.StoreResult
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.StateFlow
Expand All @@ -24,8 +24,4 @@ class PaginationStatusesViewModel @Inject constructor(
errorFlagRepository.toggleErrorFlag()
}

fun onItemRendered(index: Int) {
bookRepository.onItemRendered(index)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,23 @@ package com.elveum.store.demo.feature.examples.store_simple.basic
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.elveum.store.demo.ui.components.DemoScaffold
import com.elveum.store.demo.ui.components.EditableField
import com.elveum.store.demo.ui.components.Heading
import com.elveum.store.load.StoreResult
import com.elveum.store.load.invalidate

@Composable
fun SimpleStoreBasicsScreen() = DemoScaffold(
Expand Down Expand Up @@ -93,7 +78,7 @@ fun SimpleStoreBasicsScreen() = DemoScaffold(
onNewValue = viewModel::updateBio,
)

Button(onClick = viewModel::reload) {
Button(onClick = userProfile::invalidate) {
Text("Reload")
}
Spacer(Modifier.weight(1f))
Expand All @@ -102,7 +87,7 @@ fun SimpleStoreBasicsScreen() = DemoScaffold(
Spacer(Modifier.weight(1f))
Text("${userProfile.exception.message}")
Button(
onClick = viewModel::reload,
onClick = userProfile::invalidate,
) {
Text("Try Again")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@ class SimpleStoreBasicsViewModel @Inject constructor(
errorFlagRepository.toggleErrorFlag()
}

fun reload() {
userProfileRepository.reload()
}

fun updateAge(age: Int) {
currentUserProfile?.copy(age = age)?.let(::updateProfile)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ class UserProfileRepository @Inject constructor(
return store.observe()
}

fun reload() {
store.invalidateAsync()
}

suspend fun update(newProfile: UserProfile) {
store.optimisticUpdate {
emit(newProfile)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ fun CombinedSimpleStoreScreen() {
DemoAction(
icon = Icons.Default.Refresh,
state = if (isRefreshInProgress) DemoActionState.Loading else DemoActionState.Default,
onClick = { state.images.invalidate(LoadConfig.SilentLoading) },
onClick = { state.images.invalidate() },
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ class GalleryRepository @Inject constructor(
.map {isKeepContentOnError ->
LoadRequest.builder().run {
if (isKeepContentOnError) {
keepContentOnLoadAndError()
keepContentOnLoadAndError().keepContentOnQueryAndError()
} else {
keepContentOnLoad()
keepContentOnLoad().keepContentOnQuery()
}
}.build()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.elveum.container.subject.paging.internal

import com.elveum.container.BackgroundLoadMetadata
import com.elveum.container.BackgroundLoadState
import com.elveum.container.Container
import com.elveum.container.ContainerMetadata
import com.elveum.container.EmptyMetadata
Expand Down Expand Up @@ -164,6 +165,7 @@ internal class PageRecordsState<Key, T>(
val outputList = outputListSnapshot()

val bgLoadMetadata = containers.firstOrNull()?.metadata?.get<BackgroundLoadMetadata>()
?: BackgroundLoadMetadata(BackgroundLoadState.Idle)
val finalMetadata = if (config.emitMetadata) {
OnItemRenderedCallbackMetadata(onItemRenderedRef) +
NextPageStateMetadata(pageState) +
Expand Down
2 changes: 1 addition & 1 deletion gradle-public.properties
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ android.nonTransitiveRClass=true

# Maven Central publishing coordinates
GROUP=com.elveum
VERSION_NAME=3.3.1
VERSION_NAME=3.3.2

# POM metadata
POM_DESCRIPTION=A library for simplifying state management and data loading in Android applications
Expand Down
2 changes: 1 addition & 1 deletion skills/container-store/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ discovers skills (e.g. `~/.agents/skills/` for Codex), or paste
| `references/api.md` | Every public import, `StoreResult` / `LoadRequest` semantics, typical operations |
| `references/patterns.md` | Layer-by-layer patterns: data sources, repositories, ViewModels, Compose, DI, scoping |

The skill matches library version `3.3.1`.
The skill matches library version `3.3.2`.
8 changes: 6 additions & 2 deletions skills/container-store/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
name: container-store
description: Use when writing, updating, or reviewing any code that references Store-related symbols (StoreFactory, SimpleStore, KeyedStore, KeyedQueryStore, PagedStore, PagedKeyedStore, PagedQueryStore, SimpleQueryStore, StoreResult, LoadRequest, StoreResultReducer) or when integrating the com.elveum:store Kotlin/Android library. Do NOT inspect or decompile JAR/AAR files to understand this library - all API and usage patterns are documented in references/api.md and references/patterns.md.
metadata:
version: 3.3.2
---

# Container Store Library
Expand All @@ -22,13 +24,13 @@ directly - no decompilation or dependency tree inspection needed.

## Dependency Setup

Maven coordinates: `com.elveum:store:3.3.1` (transitively brings
Maven coordinates: `com.elveum:store:3.3.2` (transitively brings
`com.elveum:container`, whose types are part of the public API).

```toml
# gradle/libs.versions.toml
[versions]
store = "3.3.1"
store = "3.3.2"
[libraries]
store = { module = "com.elveum:store", version.ref = "store" }
```
Expand Down Expand Up @@ -105,6 +107,7 @@ still appropriate.
| Reload from the UI (try-again / pull-to-refresh) — PREFERRED | `result.invalidate()` on the rendered `StoreResult`; reloads the origin store with no ViewModel/repository plumbing |
| Pull-to-refresh (keep content visible) | Observe with `LoadRequest.Silent`, then `result.invalidate()`; progress shows via `result.isBackgroundLoading()` |
| Try-again (reload showing `Loading`) | `result.invalidate()` on the failed result (default request re-shows `Loading`) |
| Keep content per reload trigger (invalidate vs query change) | `LoadRequest.builder().keepContentOnLoad()` (invalidate) and/or `.keepContentOnQuery()` (query change), then `.build()`; each unset trigger shows `Loading`. `LoadRequest.Silent` = both (see api.md) |
| Reload without a result at hand (old way, still valid) | `store.invalidateAsync()` / `store.invalidate()` (key variants for keyed) |
| Replace cached result outright | `store.updateWith(StoreResult.Loaded(new))` / `store.updateWith(key, ...)` |
| Read-modify-write loaded value | `store.updateIfSuccess { old -> new }` / `store.updateIfSuccess(key) { old -> new }` (no-op unless `Loaded`) |
Expand All @@ -117,6 +120,7 @@ still appropriate.
| Pagination: next-page status | `result.nextPageState` (`Idle`/`Pending`/`Error(retry)`) |
| React while store is observed (connect to other stores/events) | `.whenActive { ... }` chained after `build`; `this` is the store, block runs while observed & is cancelled on cache release (see patterns.md "Relations between stores") |
| Attach/read custom result flags (metadata) | `PagedList(items, nextKey, metadata = MyMeta(...))` / `emit(v, metadata = MyMeta(...))`; read `result.metadata.get<MyMeta>()` (see api.md) |
| Strip metadata for tests / equality (`assertEquals`) | `result.raw()` → same `Loading`/`Loaded`/`Failed` with all metadata dropped, so results compare by value/exception only |

Cache behavior (all stores): lazy first fetch, one shared in-memory
cache, released after the last observer unsubscribes plus
Expand Down
16 changes: 14 additions & 2 deletions skills/container-store/references/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import com.elveum.store.load.filterLoaded // Flow<StoreResult<T>>.fi
import com.elveum.store.load.toStoreResult // Container<T>.toStoreResult(): StoreResult<T> (interop)
import com.elveum.store.load.toContainer // StoreResult<T>.toContainer(): Container<T> (interop)
import com.elveum.store.load.withMetadataFrom // StoreResult<T>.withMetadataFrom(origin): merge metadata
import com.elveum.store.load.raw // StoreResult<T>.raw(): strip ALL metadata (for testing/equality)
import com.elveum.store.load.isLoaded // smart-cast contract
import com.elveum.store.load.isFailed // smart-cast contract
import com.elveum.store.load.isCompleted
Expand Down Expand Up @@ -235,6 +236,17 @@ val flags = result.metadata.get<PagingFlagsMetadata>() // PagingFlagsMetadata?
if (result.metadata.hasNextPage) { /* ... */ } // via the typed accessor
```

**Stripping metadata for tests** — because metadata rides along on every
`StoreResult`, two results with the same value/exception but different metadata
are **not** equal. When asserting in tests, call `result.raw()` to drop all
metadata and compare only the value/exception:

```kotlin
assertEquals(StoreResult.Loaded(expectedUser), store.get().raw())
```

`raw()` returns `Loading` unchanged, and `Loaded`/`Failed` with `EmptyMetadata`.

Combine several entries with `+` (a same-typed entry on the right replaces the
one on the left): `PagingFlagsMetadata(true) + EtagMetadata(tag)`. Metadata is
propagated from the loader/`PagedList` to the emitted `StoreResult`, so it is the
Expand All @@ -257,8 +269,8 @@ of always meaning `LoadRequest.Default`:
| Request | Effect |
|---------|--------|
| `LoadRequest.Default` | Fetch only if not cached; observers see `Loading` during the load |
| `LoadRequest.Silent` | Keep currently shown content while reloading (pull-to-refresh); progress visible via `isBackgroundLoading()` |
| `LoadRequest.builder()` | Custom: `freshMode()` (skip caches), `offlineMode()` (cache only; emits `NoCachedDataException` if empty), `keepContentOnLoad(replaceErrorsOnReload = true)`, `keepContentOnLoadAndError(replaceErrorsOnReload = true)`, then `build()`. `replaceErrorsOnReload = false` keeps even a current error visible while reloading |
| `LoadRequest.Silent` | Keep currently shown content while reloading, both on invalidate **and** on query change (pull-to-refresh); progress visible via `isBackgroundLoading()`. `= builder().keepContentOnLoad().keepContentOnQuery().build()` |
| `LoadRequest.builder()` | Custom: `freshMode()` (skip caches), `offlineMode()` (cache only; emits `NoCachedDataException` if empty), then `build()`. Keep-content is set **per reload trigger**: `keepContentOnLoad` / `keepContentOnLoadAndError` for invalidation (`invalidate`/`result.invalidate()`), `keepContentOnQuery` / `keepContentOnQueryAndError` for query changes (`submitQuery`/query flow). Each defaults to showing `Loading` if its option is not set, so `keepContentOnLoad()` alone reloads silently but still shows `Loading` on a query change. Each option takes `replaceErrorsOnReload`/`replaceErrorsOnQuery = true`; pass `false` to also keep a current error visible while reloading. Each family may be set at most once (enforced by the fluent builder type) |

### Store behaviour (all types)

Expand Down
9 changes: 7 additions & 2 deletions store/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ the hood.
Add the following line to your `build.gradle` file:

```
implementation "com.elveum:store:3.3.1"
implementation "com.elveum:store:3.3.2"
```

The `store` artifact depends on `com.elveum:container`, so the Container
Expand Down Expand Up @@ -412,7 +412,12 @@ reload, and every observer keeps receiving data according to the request
**it** subscribed with via `observe(...)` (or the builder default).

The `LoadRequest.builder()` also supports `freshMode()` (skip caches, force
a remote fetch) and `offlineMode()` (use only cached data). See
a remote fetch) and `offlineMode()` (use only cached data). Keep-content
behaviour is configured per reload trigger: `keepContentOnLoad` /
`keepContentOnLoadAndError` apply to invalidation, while `keepContentOnQuery`
/ `keepContentOnQueryAndError` apply to query changes - so a single request can
reload silently on `invalidate()` yet show a `Loading` state when the query
changes. `LoadRequest.Silent` keeps content on both. See
[Load Requests](docs/load-requests.md).

## Consuming Stores in ViewModels
Expand Down
Loading
Loading