Files
amethyst/.claude/skills/feed-patterns/references/viewmodel-base-classes.md
T
Claude 60edd473c7 Refreshes .claude/ skill library: fixes stale refs, adds 4 new skills
- Updates CLAUDE.md tech stack to current versions (Compose 1.10.3, Kotlin 2.3.20).
- Reframes kotlin-multiplatform iOS as mature; adds secp256k1-kmp 0.23.0 references.
- Updates desktop-expert Main.kt references (code grew from ~270 to 1341 lines and
  NavigationRail moved to ui/deck/SinglePaneLayout.kt); replaces obsolete
  "hardcoded ctrl = true" anti-pattern note with accurate isMacOS branching.
- Removes compose-desktop.md (superseded by desktop-expert/).
- Adds nostr-expert references: nip19-bech32, event-factory, crypto-and-encryption,
  large-cache. Adds kotlin-expert/common-utilities, compose-expert/rich-text-parsing,
  android-expert/image-loading.
- New skills: account-state (Account + LocalCache), relay-client (subscriptions,
  filter assemblers, preloaders), feed-patterns (FeedFilter + FeedViewModel family),
  auth-signers (NostrSigner across internal / NIP-46 / NIP-55).
2026-04-21 21:00:45 +00:00

91 lines
3.6 KiB
Markdown

# ViewModel Base Classes
Inheritance tree for the shared feed ViewModels in `commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/`.
## Tree
```
androidx.lifecycle.ViewModel
InvalidatableContent (interface)
FeedViewModel(localFilter: FeedFilter<Note>, cacheProvider: ICacheProvider)
├── ListChangeFeedViewModel (list membership changes often)
│ │
│ └── (concrete bookmark / list feeds)
├── ChatroomFeedViewModel (DM thread)
└── MarmotGroupFeedViewModel (NIP-29 group chat)
```
Tangentially related (same folder, not in the tree):
- `LiveStreamTopZappersViewModel.kt` — sidebar state for live streams.
- `SearchBarState.kt` — search input + suggestions.
- `ChatNewMessageState.kt` — composer state for a new DM.
- `thread/*` — thread ViewModels (not technically feeds but share wiring).
## `FeedViewModel`
```kotlin
abstract class FeedViewModel(
localFilter: FeedFilter<Note>,
val cacheProvider: ICacheProvider,
) : ViewModel(), InvalidatableContent {
val feedState = FeedContentState(localFilter, viewModelScope, cacheProvider)
fun invalidateAll() // full recompute
fun invalidateInsertData(newNotes: Set<Note>) // additive path
fun invalidateReplace(replaced: Set<Note>) // replaceable/addressable update
}
```
`FeedContentState` is the thing the UI collects:
- `feedContent: StateFlow<FeedState>` — the actual list, loading flag, paging state.
- Runs the `localFilter.feed()` on a background dispatcher.
- Debounces consecutive invalidations so bursts of relay frames don't thrash the filter.
## `ListChangeFeedViewModel`
Extends `FeedViewModel`. Override point:
```kotlin
abstract class ListChangeFeedViewModel(...) : FeedViewModel(...) {
// Automatically re-invalidates on Account list-flow changes
abstract fun dependencyList(): List<Flow<*>>
}
```
Used for bookmarks, mutes, and custom `NIP-51` lists — anything whose membership is decided by an `Account` StateFlow.
## `ChatroomFeedViewModel`
Wraps filter + relay subscription + typing-indicator state for a single DM thread. Use directly for chat screens; don't reimplement per-thread.
## `MarmotGroupFeedViewModel`
NIP-29 (marmot variant) group feed. Adds group membership / moderator state on top of the base feed.
## When to Extend vs Reuse
- **Just a new filter** → instantiate `FeedViewModel` with your filter; no new class needed.
- **New invalidation signal** → subclass and override `init` to add collectors.
- **Entirely new paging model** (infinite scroll, server-assisted paging) → subclass with a custom `FeedContentState`.
- **Non-feed state** (search, composer) → don't use `FeedViewModel` at all; see `SearchBarState.kt` / `ChatNewMessageState.kt` for narrow-state patterns.
## Platform Wrapping
On Android, feed ViewModels are created via `viewModel { HashtagFeedViewModel(...) }` in the composable. On Desktop, they're instantiated directly and stored in a `WorkspaceManager` column (see `desktopApp/.../ui/deck/WorkspaceManager.kt`). The ViewModel class itself is KMP-friendly.
## Gotchas
- **Multiple subscribers to `feedContent`** are fine — it's a `StateFlow`.
- **ViewModels survive configuration changes on Android** but not on Desktop `key {}` rebuilds — re-instantiate in Desktop's workspace lifecycle.
- **`cacheProvider` is almost always `LocalCache`** but the parameter exists so tests can inject a fixture.
- **Don't call `invalidateAll()` from UI** — it's triggered by the ViewModel's own collectors. Calling it from the composable just causes extra filter runs.