ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

KomiStore 最近浏览(Recently Viewed)功能解析:基于 SeenReposRepository 的本地浏览历史实现

KomiStore 最近浏览(Recently Viewed)功能解析:基于 SeenReposRepository 的本地浏览历史实现 移动开发桌面应用【免费下载链接】komi-store A free, open-source app store for developers releases on GitHub, Codeberg Forgejo — browse, discover, and install apps with one click. Formerly GitHub Store.项目地址https://gitcode.com/gh_mirrors/git/komi-store点击查看免费下载导读本文以 feature/recently-viewed/CLAUDE.md 为骨架深入解析 KomiStoreGitHub Store 的继承者中最近浏览Recently Viewed功能的完整实现。该功能是纯 Presentation 层模块仓库的 Details 页面一旦被打开即通过SeenReposRepository把该仓库写入本地浏览历史同一份数据同时驱动最近浏览页面与 Home/Search 的Hide seen过滤。读完本文你将掌握该模块的目录结构、ViewModel 状态流、UI 组件、导航接线以及底层 Room 表与数据写入调用链。一、功能定位本地浏览历史不做网络请求Recently Viewed是 KomiStore 中的一份本地最近浏览历史用户每次打开任意仓库的 Details 页面该仓库就会被记录用户可以在个人资料页进入最近浏览列表快速找回之前看过的仓库。根据 feature/recently-viewed/CLAUDE.md该功能有两个关键设计约束Presentation-only模块内部不包含 data 层与 domain 层所有数据读写全部委托给 core 模块的SeenReposRepository无网络浏览历史完全存储在本机不产生任何网络请求也不依赖登录态。SeenReposRepository是核心中的单一数据源——同一个 pipeline既供最近浏览页面消费getAllSeenRepos又供 Home / Search 的Hide seen过滤消费getAllSeenRepoIds从而保证两处功能看到的已看过集合永远一致。二、模块结构6 个文件的精简布局原文档给出了模块目录骨架实际源码与之一一对应feature/recently-viewed/presentation/ └── src/commonMain/kotlin/zed/rainxch/recentlyviewed/presentation/ ├── RecentlyViewedViewModel.kt // 状态流 Action 分发 ├── RecentlyViewedState.kt // UI 状态 ├── RecentlyViewedAction.kt // 用户意图 sealed interface ├── RecentlyViewedRoot.kt // Root ScreenCompose 页面 ├── model/RecentlyViewedRepo.kt // UI 模型 ├── mappers/RecentlyViewedRepoMapper.kt // SeenRepo - UI 模型 └── components/RecentlyViewedItem.kt // 列表卡片因为模块只依赖 core 层SeenReposRepository、core.presentation组件库、Res字符串资源全部代码放在commonMain天然支持 Android / Desktop 多平台编译无需任何平台专属代码。三、数据模型与映射从 SeenRepo 到 UI 模型3.1 领域模型 SeenRepoSeenReposRepository定义在 core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/SeenReposRepository.kt接口暴露五个方法interface SeenReposRepository { fun getAllSeenRepoIds(): FlowSetLong // 供 Hide seen 过滤 fun getAllSeenRepos(): FlowListSeenRepo // 供最近浏览列表 suspend fun markAsSeen(repo: GithubRepoSummary) // Details 打开时写入 suspend fun markAsSeen(repoId, repoName, repoOwner, repoOwnerAvatarUrl, repoDescription, primaryLanguage, repoUrl) suspend fun removeFromHistory(repoId: Long) // 删除单条 suspend fun clearAll() // 清空全部 }3.2 UI 模型 RecentlyViewedRepoRecentlyViewedRepomodel/RecentlyViewedRepo.kt是列表卡片所需的全部字段data class RecentlyViewedRepo( val repoId: Long, val repoName: String, val repoOwner: String, val repoOwnerAvatarUrl: String, val repoDescription: String?, val primaryLanguage: String?, val repoUrl: String, val viewedAtFormatted: String, // 由 seenAt 格式化而来 )3.3 映射器mappers/RecentlyViewedRepoMapper.kt 定义了SeenRepo.toRecentlyViewedRepoUi()扩展函数把领域模型逐字段映射为 UI 模型其中时间戳seenAt通过 core 的formatAddedAt工具格式化为人类可读的viewedAtFormatted在卡片上以 查看时间 chip 展示。四、ViewModel响应式状态流与 Action 分发4.1 状态定义RecentlyViewedState.kt 只有两个字段data class RecentlyViewedState( val repositories: ImmutableListRecentlyViewedRepo persistentListOf(), val isLoading: Boolean false, )使用kotlinx.collections.immutable的ImmutableList保证 Compose 状态不可变、列表稳定。4.2 Action 定义RecentlyViewedAction.kt 是 sealed interface覆盖全部用户意图sealed interface RecentlyViewedAction { data object OnNavigateBackClick : RecentlyViewedAction data class OnRepositoryClick(val repo: RecentlyViewedRepo) : RecentlyViewedAction data class OnDeveloperProfileClick(val username: String) : RecentlyViewedAction data class OnRemoveFromHistory(val repo: RecentlyViewedRepo) : RecentlyViewedAction data object OnClearAllHistory : RecentlyViewedAction }4.3 ViewModel 实现细节RecentlyViewedViewModel.kt 采用标准的单向数据流模式几个实现细节值得注意延迟加载通过hasLoadedInitialData标志配合onStart { loadRecentlyViewed() }只在有订阅者时加载一次避免冷启动无谓开销状态共享stateIn(scope, SharingStarted.WhileSubscribed(5_000L), initialValue)—— 订阅停止 5 秒后自动断开上游回收资源后台映射flowOn(Dispatchers.Default)把SeenRepo - UI 模型的映射放到默认调度器避免阻塞主线程写操作OnRemoveFromHistory→seenReposRepository.removeFromHistory(action.repo.repoId)单行删除OnClearAllHistory→seenReposRepository.clearAll()清空整表数据写入后由于getAllSeenRepos()返回FlowRoom 会在表变化时自动重新发射UI 无需手动刷新即可实时更新。注意OnNavigateBackClick、OnRepositoryClick、OnDeveloperProfileClick在 ViewModel 中是空实现Unit——它们属于导航类意图由 Root 层转交 Navigation 处理见第六节。五、UI 组件卡片与自适应网格5.1 列表卡片 RecentlyViewedItemcomponents/RecentlyViewedItem.kt 渲染每张卡片从上到下依次为所有者行28dp 圆形头像GitHubStoreImage加载repoOwnerAvatarUrl 所有者名整行可点击跳转开发者主页仓库信息行仓库名Title 角色、单行省略 描述Body、最多 2 行右侧是KomiIconButtonIcons.Outlined.ClosecontentDescription 使用Res.string.recently_viewed_remove_cd点击触发OnRemoveFromHistory标签行FlowRow中展示两个KomiChip——主语言 chipIcons.Default.Code仅当primaryLanguage非空与查看时间 chipIcons.Default.Schedule显示viewedAtFormatted。卡片整体包裹在KomiSurface(onClick onItemClick)中点击卡片任意区域即进入 Details 页面。5.2 页面 RecentlyViewedScreenRecentlyViewedRoot.kt 内包含两层RecentlyViewedRoot通过koinViewModel()获取 ViewModelcollectAsStateWithLifecycle()订阅状态并负责把导航类 Action 转交给上层回调onNavigateBack/onNavigateToDetails/onNavigateToDeveloperProfile其余 Action 交给viewModel.onAction(action)RecentlyViewedScreen纯 UI使用KomiScaffoldKomiTopBarCompact 尺寸标题Res.string.recently_viewedleading 为返回箭头Icons.AutoMirrored.Filled.ArrowBack搭建页面骨架。列表采用LazyVerticalStaggeredGrid自适应瀑布流columns StaggeredGridCells.Adaptive(350.dp), // 每列至少 350dp自动换列 verticalItemSpacing 12.dp, horizontalArrangement Arrangement.spacedBy(12.dp), contentPadding PaddingValues(horizontal 8.dp, vertical 12.dp),配合items(state.repositories, key { it.repoId })稳定复用、Modifier.animateItem()平滑增删动画。桌面端额外叠加了两层能力ScrollbarContainer滚动条受LocalScrollbarEnabled控制与arrowKeyScroll(gridState, autoFocus true)方向键导航state.isLoading时居中显示KomiCircularProgress。六、导航接线GithubStoreGraph.RecentlyViewedScreen6.1 路由定义路由目标定义在 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt#L56-L57Serializable data object RecentlyViewedScreen : GithubStoreGraph采用 Navigation Compose 类型安全路由Serializablesealed class。6.2 页面注册与回调在 AppNavigation.kt 中注册composableGithubStoreGraph.RecentlyViewedScreen并接线三个导航回调onNavigateBack→navController.navigateUp()onNavigateToDetails { repoId - navController.navigate(GithubStoreGraph.DetailsScreen(repositoryId repoId)) }onNavigateToDeveloperProfile { username - navController.navigate(GithubStoreGraph.DeveloperProfileScreen(username)) }。6.3 入口个人资料页最近浏览的入口位于 Profile 页面。在 ProfileGraph.kt 中ProfileRoot的onNavigateToRecentlyViewed回调执行navController.navigate(GithubStoreGraph.RecentlyViewedScreen)与 Starred / Favourites 等列表页并列。此外桌面端工具栏的深链处理 HandleDesktopToolbarDeeplinks.kt 也可直接跳转到该页面。七、底层实现Room 表与数据写入调用链7.1 数据表与 DAO浏览历史最终落在 Room 的seen_repos表实体 SeenRepoEntity.ktEntity(tableName seen_repos) data class SeenRepoEntity( PrimaryKey val repoId: Long, val repoName: String, val repoOwner: String, val repoOwnerAvatarUrl: String, val repoDescription: String?, val primaryLanguage: String?, val repoUrl: String, val seenAt: Long, // 访问时间戳毫秒 )DAO SeenRepoDao.kt 定义了核心 SQLQuery(SELECT repoId FROM seen_repos) fun getAllSeenRepoIds(): FlowListLong // 供过滤 Query(SELECT * FROM seen_repos ORDER BY seenAt DESC) fun getAllSeenRepos(): FlowListSeenRepoEntity // 按时间倒序 Insert(onConflict OnConflictStrategy.REPLACE) suspend fun insert(entity: SeenRepoEntity) // repoId 冲突时覆盖 Query(DELETE FROM seen_repos WHERE repoId :repoId) suspend fun deleteById(repoId: Long) Query(DELETE FROM seen_repos) suspend fun clearAll()两个细节值得注意OnConflictStrategy.REPLACE同一仓库被重复查看时按repoId覆盖旧行seenAt会更新为最新访问时间System.currentTimeMillis()并因ORDER BY seenAt DESC自动上浮到列表顶部——这就是最近浏览排序的实现方式响应式DAO 全部返回Flow配合 Room 的观察机制任何 insert / delete / clearAll 都会立即触发 UI 刷新无需手动通知。7.2 Repository 实现SeenReposRepositoryImpl.kt 是对 DAO 的封装markAsSeen(repo: GithubRepoSummary)重载会自动从仓库摘要中取出 id / name / owner / avatar / description / language / url 并写入seenAt System.currentTimeMillis()removeFromHistory与clearAll分别对应deleteById与clearAll。7.3 写入时机DetailsViewModel原文档指出访问时间戳来自 Details 页面打开时的seenRepoDao.insert(...)调用。实际调用链在 DetailsViewModel.kt加载仓库成功后立即执行launch { seenReposRepository.markAsSeen(repo) }也就是说只要 Details 页面成功加载了一个仓库该仓库就会进入浏览历史无需任何额外用户操作。并且DetailsViewModel的seenReposRepository依赖同样注册在 ViewModelsModule.kt 中。7.4 Hide seen过滤的同一数据源Home 与 Search 通过getAllSeenRepoIds()复用同一份数据实现跳过已查看仓库HomeViewModel.ktobserveSeenRepos()收集 ID 集合后触发rebuild()SearchViewModel.kt收集后更新seenRepoIds并给每个 repo 打上isSeen标记该开关由 Tweaks 隐私设置控制对应字符串 Hide repos Ive already viewedstrings.xml键为hide_seen_enabled见 TweaksRepositoryImpl.kt。八、依赖注入为什么没有 data/di原文档特别强调RecentlyViewedViewModel注册在composeApp/.../app/di/ViewModelsModule.kt不存在 data/di。这是因为 recently-viewed 模块本身不创建任何 repository / DAO 实例只是消费 core 层已经注入好的SeenReposRepository。在 ViewModelsModule.kt 中viewModelOf(::RecentlyViewedViewModel)viewModelOf通过 Koin 的构造函数自动装配从容器中解析SeenReposRepository其实现SeenReposRepositoryImpl绑定于 core 的 data/di 模块注入 ViewModel。整个 recently-viewed 模块因此保持纯 Presentation 零自己管理的依赖职责边界非常干净。九、动手验证数据流全景把整条链路串起来可以按以下步骤在本地验证仓库为只读仅用于观察源码与运行打开任意仓库的 Details 页面→ 触发 DetailsViewModel.kt 的markAsSeen数据落库→SeenReposRepositoryImpl调用SeenRepoDao.insert以 REPLACE 策略写入seen_repos表Profile 页进入最近浏览→GithubStoreGraph.RecentlyViewedScreen页面中的RecentlyViewedViewModel通过getAllSeenRepos()拿到按时间倒序的Flow实时刷新→ Room 自动发射新数据RecentlyViewedScreen以自适应瀑布流渲染卡片删除单条→ 卡片右上角Close按钮触发removeFromHistory(repoId)清空全部→clearAll()一键清表联动过滤→ 若开启 Tweaks 的 Hide repos Ive already viewedHome/Search 会通过getAllSeenRepoIds()立即排除这些仓库。结语KomiStore 的 Recently Viewed 模块是一个教科书式的 Compose Multiplatform 单向数据流样例Presentation-only 的职责划分、以SeenReposRepository为单一数据源复用浏览记录、Room 的响应式Flow驱动 UI 自动刷新以及OnConflictStrategy.REPLACE ORDER BY seenAt DESC实现的最近优先排序都是值得复用的工程模式。阅读时建议对照 feature/recently-viewed/CLAUDE.md、SeenReposRepository.kt 与 RecentlyViewedViewModel.kt 三个文件即可完整还原该功能的实现全貌。赞分享移动开发桌面应用【免费下载链接】komi-store A free, open-source app store for developers releases on GitHub, Codeberg Forgejo — browse, discover, and install apps with one click. Formerly GitHub Store.项目地址https://gitcode.com/gh_mirrors/git/komi-store点击查看免费下载相关推荐eShop最近浏览用户浏览历史记录功能eShop最近浏览用户浏览历史记录功能 痛点电商平台的用户行为追踪缺失 在当今竞争激烈的电商环境中用户浏览行为数据是提升转化率的关键。然而许多电商平台忽后端电商前端微服务如何永久保存微信聊天记录解锁你的数字记忆宝库如何永久保存微信聊天记录解锁你的数字记忆宝库 你是否曾经因为手机丢失、系统升级或微信清理而永远失去了珍贵的聊天记录那些与家人的温馨对话、与朋友的深夜畅谈、与nnn历史记录功能终极指南快速访问最近浏览目录的完整教程nnn历史记录功能终极指南快速访问最近浏览目录的完整教程 nnn作为一款高效的终端文件管理器其历史记录功能能够帮助用户快速访问最近浏览的目录极大提升文件导CLI上一篇三步实现umi路由权限控制基于角色的访问控制方案下一篇gpui-kit Global State 实战指南基于 GPUI Global Trait 的跨组件共享状态管理创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表