ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SimpMusic 现代 Android 架构指南:Clean Architecture 分层、依赖注入与多模块化实战

SimpMusic 现代 Android 架构指南:Clean Architecture 分层、依赖注入与多模块化实战
  • 音视频

【免费下载链接】SimpMusic

A cross-platform music app using YouTube Music for backend

项目地址:https://gitcode.com/gh_mirrors/si/SimpMusic
点击查看免费下载

导读

本文以仓库内 AI 开发规范文档.claude/skills/android-architecture/SKILL.md为核心骨架,系统讲解现代 Android(及 Compose Multiplatform)应用的 Clean Architecture 三层架构、Hilt/依赖注入规范与多模块化策略,并对照 SimpMusic 仓库的真实代码给出可落地的验证证据。读完你将掌握:如何划分 UI / Domain / Data 三层的职责与依赖方向、如何用注解与模块组织依赖注入、如何设计:core:*与:feature:*的多模块工程结构,以及如何在 SimpMusic 这种 Compose Multiplatform 项目中落地同一套原则。

一、High-Level Layers:三层架构与依赖方向

SKILL 文档给出的第一条铁律是:将应用拆分为三个主层,依赖必须严格"向内(向下)"流动到核心逻辑,绝不允许反向依赖。

1. UI Layer(表现层)

  • 职责:展示数据、处理用户交互。
  • 组成:Activity、Fragment、Composable、ViewModel。
  • 依赖:只依赖 Domain 层(简单项目也可直接依赖 Data 层接口);绝不能直接依赖 Data 层的实现细节。

在 SimpMusic 中,表现层集中在composeApp的 commonMain 与androidApp:

  • Android 入口 MainActivity.kt 负责组装 Koin 模块、绑定媒体服务、最终以setContent { App(viewModel) }挂载 Compose 界面;
  • 共享 UI 根组件 App.kt 只收集SharedViewModel暴露的StateFlow(如sleepTimerState、nowPlayingState、themeMode),通过collectAsStateWithLifecycle()订阅——这正是"UI 层只面向 ViewModel,不触碰数据实现"的体现;
  • 全部 ViewModel 位于composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/viewModel/,它们统一继承自 BaseViewModel.kt。

2. Domain Layer(业务逻辑层)【可选但推荐】

  • 职责:封装复杂业务规则、促进复用。
  • 组成:Use Case(如GetLatestNewsUseCase)、Domain Model(纯 Kotlin data class)。
  • 纯 Kotlin:不得包含任何 Android 框架依赖(不能出现android.*import)。
  • 依赖:只依赖 Repository 接口。

SKILL 特别强调 Domain 层的"纯 Kotlin"属性。SimpMusic 对此的执行非常彻底:settings.gradle.kts将:common、:data、:domain三个核心模块显式映射到仓库根目录下的coregit 子模块(project(":domain").projectDir = File(coreDir, "domain")),而core/domain从 CLAUDE.md 的架构说明看承载的是"领域模型、用例、Repository 接口、业务规则",是纯 Kotlin 逻辑层,不依赖任何 Android 框架。

3. Data Layer(数据层)

  • 职责:管理应用数据(拉取、缓存、保存)。
  • 组成:Repository 实现、Data Source(Retrofit API、Room DAO)。
  • 依赖:只依赖外部数据源与第三方库。

对应到 SimpMusic:core/data提供 Repository 实现与 Room 数据库,core/service/*提供网络服务实现。仓库中AlbumViewModel依赖的是com.maxrave.domain.repository.AlbumRepository接口,而具体的 Room/网络实现被屏蔽在 Domain 层之后——UI 层永远看不到 SQLite 与网络细节。

二、Dependency Injection:Hilt 规范与 SimpMusic 的 Koin 等价实现

SKILL 规定"使用 Hilt 完成所有依赖注入",并给出了四个注解用法。但必须指出一个仓库实测事实:当前 SimpMusic 工程中不存在任何 dagger/hilt 依赖(全仓库检索零命中),实际选用的是 Koin。两者的角色等价——都是把"谁依赖谁"从手工new中解放出来。下面先完整保留 SKILL 的 Hilt 规范,再逐条给出仓库中的 Koin 对应物。

SKILL 中的 Hilt 用法(规范原文)

  • @HiltAndroidApp:标注Application类。
  • @AndroidEntryPoint:标注 Activity 与 Fragment。
  • @HiltViewModel:标注 ViewModel,并使用标准的构造函数注入。
  • Modules:
    • @Module+@InstallIn(SingletonComponent::class):用于应用级单例(如 Network、Database)。
    • 抽象类中使用@Binds绑定接口实现(比@Provides更干净)。

SimpMusic 中的 Koin 等价实现

Hilt(SKILL 规范)SimpMusic(Koin 实现)
@HiltAndroidAppApplicationSimpMusicApplication.kt 继承KoinComponent,在onCreate()里startKoin { androidContext(...); loadAllModules(...); loadKoinModules(viewModelModule) }
@AndroidEntryPointActivityMainActivity.kt 通过by inject<SharedViewModel>()注入;并在onCreate/onDestroy中loadKoinModules/unloadKoinModules(viewModelModule)以重建 ViewModel
@HiltViewModel构造函数注入ViewModelModule.kt 中viewModel { AlbumViewModel(get(), get()) }声明全部 ViewModel 及其构造依赖
@Module单例loadAllModules(AppIdentity(...))从core/data的 Koin 模块加载网络、数据库等单例

Koin 在commonMain同样可用(App.kt 中SharedViewModel: SharedViewModel = koinInject()),这是它能在 Compose Multiplatform 的共享代码中取代 Hilt 的关键优势——Hilt 依赖 Android Gradle 插件,无法直接服务 jvmMain/iosMain。

值得一提的设计细节:SKILL 推荐"用@Binds绑接口实现,比@Provides更干净"。Koin 侧 SimpMusic 也贯彻了"面向接口"思想——例如ViewModelModule.kt中LoginSyncHostViewModel(get())/LoginSyncSenderViewModel(get())的注释写明"Koin resolves dependencies when a ViewModel is created",各平台分别绑定自己的 Repository 实现,UI 层只依赖 Domain 的 Repository 接口。

三、Modularization Strategy:多模块工程结构

SKILL 建议生产级应用采用多模块策略以提升构建速度与关注点分离,并给出标准模块划分:

  • :app:主入口,连接各 feature;
  • :core:model:共享领域模型(纯 Kotlin);
  • :core:data:Repository、DataSource、Database、Network;
  • :core:domain:Use Case 与 Repository 接口;
  • :core:ui:共享 Composable、主题、资源;
  • :feature:[name]:独立 feature 模块,自带 UI 与 ViewModel,只依赖:core:domain与:core:ui。

SimpMusic 的实际模块拓扑

对照 settings.gradle.kts,仓库的模块命名做了自己的取舍,但分层思想与 SKILL 完全一致:

  • 主入口::androidApp(Android 壳)、:vivoAndroidApp(vivo 定制壳)、:composeApp(共享 UI + 业务,可运行 Desktop)、:desktopApp;
  • 核心三模块::common、:domain、:data——全部通过project(...).projectDir = File(coreDir, "...")指向core子模块;
  • 服务模块(对应 SKILL 的 Data Source)::ktorExt、:kotlinYtmusicScraper、:spotify、:aiService、:lyricsService、:listenTogether、:loginSync等,映射到core/service/*;
  • 媒体模块::media3、:media3-ui(Android 播放)、:media-jvm、:media-jvm-ui(桌面播放),映射到core/media/*;
  • 功能开关型成对模块::crashlytics/:crashlytics-empty、:cast/:cast-empty、:lastfm/:lastfm-empty——Full 版与 FOSS 版通过isFullBuild属性在构建期二选一,保证 F-Droid 构建不含任何闭源依赖。

可以看到 SimpMusic 并没有生搬硬套:core:model、:core:ui、:feature:[name]的命名,而是按"共享代码放在composeApp+ 多平台 commonMain、平台专属能力拆成 expect/actual 或独立模块"的思路组织——这是 Compose Multiplatform 对 SKILL 模块化原则的合理演绎。

依赖方向也严格执行"高层只能依赖低层":CLAUDE.md 给出的依赖链为UI(composeApp) → Domain(core/domain) → Data(core/data) → Service(core/service/*) → Common(core/common),与 SKILL 的"依赖向内流动"一致。例如core/service/loginSync的说明明确写道:服务层只负责传输、不携带业务语义;Domain 层持有LoginSyncHostRepository/LoginSyncSenderRepository接口;Data 层才是唯一知道 payload 含义的地方——这正是三层边界在真实工程中的贯彻。

四、Checklist:落地实现自检清单

SKILL 提供了三条实施检查项,结合仓库代码逐条验证:

  1. 确保 Domain 层无 Android 依赖从源码结构看,core/domain被独立映射到core子模块且不引入android.*;composeApp的 commonMain 虽然包含 UI 与 ViewModel,但 ViewModel 只 importandroidx.lifecycle.ViewModel(如 BaseViewModel.kt),领域模型与业务规则全部经由com.maxrave.domain.*引用,边界清晰。

  2. Repository 默认提供主线程安全的 suspend 函数(内部按需使用Dispatchers.IO)AlbumViewModel.kt 中albumRepository.getAlbumData(browseId)、albumRepository.getAlbumAsFlow(browseId)均为挂起函数或 Flow,UI 侧统一在viewModelScope.launch中订阅,没有在主线程直接执行 I/O 的写法。

  3. ViewModel 通过StateFlow与 UI 层交互这是 SimpMusic 最显著的统一模式。以 AlbumViewModel.kt 为例:

private val _uiState: MutableStateFlow<AlbumUIState> = MutableStateFlow(AlbumUIState.initial()) val uiState: StateFlow<AlbumUIState> = _uiState

再配合_uiState.update { it.copy(...) }线程安全地更新状态,UI 侧用collectAsStateWithLifecycle()收集。此模式在仓库中被 Systematically 使用(AnalyticsViewModel、ArtistViewModel、AutoEqViewModel等同构),与配套规范.claude/skills/android-viewmodel/SKILL.md中"StateFlow 必须有初始值、对外只暴露只读 StateFlow、用.update {}更新"的建议完全一致。

五、在 Compose Multiplatform 中执行架构原则的补充要点

SKILL 面向传统 Android 工程编写,SimpMusic 则把它平移到了 KMP 共享代码上,有三点值得注意:

  1. ViewModel 不再需要@AndroidEntryPointcommonMain 的 ViewModel 通过BaseViewModel : ViewModel(), KoinComponent直接使用by inject<...>()(如mediaPlayerHandler、SongRepository),绕开了 Activity/ViewModelStore 的 Hilt 桥接,天然跨 Android/Desktop 复用。

  2. 纯 Kotlin 边界要延伸到服务层由于:data以implementation依赖服务模块,UI 无法直接看到服务层(如lyricsService的说明:"the dependency stops there andcomposeAppcannot see it——the layering rule working as intended")。UI 需要的数据一律经 Domain 的 Repository 接口中转,这是三层原则在依赖图上最直观的体现。

  3. 成对空实现模块是"平台/发行版边界"的另一种表现cast-empty、crashlytics-empty、lastfm-empty都以"同名公开 API + 返回 false/no-op 的桩实现"隔离闭源 SDK,功能代码与发行策略解耦,属于多模块化的高级用法。

参考资料(仓库内)

  • 规范原文:.claude/skills/android-architecture/SKILL.md
  • 配套 ViewModel 规范:.claude/skills/android-viewmodel/SKILL.md
  • 模块清单与子模块映射:settings.gradle.kts
  • 依赖版本目录:gradle/libs.versions.toml
  • 依赖注入装配:ViewModelModule.kt、SimpMusicApplication.kt
  • ViewModel 基类与 StateFlow 示例:BaseViewModel.kt、AlbumViewModel.kt
  • 架构总览:CLAUDE.md
  • 音视频

【免费下载链接】SimpMusic

A cross-platform music app using YouTube Music for backend

项目地址:https://gitcode.com/gh_mirrors/si/SimpMusic
点击查看免费下载

相关推荐

上一篇:终极指南:Air与Docker完美集成实现Go开发热重载
下一篇:如何3分钟上手DSH Desktop:从下载macOS安装包到第一次官方DSH会话

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进