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

3.6 KiB

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

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:

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.