docs/contributing/architecture.md
Animeko (简称 Ani) 现在已经是一个不小的项目,本章节将给你一个全面的了解。
Ani 不只是一个客户端,还涉及弹幕服务端,Bangumi 服务等依赖。 此文章随后只关注 Ani 客户端部分,不过了解相关服务关系仍然可以为你提供帮助:
| 名称 | 仓库/主页地址 | 说明 |
|---|---|---|
| Animeko 客户端 | 当前 | Android/iOS/PC 客户端统一仓库 |
| Animeko 服务端 | GitHub | Ani 弹幕服务 |
| Bangumi 最高热度排行榜等缓存服务 | ||
| 新番时间表等高级索引服务 | ||
| 弹弹play | 主站 | 弹弹play 弹幕服务 |
| Bangumi | GitHub / 主站 | 提供番剧详情、剧集信息、评论等信息功能; Bangumi OAuth |
| Bangumi Next | 主站 | 提供一些专用接口用来查询 Bangumi 的一些信息 |
依赖关系如下图所示。其中箭头 A -> B 表示 A 依赖 B.
flowchart TD
subgraph "open-ani 组织"
direction LR
APP[Animeko 客户端]
ani-server[Animeko 服务端]
end
subgraph "Bangumi"
direction LR
bangumi-api[api.bgm.tv]
bangumi-next[next.bgm.tv]
end
subgraph "弹弹play"
direction LR
ddplay[弹弹play 弹幕服务]
end
APP ---> bangumi-api
ani-server --> bangumi-next
APP ---> bangumi-next
APP ---> ddplay
APP --> ani-server
ani-server --> bangumi-api
一些关键点:
本章节将从大到小讲述 Ani 客户端 (本 GitHub 仓库) 的架构。
我们会先从仓库结构 了解仓库内有哪些模块及其作用,然后进入最重要的 APP 模块了解 APP 架构,随后按照架构的每一层级深入了解各个模块的作用。
本章节会假设你熟悉一些 Kotlin 多平台技术。如果你不熟悉,可以先阅读这里。
Ani 使用 Gradle 构建系统。Gradle 构建系统由多个模块 (module) 组成,每个模块都有自己的功能和职责。
Ani 客户端大致包含 30 个模块。我们可以把它们分成以下几个类别,这也对应了仓库内的目录结构:
utils:基础工具模块,包含日志库、序列化、BBCode 等基础工具datasource:数据源
api:通用数据源接口bangumi:Bangumi 数据源ikaros、jellyfin、bt/mikan、...:各数据源实现client:封装 Ani 服务端的接口danmaku:弹幕功能
dandanplay:弹弹 play 弹幕服务ui:弹幕引擎以及视频播放器上的弹幕层 UItorrent:BitTorrent 模块,接入 anitorrentapp
shared:全平台共享代码,包括全部 UI 代码和业务逻辑等android,desktop,ios:各个平台的入口模块,用于打包 apk/exe/dmg可以看到,app/shared 是存放绝大部分代码的地方,也是最重要的模块,下文会详细解释。其他模块比较简单,基于名称就能了解其作用,不再赘述。
我们将详细了解 app/shared 模块内部的架构.
APP 采用安卓官方推荐架构,即 UI 到 Domain 到 Data 的分层架构,并依照多平台目标增加了平台层:
graph TD
UI[UI Layer] --> Domain[Domain Layer] --> Data[Data Layer] --> Platform[Platform Layer]
UI 层负责展示数据,Domain 层负责业务逻辑,Data 层负责数据的获取和存储,Platform 层负责适配不同平台的基础需求。
在 Ani 中,shared 模块内部依照该层级关系拆解为:
app-platform:平台层。用于适配不同平台的基础需求。包含 BuildConfig、权限管理、音量控制器等。
平台:即 APP 支持的操作系统。例如 Android,macOS,Windows,iOS。
app-data:数据层与业务层。由于一些历史遗留原因,该模块内部同时有数据层和业务层的代码,
不过在不同包名下:
src/commonMain/data:数据层,包含数据库持久化,网络处理。src/commonMain/domain:业务层,包含各个功能的业务逻辑,处理数据源的数据。ui-*:UI 层。用于展示数据,与用户交互。按功能拆分为多个模块,但它们都拥有名称前缀 ui-。下面将浅要介绍各层的功能。如果你已经熟悉安卓官方推荐架构,可以跳过。
数据层负责数据的获取和存储。它会执行网络请求,管理本地缓存,并暴露数据模型 data class 和数据仓库
Repository 给业务层使用。
在 Ani 中,数据层主要包含以下几个部分(在
app/shared/app-data/src/commonMain/data 中):
models:数据模型。即番剧信息、剧集信息、用户信息等的 data class。也会被 Domain 层使用。persistent:持久化
datastore:Data Store 数据存储。相当于把数据序列化成 JSON 和 ProtoBuf
后存储到一个文件内。一些非常简单的配置信息会使用这种方式存储。database:Room SQLite 数据库。用于存储番剧信息缓存、用户收藏信息缓存、观看记录等。network:网络请求。例如 BangumiSubjectService 用于执行 HTTP 请求,获取 Bangumi
番剧信息。repository:数据仓库。每个数据仓库协调网络层提供的 service 和持久层的数据库,管理一类数据的获取和存储。
例如,SubjectCollectionRepository 负责结合 network 层的 BangumiSubjectService 和 database 层的
SubjectCollectionDao,在获取数据时自动缓存网络请求结果到数据库。业务层负责业务逻辑。
业务层会使用数据层提供的数据模型和数据仓库,处理数据,暴露特定业务的专业处理器和 UseCase 给 UI 层使用。例如:
MediaFetcher 来实现这些功能。MediaSelector 来处理这些逻辑。EpisodeFetchSelectPlayState 完成这些子功能的组装。这些处理器都与 UI 无关,且不会绑定到特定的数据来源(不直接依赖网络服务而是依赖抽象的 Repository
接口)。它们通常拥有全面的单元测试。
我们无法一一列举所有业务逻辑。
app/shared/app-data/src/commonMain/domain 内按照对应的业务类型拆分为多个包,例如
player、media、
settings 等。请参考源代码以了解具体现有业务有哪些。
[!TIP] 如何快速定位你想修改的业务逻辑
从 UI 入手。全局搜索 UI 页面上的关键字,根据调用关系找到对应的 ViewModel,进而找到对应业务逻辑。
这里我们解答一些团队内的规范:
Ani 使用 UseCase 设计(参考安卓官方推荐)。对于同一个业务场景的重复操作,可以封装为一个 UseCase 来减少重复。
但 UseCase 封装不是必须的。如果功能非常简单,你可以在 UI 的 state holder(例如 ViewModel)中直接调用 Repository,而无需经过 UseCase。这非常适合一些简单的 CRUD 操作:
class MyViewModel(
private val subjectId: Int,
) : AbstractViewModel(), KoinComponent {
private val episodeCollectionRepository: EpisodeCollectionRepository by inject()
fun setAllEpisodesWatched() {
viewModelScope.launch {
// GOOD
episodeCollectionRepository.setAllEpisodesWatched(subjectId)
}
}
}
复杂操作必须在 Domain 层封装一个对象或者 UseCase。UI 不允许进行复杂数据操作。
UI 层负责展示数据,与用户交互。它只会跟 Domain 层接触,不会直接访问 Data 层。
Ani UI 100% 使用 Compose Multiplatform(CMP)编写。
CMP 是 JetBrains 开发的,基于 Android Jetpack Compose 的多平台 UI 框架,支持 iOS、PC 等平台,用法与 Jetpack Compose 几乎一样。
前文已经说过,Ani 的 UI 代码按业务类型划分为一些名为 ui-* 的模块。
我们列举一些关键 UI 模块(以实际源码为准,这个列表不会及时更新):
ui-foundation:基础 UI 组件。包含主题、错误处理、基础控件等ui-adaptive:自适应布局的支持ui-comment:评论功能ui-settings:设置页ui-subject:条目(番剧)详情页、剧集详情页(播放页)、评分ui-onboarding:新用户引导页ui-exploration:首页的探索分区。包含“最高热度”、“新番时间表”、“继续观看”栏目video-player:播放器的控制器 UI:danmaku:ui 模块内,而非 :app:sharedapp/shared/src 中。这是历史遗留,以后会全部拆分为单独的 ui-* 模块有关 UI 层的最佳实践,请参考 Jetcaster。
由于历史遗留原因,Ani 项目内留有许多不好的实践,但新编写的代码遵循最佳实践。所以,如果你想查看 Ani
中的最佳实践,可以阅读 ui-exploration 和 ui-onboarding。
我们将 Ani 的部分组件独立为单独的项目开发,并采用宽松的 Apache 2.0 协议开源 (相对于 Ani 的 AGPL 协议)。如果你需要修改相关功能,请查看以下项目:
Mediamp: 多媒体播放器库,用于播放音频和视频。 注意,Mediamp 只包含播放器内核实现。播放器 UI 和控制部分仍然在 Ani 客户端 (当前) 项目中。
anitorrent: 封装 libtorrent 的 BitTorrent 引擎,用于下载番剧。 Anitorrent 只提供低级 API。具体的 BT piece 控制、下载任务管理、缓存管理等功能仍然在 Ani 客户端 ( 当前) 项目中。