Now in Android 模块化学习之旅:app/feature/core三层 Gradle 模块划分实战解析
【免费下载链接】nowinandroidA fully functional Android app built entirely with Kotlin and Jetpack Compose项目地址: https://gitcode.com/GitHub_Trending/no/nowinandroid
本文以 Now in Android(NiA)官方仓库的 ModularizationLearningJourney.md 为主线,系统讲解该应用如何将功能拆分为app、feature(api/impl 双子模块)与core三类 Gradle 模块,并结合settings.gradle.kts、TopicNavKey等源码证据,帮助读者掌握模块依赖规则、导航解耦方式与依赖图维护流程,可直接迁移到自己的多模块 Android 项目中实践。
学习路径概览:为什么 NiA 要这样拆分模块
模块化(Modularization)是把大型应用拆分为多个独立、可复用模块的工程实践。Now in Android 是一个完全基于 Kotlin 与 Jetpack Compose 构建的完整功能型示例应用,其仓库中每个模块的 README 都附带一张模块依赖图(例如:app模块依赖图),可以用来快速理解整个项目的结构。有关模块化的完整官方理论,可参考 Android 官方模块化指南。
本文档的核心价值在于:它不止给出了 NiA 的模块清单,更明确了三类模块各自的边界、它们之间的依赖规则(谁可以依赖谁、谁绝不能依赖谁),以及这些规则在真实源码中的落地形态。下面的 Mermaid 图概括了 NiA 中app、feature、core三类模块的典型关系(完整图例参见 docs/ModularizationLearningJourney.md 中的模块类型图):
图中图例约定如下:app通过虚线(implementation依赖)引用各个feature模块;android-library类型的core模块通过实线(api依赖)向jvm-library模块暴露 API。Top tip:在模块化规划阶段,先画出一张模块图来可视化各模块之间的依赖关系,是非常有效的规划手段——本仓库用 Mermaid 以代码形式维护这张图,天然适合评审与自动化校验。
NiA 的三类模块及其职责边界
Now in Android 将全部模块划分为三类,每一类都有明确的职责与依赖约束(完整模块清单见 settings.gradle.kts 中的include声明)。
app模块:一切代码的组装者
app模块包含应用级与脚手架类,负责把其余代码库"绑"在一起,典型实现包括:
MainActivity(app/src/main/.../MainActivity.kt):应用入口 Activity;NiaApp(NiaApp.kt):根 Compose 组合,承载主题、渐变背景、离线 Snackbar 与顶部应用栏;- 应用级导航:通过
NiaNavHost与TopLevelDestination组织页面跳转。
在NiaApp.kt中可以看到app模块如何"指挥"各 feature 模块:它通过entryProvider同时注册了forYouEntry、bookmarksEntry、interestsEntry、topicEntry、searchEntry五个入口,并通过 TopLevelNavItem.kt 中的TOP_LEVEL_NAV_ITEMS映射ForYouNavKey、BookmarksNavKey、InterestsNavKey到对应的图标与标题资源——这些 NavKey 全部来自各 feature 的api模块,app 只负责把它们组合进导航体系,这正是"应用级脚手架"的体现。
app模块依赖所有feature模块以及必需的core模块。从 app/README.md 的完整依赖图可以看到:app以implementation方式指向:feature:*:api、:feature:*:impl、:sync:work以及:core:analytics、:core:common、:core:data、:core:designsystem等,还与:benchmarks(Baseline Profile 测试 APK)存在双向虚线关联。
feature模块:单点职责 + api/impl 双子模块
feature 模块处理应用中单一职责的功能。例如ForYou功能负责 "ForYou" 屏幕的全部内容与 UI 状态(包括首次运行的引导流程)。NiA 中 feature 模块本身不是独立的 Gradle 模块,而是被拆成两个子模块:
api—— 只包含导航键(navigation keys);impl—— 包含其余所有内容(UI 组件、ViewModel 等)。
这种拆分带来两个关键能力:
- 跨功能导航解耦:功能 A 可以通过目标功能 B 的
api模块中的导航键,跳转到功能 B,而不需要了解 B 的任何内部实现。 - 复用与隔离:
api/impl子模块可以被任何应用使用,包括测试或带 flavor 的应用。一个类如果只被某一个 feature 用到,就留在该 feature 内;如果被多个功能共享,则应下沉到合适的core模块。
feature 模块的依赖铁律:
api模块不得依赖其他 feature 的api或impl模块;impl模块只能依赖其他 feature 的api模块(不得依赖其他 feature 的impl);- 两个子模块都只能依赖它们确实需要的
core模块。
这条规则在源码中可以得到直接验证。以 feature/topic/impl/build.gradle.kts 为例,feature:topic:impl的依赖只有projects.core.data、projects.feature.topic.api以及测试相关的core:testing,完全没有触碰其他 feature 的impl。
core模块:跨模块共享的公共库
core模块是包含辅助代码与特定依赖的公共库,供应用内其他模块共享。它们可以依赖其他 core 模块,但绝不能依赖 feature 模块或 app 模块。NiA 的 core 层非常丰富,从 settings.gradle.kts 可以看到至少包括:analytics、common、data、data-test、database、datastore、datastore-proto、datastore-test、designsystem、domain、model、navigation、network、notifications、screenshot-testing、testing、ui。注意其中common、model、datastore-proto是纯 JVM 库(jvm-library),在依赖图中以紫色标记,可以显著提升单元测试速度与构建缓存命中率。
杂项模块
除上述三类外,NiA 还有一批特殊模块:sync(含sync:work与sync:sync-test,负责后台同步)、benchmarks(Macrobenchmark 基准测试)、ui-test-hilt-manifest、lint,以及app-nia-catalog—— 一个专门用于快速展示设计系统的目录应用(可在 IDE 中通过app-nia-catalog运行配置查看 design system)。
模块示例对照表:从文档到源码逐一对号入座
原文档用一张对照表说明每个模块的职责与关键类,以下结合仓库源码逐一印证:
| 模块 | 职责 | 关键类与实例 |
|---|---|---|
app | 将应用正常运转所需的一切绑定在一起,包括 UI 脚手架与导航 | NiaApp、MainActivity;通过NiaNavHost、NiaAppState、TopLevelDestination(当前仓库中对应 TopLevelNavItem.kt 与 NiaAppState.kt)实现应用级导航 |
feature:*:api | 提供其他 feature 可用的导航键与导航函数 | TopicNavKey(见下节源码分析) |
feature:*:impl | 特定功能或用户旅程的完整实现,通常包含 UI 组件与 ViewModel,并从其他模块读取数据 | feature:topic:impl的TopicScreen、TopicViewModel(TopicScreen.kt、TopicViewModel.kt);feature:foryou:impl展示用户新闻流与首次运行引导 |
core:data | 从多个数据源获取应用数据,供不同 feature 共享 | TopicsRepository(TopicsRepository.kt)等仓库接口,其默认实现为OfflineFirstTopicsRepository |
core:designsystem | 设计系统:核心 UI 组件(多为定制化的 Material 3 组件)、应用主题与图标,可通过app-nia-catalog运行配置预览 | NiaIcons、NiaButton、NiaTheme |
core:ui | 供 feature 模块使用的复合 UI 组件与资源(如新闻流)。与designsystem不同,它依赖数据层,因为要渲染NewsResource等模型 | NewsFeed、NewsResourceCardExpanded |
core:common | 模块间共享的通用类 | NiaDispatchers、Result |
core:network | 发起网络请求并处理来自远程数据源的响应 | RetrofitNiaNetworkApi |
core:testing | 测试依赖、测试仓库与工具类 | NiaTestRunner、TestDispatcherRule |
core:datastore | 使用 DataStore 存储持久化数据 | NiaPreferences、UserPreferencesSerializer |
core:database | 使用 Room 的本地数据库存储 | NiaDatabase、DatabaseMigrations、各Dao类(数据库 schema 版本见 core/database/schemas/.../NiaDatabase) |
core:model | 全应用共享的模型类 | Topic(Topic.kt)、Episode、NewsResource |
一个值得注意的细节:core:ui依赖数据层(如NewsResource模型)而core:designsystem不依赖,这说明 NiA 刻意把"纯展示组件"与"绑定了业务模型的复合组件"分成两个 core 模块,从而让设计系统保持最大程度的可复用性。
跨模块导航的解耦范式:TopicNavKey实例剖析
文档以:topic:api与:interests:impl的交互作为 feature 间导航的标准范式。真实的 TopicNavKey.kt 实现如下:
@Serializable data class TopicNavKey(val id: String) : NavKey fun Navigator.navigateToTopic( topicId: String, ) { navigate(TopicNavKey(topicId)) }可以看到:
TopicNavKey是一个@Serializable的 data class,实现androidx.navigation3.runtime.NavKey,携带目标话题的id作为导航参数;navigateToTopic是Navigator的扩展函数,内部直接navigate(TopicNavKey(topicId))。
这个范式下,:interests:impl只需依赖:topic:api,即可在用户点击某个 topic 时调用navigateToTopic(topicId)从InterestsScreen跳到TopicScreen。Navigator与导航状态的定义在 core/navigation,并有对应的 NavigatorTest.kt 单元测试验证导航逻辑。整个链路验证了文档的结论:feature 间通过 api 模块暴露的导航键通信,实现彻底解耦,这正是 api/impl 拆分最重要的工程收益。
依赖图的维护:README 图、Build.yaml 与graphUpdate任务
每个模块的README.md都包含一张模块图(例如:app模块依赖图)。这些图不是手工维护的静态图片,而是与代码同步的 Mermaid 文本块。
当模块依赖发生变化时,图会由 Build.yaml 工作流自动更新。具体机制(见该工作流第 81 行与第 93 行):
./gradlew graphUpdate- 工作流中运行
./gradlew graphUpdate重新生成各模块 README 中的依赖图; - 若图与当前依赖不一致(例如开发者改了依赖但没更新图),第 93 行会输出错误提示
Check Graphs failed, please update graphs with: ./gradlew graphUpdate并使 CI 失败,强制开发者提交同步的图。
开发者也可以在本地手动运行graphUpdate任务刷新所有 README 图。这一做法把"架构图漂移"问题变成了 CI 可检查项,值得在大型多模块项目中借鉴。
模块化程度的权衡:过度模块化 vs 面向未来扩展
NiA 的模块化方案是结合项目路线图、规划中的工作与新特性反复推敲后确定的。其核心目标是:在"过度模块化一个小型应用"与"用这个机会展示适合更大代码库、更接近生产环境真实应用的模块化模式"之间找到平衡。
原文档(docs/ModularizationLearningJourney.md)给出的关键建议值得强调:
- 模块化没有唯一正确答案:应用大小、代码库现状与团队偏好不同,方案必然不同。很少有哪一种方式适合所有场景。
- 规划先行:动手前先明确目标、要解决的问题、未来工作与潜在障碍,是定义最合适结构的关键步骤。可以组织一次头脑风暴,把模块与依赖画成图来可视化规划。
- 按需调节粒度(granularity):粒度指代码库由多少模块组成。数据层规模小的时候,放在单一模块完全没问题;一旦仓库与数据源数量增长,就值得考虑拆分为独立模块。
- 保持演进心态:NiA 的方案并非一成不变的结构,未来仍可能演进,它只是一个"最适合本项目的通用指导方针",开发者可以在其上继续修改、扩展。例如通过提高代码库粒度来进一步拆分。
这也与仓库中core模块的丰富分层相互印证:NiA 把数据、数据库、网络、DataStore、设计系统、UI、领域层等都拆成了独立 core 模块,正是"按需扩展粒度"的具体示范。
小结:把 NiA 的模块化经验移植到自己的项目
回顾全文,Now in Android 的模块化方案可以提炼为四条可直接落地的经验:
- 三层职责划分:
app负责组装与脚手架,feature负责单点业务功能,core负责跨模块共享的基础能力,三者依赖方向严格单向(feature → core,app → 一切)。 - api/impl 双子模块:feature 对外只暴露导航键,用
@Serializable的NavKey+Navigator扩展函数实现无实现依赖的跨功能跳转,可测试、可复用、可替换。 - 依赖规则显式化:
api不依赖其他 feature,impl只依赖其他 feature 的api,core 不依赖 feature/app——这些约束在build.gradle.kts中可审计、可强制。 - 架构图自动化:用 Mermaid 在 README 中维护模块图,通过
./gradlew graphUpdate与 Build.yaml 工作流保证图与代码一致。
最后请记住:这不是唯一正确的方案,而是 NiA 团队在社区反馈与项目实际约束下给出的一个高质量参照系。阅读源码时,可以从 settings.gradle.kts 的模块清单出发,对照 app/README.md 的完整依赖图,再逐个深入各 feature 与 core 模块的源码与测试,构建起对多模块 Android 应用架构的完整认知。
【免费下载链接】nowinandroidA fully functional Android app built entirely with Kotlin and Jetpack Compose项目地址: https://gitcode.com/GitHub_Trending/no/nowinandroid
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考