本文档面向维护者和贡献者,描述当前项目的技术架构、关键模块、数据流和开发约定。用户侧介绍请看 README.md。
Han1meViewer 当前是以 Kotlin + Jetpack Compose 为主的 Android 应用,整体采用 MVVM 风格组织。
核心技术:
- Kotlin 2.3.x,Java 21。
- Android Gradle Plugin 9.2.x。
- Jetpack Compose + Material 3。
- Navigation Compose typed routes。
- ViewModel + StateFlow + SharedFlow。
- Retrofit 3 + OkHttp + kotlinx.serialization converter。
- Jsoup 解析 HTML DOM。
- Room + KSP。
- WorkManager 后台下载和更新任务。
- Coil 3 Compose 图片加载。
- Media3 ExoPlayer、JZVD、MPV Android 播放链路。
- Firebase Analytics、Crashlytics、Performance、Remote Config、Realtime Database。
当前代码仍有少量历史 View/XML 组件,例如 JZVD 自定义 View、部分设置/播放器相关布局和 yenaly_libs 基础库组件,但主业务页面已经以 Compose 为主。
Gradle 模块:
:app:主应用模块,包含 UI、业务状态、网络解析、本地数据库、播放器、下载、更新和资源。:yenaly_libs:项目内公共库,提供基础 Activity、Fragment、ViewModel、Preference、工具类和部分 View 封装。buildSrc:构建辅助逻辑,维护版本号、构建来源、提交 SHA 等 Gradle 侧配置。
主应用关键目录:
app/src/main/java/com/yenaly/han1meviewer/
├── logic/ 数据、网络、解析、数据库、状态
│ ├── network/ Retrofit Service、OkHttp、DNS、Cookie、更新服务
│ ├── model/ 业务模型和解析后的页面模型
│ ├── dao/ Room DAO 和 Database
│ ├── entity/ Room 实体和本地实体
│ ├── state/ WebsiteState、PageLoadingState、VideoLoadingState
│ ├── NetworkRepo.kt 网络仓库入口
│ ├── DatabaseRepo.kt 本地仓库入口
│ └── Parser.kt HTML/JSON 解析入口
├── ui/
│ ├── activity/ MainActivity、登录、Cloudflare、手动 Cookie 页面
│ ├── navigation/ 主导航、设置导航、路由和安全跳转
│ ├── screen/ Compose 页面
│ ├── component/ Compose 组件
│ ├── viewmodel/ 页面 ViewModel
│ ├── view/ 自定义 View 和播放器 View
│ └── theme/ Compose 主题、颜色、尺寸
├── util/ 文件、网络、权限、主题、Cookie、Toast 等工具
├── worker/ 下载 Worker、更新 Worker、下载管理器
└── HanimeApplication.kt 应用入口
UI 层主要由 Compose 页面和组件组成。
关键职责:
- 只消费 ViewModel 暴露的状态。
- 通过回调触发导航和业务动作。
- 不直接访问 Retrofit Service 或 Room DAO。
LazyColumn、LazyRow、LazyVerticalGrid使用稳定 key 时,数据源必须提前保证唯一。
典型文件:
ui/screen/home/HomePageScreen.ktui/screen/search/SearchScreen.ktui/screen/video/VideoIntroductionScreen.ktui/screen/video/VideoRouteHostScreen.ktui/screen/home/download/DownloadScreen.ktui/screen/home/myplaylist/MyPlayListScreen.ktui/component/VideoCardItem.ktui/component/lazy/AnimatedLazy.kt
ViewModel 是页面状态和业务动作的入口。
约定:
- 持久页面状态使用
MutableStateFlow。 - 一次性事件使用
MutableSharedFlow或 UI 回调。 - 不让 Compose 页面直接处理分页合并、去重、登录态判断等业务细节。
- 网络列表分页追加时应按稳定业务键去重,例如
videoCode、listCode、id。 - 登录用户相关页面要先检查
Preferences.isAlreadyLogin和Preferences.savedUserId。
典型文件:
ui/viewmodel/MainViewModel.ktui/viewmodel/SearchViewModel.ktui/viewmodel/VideoViewModel.ktui/viewmodel/CommentViewModel.ktui/viewmodel/DownloadViewModel.ktui/viewmodel/MyPlayListViewModelV2.ktui/viewmodel/OnlineWatchHistoryViewModel.ktui/viewmodel/CreatorCenterViewModel.ktui/viewmodel/UserAccountViewModel.kt
数据层由 NetworkRepo、DatabaseRepo、Parser、Retrofit Service 和 Room DAO 组成。
网络数据流:
ViewModel -> NetworkRepo -> HanimeNetwork Service -> Parser -> WebsiteState/PageLoadingState/VideoLoadingState -> ViewModel
本地数据流:
ViewModel/Worker -> DatabaseRepo -> Room DAO -> Flow / suspend result -> ViewModel/UI
关键文件:
logic/NetworkRepo.ktlogic/DatabaseRepo.ktlogic/Parser.ktlogic/network/HanimeNetwork.ktlogic/network/ServiceCreator.ktlogic/network/service/HanimeBaseService.ktlogic/network/service/HanimeMyListService.ktlogic/network/service/HanimeCommentService.ktlogic/dao/HistoryDatabase.ktlogic/dao/DownloadDatabase.ktlogic/dao/MiscellanyDatabase.kt
项目使用三个主要状态包装类型:
WebsiteState<T>:普通页面或操作状态,包含 Loading、Success、Error。PageLoadingState<T>:分页列表状态,包含 Loading、Success、NoMoreData、Error。VideoLoadingState<T>:视频详情状态,处理视频不存在、解析失败、加载中和成功状态。
使用建议:
- 首页、订阅、账号、修改操作等非分页数据使用
WebsiteState。 - 搜索、收藏、稍后观看、播放列表、在线历史、创作中心列表使用
PageLoadingState。 - 视频详情使用
VideoLoadingState。 - UI 层不要根据异常类型硬编码网络逻辑,异常映射应尽量在
NetworkRepo或 Parser 侧完成。
主导航基于 Navigation Compose typed routes。
关键文件:
ui/navigation/main/MainRoutes.ktui/navigation/main/MainNavHost.ktui/navigation/main/MainNavigationActions.ktui/navigation/NavControllerExt.ktui/navigation/settings/SettingsRoutes.ktui/navigation/settings/SettingsNavHost.kt
路由示例:
@Serializable
data class SearchRoute(
val query: String? = null,
val advancedSearchJson: String? = null,
)
@Serializable
data class VideoRoute(
val videoCode: String,
val localUri: String? = null,
)导航约定:
- 普通跳转使用
navController.navigateSafely(...),避免快速点击重复入栈。 - 页面间只传递路由必要参数,不传大对象。
- 复杂搜索参数通过 JSON 字符串承载,进入
SearchRouteScreen后再灌入SearchViewModel。 - 视频本地文件播放使用
VideoRoute(videoCode = "-1", localUri = uri)这一路径。
搜索入口:
- 首页搜索框进入
SearchRoute(query = keyword)。 - 标签点击进入
SearchRoute(query = tag)。 - 高级搜索进入
SearchRoute(advancedSearchJson = json)。 - 详情页作者区域会同时传入
query = artist.name和高级搜索 JSON,以保证搜索框填充并自动搜索。
SearchScreen 支持:
initialQuery自动填充。- 初始关键词自动搜索。
- 高级搜索条件自动搜索。
- 搜索历史建议。
- 下拉刷新和加载更多。
分页列表去重规则:
- 视频列表按
HanimeInfo.videoCode去重。 - 播放列表按
Playlists.Playlist.listCode去重。 - 上传中/已上传列表按各自
videoCode去重。 - 评论列表使用评论模型提供的
stableKey。
原因:Compose Lazy* 使用 key = { ... } 时,数据源中出现重复 key 会直接抛出:
java.lang.IllegalArgumentException: Key "xxxx" was already used.
因此分页合并应优先在 ViewModel 层处理:
(previous + incoming).distinctBy(HanimeInfo::videoCode)视频页由路由、Host、播放器、Tab 内容和详情内容协作完成。
关键文件:
ui/navigation/main/VideoRoute.ktui/screen/video/VideoRouteHostScreen.ktui/screen/video/VideoRouteContent.ktui/screen/video/VideoShellContent.ktui/screen/video/VideoTabsContent.ktui/screen/video/VideoIntroductionScreen.ktui/screen/video/CommentScreen.ktui/screen/video/ChildCommentScreen.ktui/screen/video/VideoRouteActions.ktui/viewmodel/VideoViewModel.kt
主要状态:
hanimeVideoStateFlow:视频加载状态。hanimeVideoFlow:当前视频数据。videoHostUiStateFlow:Tab、AppBar、PIP、播放器高度、评论角标等 UI 状态。videoIntroUiStateMap:按videoCode缓存详情页滚动位置、系列横向列表位置和已恢复数据。
视频详情加载:
VideoRoute -> VideoViewModel.getHanimeVideo -> NetworkRepo.getHanimeVideo -> Parser.hanimeVideoVer2 -> HanimeVideo
本地播放:
VideoRoute(videoCode = "-1", localUri) -> VideoViewModel.buildLocalPlayInfo -> HanimeVideo(videoUrls = localUri)
作者点击搜索:
VideoIntroductionScreen的ArtistSection触发onOpenArtist。VideoRouteActions.openArtistSearch构造SearchRoute。- 搜索页通过
route.query自动填充作者名并发起搜索。
项目保留了历史 JZVD 播放器封装,同时引入 Media3 和 MPV 能力。
关键文件:
ui/view/video/HJzvdStd.ktui/view/video/HMediaKernel.ktui/view/video/HanimeDataSource.ktui/screen/video/VideoPlayerUi.ktutil/Videos.ktHanimeResolution.kt
播放相关约定:
- 画质信息由
HanimeResolution解析后提供给播放器。 - 播放器 UI 状态不要直接写入网络模型,应通过 ViewModel 或播放器状态对象同步。
- PIP、屏幕方向、播放器高度、滚动禁用等与页面布局相关的状态归
VideoViewModel.VideoHostUiState管理。 - MPV 相关设置归
MpvPlayerSettingsRouteScreen和偏好设置管理。
下载由 UI、ViewModel、下载管理器、Worker 和 Room 共同完成。
关键文件:
ui/screen/home/download/DownloadScreen.ktui/screen/home/download/DownloadingScreen.ktui/screen/home/download/DownloadedScreen.ktui/viewmodel/DownloadViewModel.ktworker/HanimeDownloadManagerV2.ktworker/HanimeDownloadWorker.ktlogic/entity/download/HanimeDownloadEntity.ktlogic/entity/download/DownloadGroupEntity.ktlogic/dao/download/HanimeDownloadDao.ktlogic/dao/DownloadDatabase.kt
下载数据流:
DownloadScreen -> DownloadViewModel -> HanimeDownloadManagerV2 -> WorkManager -> HanimeDownloadWorker -> DownloadDatabase -> Flow -> UI
设计要点:
- 长任务交给 WorkManager,避免 Activity 生命周期影响下载。
- 下载进度落库,UI 通过数据库 Flow 获得刷新。
- 前台服务通知由 Worker 维护。
- 下载目录通过 SAF 相关工具处理,避免直接依赖旧外部存储权限。
- 下载完成后可走本地播放路径进入视频页。
性能注意:
- 下载进度不应过于频繁落库,否则多个任务同时下载时会造成数据库和 UI 刷新压力。
- 更新间隔应在“进度实时性”和“数据库写入频率”之间折中。
Room 主要用于:
- 播放历史。
- 搜索历史。
- 高级搜索历史。
- 下载任务和下载分组。
- 共享/自定义关键 H 帧。
- 健康打卡记录。
关键入口:
logic/DatabaseRepo.ktlogic/dao/HistoryDatabase.ktlogic/dao/DownloadDatabase.ktlogic/dao/MiscellanyDatabase.ktlogic/dao/CheckInRecordDatabase.kt
约定:
- UI 不直接访问 DAO。
- 跨页面复用的数据库操作集中到
DatabaseRepo。 - 高频写入要评估 Flow 回调频率和 UI 刷新成本。
网络层入口:
NetworkRepo:业务方法入口,统一包装状态。HanimeNetwork:Retrofit Service 聚合。ServiceCreator:OkHttp/Retrofit 创建。Parser:HTML/JSON 转领域模型。
拦截器和辅助:
CloudflareInterceptorUserAgentInterceptorSpeedLimitInterceptorUrlLoggingInterceptorHCookieJarHDnsGitHubDnsHProxySelector
异常类型:
CloudFlareBlockedExceptionIPBlockedExceptionHanimeNotFoundExceptionLoginStateExpiredExceptionParseException
开发约定:
- Parser 应尽量容错,目标网站 DOM 改动时返回明确错误。
NetworkRepo应保留CancellationException语义,不吞掉协程取消。- 登录态过期、Cloudflare、IP blocked 等应映射成可被 UI 理解的异常或状态。
- 解析列表时优先保证
videoCode、listCode等业务键非空和稳定。
相关页面:
ui/activity/LoginActivity.ktui/activity/ManualInputCookiesActivity.ktui/activity/CloudflareActivity.ktui/screen/account/AccountScreen.ktui/screen/account/AvatarCropScreen.ktui/viewmodel/UserAccountViewModel.kt
关键点:
- 登录态和 Cookie 由
Preferences、HCookieJar和相关工具共同维护。 - 手动 Cookie 输入用于绕过无法自动登录或 WebView 登录失败的情况。
- Cloudflare 页面和拦截器用于处理访问保护导致的请求失败。
- 账号信息修改、密码修改、头像上传都通过
HanimeMyListService和NetworkRepo包装。
公告系统分为应用侧展示和管理端写入两部分。
关键文件:
ui/viewmodel/MainViewModel.ktui/component/AnnouncementDialog.ktui/screen/home/HomePageScreen.ktlogic/model/Announcement.ktHanimeAnnouncementManagerWebUI/HanimeAnnouncementManager.htmlHanimeAnnouncementManagerWebUI/PermitAdmin.py
应用侧链路:
MainViewModel.loadAnnouncements()从 Firebase Realtime Database 的announcements节点读取公告。- 只有
isActive = true的公告会参与展示。 - 首页通过公告卡片展示公告列表。
- 点击公告后进入
AnnouncementDialog,可查看标题、正文、时间、图片和按钮文案。 - 用户关闭公告后,会记录
last_dismiss_time,24 小时内不会再次自动弹出。
管理端链路:
HanimeAnnouncementManager.html是基于 Firebase Web SDK 的管理页面。- 页面通过
firebaseConfig连接到项目对应的 Realtime Database。 - 页面使用
ADMIN_EMAIL和ADMIN_PASSWORD登录 Firebase Auth。 - 登录成功后读取
announcements节点并渲染为表格。 - 页面支持新增、编辑、启用、停用和删除公告。
管理员权限:
PermitAdmin.py使用 Firebase Admin SDK 给指定 UID 写入isAdmin: true自定义声明。- 该脚本依赖本地
serviceAccountKey.json。 - 运行前需要安装
firebase-admin,并填写目标用户 UID。 - 该自定义声明用于管理端权限校验。
公告字段约定:
title:公告标题。content:公告正文。imageUrl:公告图片地址,可选。positiveText:确认按钮文本,可选。negativeText:取消按钮文本,可选。priority:排序优先级。timestamp:更新时间戳,单位为秒。isActive:是否启用。
实现约定:
- 公告数据应兼容部分字段缺失的情况。
- 管理端更新公告时会刷新
timestamp,便于前端显示最新时间。 - 停用公告时只更新
isActive,删除则直接移除节点。 - 后续如果新增公告字段,需要同步更新
Announcement模型、管理端表单和展示弹窗。
设置页采用 Compose + typed routes。
关键文件:
ui/navigation/settings/SettingsRoutes.ktui/navigation/settings/SettingsNavHost.ktui/navigation/settings/HomeSettingsRoute.ktui/screen/settings/HomeSettingsScreen.ktui/screen/settings/PlayerSettingsScreen.ktui/screen/settings/MpvPlayerSettingsScreen.ktui/screen/settings/DownloadSettingsScreen.ktui/screen/settings/NetworkSettingsScreen.ktui/screen/settings/HKeyframeSettingsScreen.kt
偏好配置入口:
Preferences.ktSearchGridColumnsConfig.ktHorizontalCardCountConfig.ktVideoCoverSize.kt
约定:
- 新设置项应集中放在
Preferences或明确的配置类里。 - UI 设置页只负责展示和修改偏好,不直接散落业务逻辑。
- 与播放器、网络、下载有关的设置要同步检查对应模块读取点。
共享关键 H 帧仍使用 assets 中的 JSON 文件作为输入。
关键文件:
app/src/main/assets/h_keyframes/app/src/main/assets/h_keyframes/README.mdlogic/entity/HKeyframeEntity.ktlogic/dao/HKeyframeDao.ktlogic/DatabaseRepo.ktui/screen/settings/HKeyframesScreen.ktui/screen/settings/SharedHKeyframesScreen.kt
读取思路:
assets/h_keyframes/*.json -> Json.decodeFromStream<HKeyframeEntity> -> sort/group/flatten -> UI
分组扁平化示意:
entities
.sortedWith(compareBy<HKeyframeEntity> { it.group }.thenBy { it.episode })
.groupBy { it.group ?: "???" }
.flatMap { (group, entities) ->
listOf(HKeyframeHeader(title = group, attached = entities)) + entities
}这种结构避免了在 assets 里维护复杂目录索引,也便于贡献者直接提交单个 JSON 文件。
项目提供了 AnimatedLazy.kt 封装:
LazyColumnLazyRowLazyVerticalGridAnimatedLazyListScopeAnimatedLazyGridScope
目的:
- 尽量兼容原生 Lazy API 的常用调用方式。
- 为列表 item 提供轻量入场动画。
- 在全项目保持较一致的列表观感。
注意:
- 它不会替代 Compose 对 key 唯一性的要求。
- 使用
items(list, key = { ... })时,调用方仍必须保证 key 唯一。 - 网络分页列表优先在 ViewModel 层去重,不建议在 UI 层临时过滤后再展示,除非数据源来自单次接口且不参与后续业务逻辑。
关键文件:
settings.gradle.ktsbuild.gradle.ktsapp/build.gradle.ktsgradle.propertiesgradle/libs.versions.tomlbuildSrc/src/main/java/Config.kt
当前构建特征:
compileSdk=37minSdk=27targetSdk=37- Java 21
- Compose Compiler 插件随 Kotlin 版本
- Release 默认启用混淆和资源压缩
- Release 只打
arm64-v8aABI split - APK 文件名格式:
Han1meViewer-v{versionName}.apk
本地常用验证:
.\gradlew.bat :app:compileDebugKotlin
.\gradlew.bat :app:assembleDebug提交前建议至少运行:
.\gradlew.bat :app:compileDebugKotlin代码约定:
- 优先做最小正确修改。
- 新 UI 优先使用 Compose。
- 页面状态优先使用
StateFlow,一次性事件用SharedFlow或回调。 - 网络和数据库访问不要写进 Composable。
- 列表分页合并要去重。
- 文本文件工作区行尾使用 CRLF,文件末尾保留一个换行符。
排查问题时优先看:
- 页面是否重复触发网络请求。
- ViewModel 是否在 Loading 状态下清空了不该清空的列表。
Lazy*key 是否唯一。- Parser 是否因 DOM 改动解析为空。
- 登录态或 Cookie 是否过期。
- Cloudflare/IP blocked 是否被正确映射。
新增首页模块:
HomePageScreen.ktMainViewModel.ktParser.homePageVer2HomePage相关 model
新增搜索过滤条件:
SearchScreen.ktAdvancedSearchSheet.ktSearchViewModel.ktHAdvancedSearch.ktNetworkRepo.getHanimeSearchResultHanimeBaseService.getHanimeSearchResult
新增视频详情字段:
HanimeVideo.ktParser.hanimeVideoVer2VideoIntroductionScreen.ktVideoRouteActions.kt,如果字段涉及点击动作或外部跳转
新增我的页面列表:
MainRoutes.ktMainNavHost.kt- 对应
RouteScreen - 对应
ViewModel NetworkRepo和Parser
新增下载相关能力:
DownloadViewModel.ktHanimeDownloadManagerV2.ktHanimeDownloadWorker.ktDownloadDatabase/ DAO / Entity- 下载设置页和权限/SAF 工具
新增设置项:
Preferences.kt或对应配置类HomeSettingsScreen.kt/ 子设置页面- 实际业务读取点
- 目标网站 DOM 改版时,优先检查
Parser.kt。 - 出现网络请求异常时,优先检查拦截器、Cookie、Cloudflare 和备用域名逻辑。
- 出现列表崩溃
Key "..." was already used时,优先检查 ViewModel 合并列表是否去重。 - 出现视频页状态错乱时,优先检查
VideoViewModel.VideoHostUiState和videoIntroUiStateMap。 - 出现下载进度异常时,优先检查 Worker、Room 实体和 DAO Flow。
- 更新依赖版本后,优先跑
:app:compileDebugKotlin,播放器、导航和 KSP/Room 是高风险区域。