用途
Feed Patterns 是一份关于 Amethyst 中信息流(feed)组合与数据访问层模式的技能说明。它用于以下场景:
- 添加一个展示笔记列表的新界面。
- 修改现有 feed 的过滤、排序或包含规则。
- 排查为何静音、关注或书签变更后 feed 未更新。
- 判断是扩展 ViewModel 还是编写新过滤器。
- 理解 feed 在 Android 与桌面端之间的共享边界。
Amethyst 的 feed 抽象包含两部分:一个 FeedFilter 决定哪些笔记属于该列表,以及一个 FeedViewModel 将当前状态以响应式方式暴露给 UI。首页、个人资料、标签、书签、通知、私信等每一个可滚动列表,都是该模式的变体。
主要能力
共享(commons)的 ViewModel:
FeedViewModel.kt:抽象类FeedViewModel(localFilter, cacheProvider)。持有 FeedContentState,订阅失效信号(来自 Account flows 与 LocalCacheFlow),重新运行过滤器,并向 UI 发出新的 FeedState。ListChangeFeedViewModel.kt:用于成员频繁变化的 feed(如书签)。ChatroomFeedViewModel.kt:私信会话 feed。MarmotGroupFeedViewModel.kt:NIP-29 / marmot 群组 feed。LiveStreamTopZappersViewModel.kt、SearchBarState.kt、ChatNewMessageState.kt:共享同一套基础设施的更窄的非 feed 状态。
共享的过滤器基类(commons):
FeedFilter.kt:抽象类FeedFilter<T> : IFeedFilter<T>。提供feed(): List<T>(针对缓存的同步查询)、feedKey(): String(用于缓存的标识)、limit()与loadTop()。AdditiveFeedFilter.kt:抽象类AdditiveFeedFilter<T> : FeedFilter<T>(), IAdditiveFeedFilter<T>。增加增量更新能力:updateListWith(oldList, newItems)运行applyFilter(newItems),并将接受的项嫁接到现有列表上(重新排序 +take(limit())),无需完整重算。ChangesFlowFilter.kt:用粗略的“状态已变更”信号包装过滤器,让 ViewModel 知道需要重新查询。FeedContentState.kt/FeedState.kt:UI 收集的响应式状态。
Android DAL(在 commons 之上的扩展):
AdditiveComplexFeedFilter.kt:抽象类AdditiveComplexFeedFilter<T, U> : FeedFilter<T>()。类似 AdditiveFeedFilter,但传入项(Set<U>)与列表行(T)类型不同。FilterByListParams.kt:许多过滤器共享的通用参数(顶部导航过滤、排除静音、since/until)。DefaultFeedOrder.kt:Note、Event 与 Card 的标准比较器(createdAt 降序 + id 作为稳定分页的平局决胜)。FeedFilters.kt/ChangesFlowFilter.kt:向后兼容的类型别名,重新导出 commons 类;不要在此添加逻辑。
具体过滤器(首页、标签、个人资料、书签、通知、社区等)位于 amethyst/.../ui/screen/loggedIn/*/ 下的功能 dal/ 子文件夹中,每个都继承 FeedFilter、AdditiveFeedFilter 或 AdditiveComplexFeedFilter。桌面端在 desktopApp/.../feeds/DesktopFeedFilters.kt 中有自己的实现。
使用方式
新增一个 Feed:
- 定义过滤器。继承
AdditiveFeedFilter<Note>(若无关增量则用普通FeedFilter<Note>;若传入项与列表行类型不同则用AdditiveComplexFeedFilter<T, U>)。实现:feedKey():稳定标识(如标签名、账户公钥)。feed():对 LocalCache / Account 状态进行同步扫描,产出有序列表。limit():分页提示。- 若为增量式:实现
applyFilter(collection: Set<Note>): Set<Note>与sort(collection: Set<Note>): List<Note>。
- 选择或编写 ViewModel。若 feed 成员经常变动(书签、通知),继承 ListChangeFeedViewModel;否则用 FeedViewModel。
- 连接失效通知。ViewModel 必须观察正确的 Account flows 与 LocalCacheFlow,以便状态变化时重新查询。
- 渲染。在 composable 中收集
viewModel.feedState.feedContent,用LazyColumn { items(..., key = { it.id }) { NoteCompose(it) } }渲染。 - 订阅中继。多数 feed 还需要一个 Subscribable 来获取历史事件。参见 relay-client 技能。
过滤器共享(Android 与桌面端):
- 过滤器基类(FeedFilter、AdditiveFeedFilter、ChangesFlowFilter)与 feed 状态(FeedContentState)位于
commons/.../ui/feeds/,共享。ViewModel 位于commons/.../viewmodels/,共享。 - 具体过滤器是各平台本地的:Android 的在
amethyst/.../ui/screen/loggedIn/*/dal/,桌面端的在desktopApp/.../feeds/。amethyst/.../ui/dal/保留 Android 专属辅助(AdditiveComplexFeedFilter、FilterByListParams、DefaultFeedOrder)以及向后兼容类型别名。 - 移植 feed 时,仅当两端需要完全相同的包含规则时才共享具体过滤器。
适用场景
适用于 Amethyst 项目中任何与信息流相关的开发任务,包括添加新列表界面、调整过滤与排序规则、排查 feed 更新问题,以及处理 Android 与桌面端的 feed 共享与移植。
限制与注意事项
- 永远不要在 composable 中扫描 LocalCache。始终通过 FeedFilter + FeedViewModel,它在后台调度器上执行并会对失效通知做防抖。
feedKey()用作缓存键。两个语义不同的 feed 必须产生不同的键,否则状态会交叉污染。- 增量更新必须与完整重算保持一致。若
applyFilter接受了feed()不会包含的笔记,用户体验会发生偏移。 - 分页不是免费的——应使用
limit()与 FilterByListParams 中的 since/until,而不是裁剪一次巨大扫描。 - 通知 feed 比较特殊——它会检查关注/静音状态(
account.kind3FollowList.flow、account.hiddenUsers)与 LocalCache 删除记录,以隐藏被静音/删除的内容;应始终走 FilterByListParams 的排除路径,而不是事后过滤。
参考资料
references/feed-filter-composition.md:添加 feed 的分步说明。references/viewmodel-base-classes.md:FeedViewModel 家族的继承图。
相关补充:account-state(数据存放位置)、relay-client(如何订阅)、compose-expert(如何渲染)。