Android 项目模块化与 Feature 组件实践
本文档结合本仓库(Now in Android)的真实结构,用通俗语言说明:官方所说的模块化(Modularization)在本项目里长什么样、feature 层的组件化约定、日常如何加模块。
仓库地址:https://github.com/android/nowinandroid
术语说明
- 模块化 :按职责拆成多个 Gradle 模块 (
app/core/feature),与 官方 Modularization 一致。- 组件化(本文狭义) :在
feature上用 api + impl 、NavKey 做业务边界,功能之间不直接依赖对方界面。
国内常把两者统称「组件化」;读 NIA 源码或英文文档时,请优先用 模块化 这个词。
(本文档内容与官方用语对齐。)
如果你已经熟悉 Android 开发,但第一次接触多模块工程,按本文从「整体 → 规则 → 实操」阅读即可。
更详细的英文说明:ModularizationLearningJourney.md。
一、模块化与组件化:分别指什么?
1.1 模块化(本项目的整体形态)
模块化 = 把原来塞在一个 app 里的代码,按职责拆成多个 Gradle 模块(每个模块可单独编译、测试、被依赖)。
可以把 App 想成一栋楼:
| 比喻 | 在本项目里对应 |
|---|---|
| 大楼外壳、电梯、总前台 | app 模块:MainActivity、NiaApp、底部导航、把各功能拼起来 |
| 一个个独立商铺(首页、书签、设置...) | feature/* 功能模块 |
| 水电、网络、物业等公共设施 | core/* 公共模块:数据、网络、主题、通用 UI 等 |
| 夜班保洁、性能测试 | sync、benchmarks 等辅助模块 |
模块化带来的好处:
- 编译更快:改一个功能时,往往只需重编相关模块。
- 边界更清晰:按模块划分所有权,减少「全工程乱引用」。
- 多人协作 :不同人负责不同
feature或core,冲突更少。 - 可测试:小模块更容易写单元测试。
结论 :Now in Android 在官方语境下是一个 模块化(multi-module)项目。
1.2 组件化(feature 层的额外约定)
在本项目中,业务功能模块 还多做了一步(不算所有 Android 模块化工程都有):
| 做法 | 作用 |
|---|---|
拆成 api + impl |
api 只暴露导航;impl 放 Screen、ViewModel |
NavKey + navigateToXxx |
跨功能跳转不依赖对方的 *Screen 类 |
impl 只依赖其他功能的 api |
降低功能之间的编译耦合 |
这叫 业务组件化 或 功能组件化 ------是模块化之上的 通信与边界规则,不是替代模块化。
结论 :core 层主要是 技术模块 ;feature 层 = 模块化 + 组件化实践。
二、本项目模块一览
在 settings.gradle.kts 里注册的所有子工程,就是整个 App 的「模块清单」。结构可以简化为:
nowinandroid/
├── app/ # 壳:组装、导航、Application
├── feature/ # 按业务拆分的功能
│ ├── foryou/ (api + impl)
│ ├── interests/(api + impl)
│ ├── bookmarks/(api + impl)
│ ├── topic/ (api + impl)
│ ├── search/ (api + impl)
│ └── settings/ (仅 impl)
├── core/ # 全 App 共享能力
│ ├── model, common, data, domain
│ ├── network, database, datastore
│ ├── designsystem, ui, navigation
│ └── analytics, notifications, testing ...
├── sync/work/ # 后台同步
├── build-logic/ # 统一 Gradle 配置(约定插件)
└── benchmarks, app-nia-catalog, lint ...
当前功能模块与职责:
| 功能 | 用户看到的界面 | 模块路径 |
|---|---|---|
| 为你推荐 | For You 首页 | feature/foryou |
| 兴趣 / 关注 | Interests | feature/interests |
| 书签 | Bookmarks | feature/bookmarks |
| 主题详情 | Topic 详情页 | feature/topic |
| 搜索 | Search | feature/search |
| 设置 | Settings | feature/settings(只有 impl) |
每个模块目录下的 README.md 里有一张 依赖关系图 ,改依赖后可以跑 ./gradlew graphUpdate 更新。
三、四类模块,各自干什么?
1. app ------ 总装配车间
职责:
Application(NiaApplication)、MainActivity- 把各
feature的导航入口注册到NiaApp里 - 底部 Tab、顶层 AppBar 等「跨功能」的 UI 骨架
原则 :app 可以依赖所有需要的 feature 和 core,但业务逻辑不要堆在这里;具体页面应在 feature/*/impl 里。
NiaApp 里把各功能的「路由注册」集中在一起,例如:
kotlin
val entryProvider = entryProvider {
forYouEntry(navigator)
bookmarksEntry(navigator)
interestsEntry(navigator)
topicEntry(navigator)
searchEntry(navigator)
}
2. feature ------ 按用户旅程拆分
一个「功能」在 Gradle 里通常拆成两个子模块:
| 子模块 | 放什么 | 依赖谁 |
|---|---|---|
api |
对外暴露的「门牌号」:导航 Key、跳转扩展函数 | 尽量只依赖 core:navigation |
impl |
真正实现:Screen、ViewModel、本功能的 navigation 注册 | 自己的 api + 需要的 core + 其他功能的 api(不能依赖别人的 impl) |
这是本项目的核心设计:功能之间不直接引用对方的界面类,只通过 api 里的导航契约通信。
3. core ------ 公共基础设施
多个功能都会用到的代码放这里,例如:
| 模块 | 典型内容 |
|---|---|
core:model |
Topic、NewsResource 等纯数据模型(尽量无 Android 依赖) |
core:data |
TopicsRepository 等仓库,统一本地 + 网络数据 |
core:domain |
用例层,组合 data,给 ViewModel 用 |
core:designsystem |
主题、按钮、图标等设计系统 |
core:ui |
跨功能的复合 UI(如新闻卡片列表) |
core:navigation |
Navigator 等导航基础设施 |
原则 :core 可以依赖别的 core,不能 依赖 feature 或 app。
4. 其他模块
sync:work:WorkManager 后台同步benchmarks:启动性能、Baseline Profileapp-nia-catalog:单独跑设计系统组件库build-logic:自定义 Gradle 插件,保证各模块配置一致
四、依赖规则(必记)
用一句话记住方向:
依赖只能「由外向内」:app → feature → core,不能反过来。
更细的约定:
app
└─ implementation → feature:*:api
└─ implementation → feature:*:impl
└─ implementation → core:*
feature:X:api
└─ 只暴露导航相关,通常 api → core:navigation
└─ ❌ 不要依赖其他 feature 的 api 或 impl
feature:X:impl
└─ implementation → feature:X:api
└─ implementation → 其他 feature 的 api(如需跳转)
└─ ❌ 不要依赖其他 feature 的 impl
core:*
└─ 可依赖其他 core
└─ ❌ 不要依赖 feature / app
代码该放哪?
- 只有 一个功能 用 → 留在该功能的
impl - 两个及以上功能 用 → 抽到合适的
core模块 - 只是「想去别的页面」→ 依赖对方
api,调用navigateToXxx()
五、功能之间如何跳转?(api + impl 的灵魂)
步骤 1:在「目的地」的 api 定义 NavKey
例如主题详情在 feature/topic/api:
kotlin
@Serializable
data class TopicNavKey(val id: String) : NavKey
fun Navigator.navigateToTopic(topicId: String) {
navigate(TopicNavKey(topicId))
}
其他模块只要 implementation(projects.feature.topic.api),就能调用 navigator.navigateToTopic(id),不需要 知道 TopicScreen 长什么样。
步骤 2:在「目的地」的 impl 注册页面
TopicEntryProvider.kt 里把 Key 和 UI 绑在一起:
kotlin
fun EntryProviderScope<NavKey>.topicEntry(navigator: Navigator) {
entry<TopicNavKey> { key ->
TopicScreen(
onTopicClick = navigator::navigateToTopic,
viewModel = hiltViewModel<TopicViewModel, Factory>(...) { ... },
)
}
}
步骤 3:在 app 里汇总所有 entry
NiaApp 的 entryProvider { ... } 把各功能的 xxxEntry(navigator) 注册进导航图。
步骤 4:别的功能只依赖 api 发起跳转
「为你推荐」页点击某个主题时(feature/foryou/impl):
kotlin
ForYouScreen(
onTopicClick = navigator::navigateToTopic, // 来自 topic:api
)
流程图:
topic:impl app (NiaApp) topic:api foryou:impl topic:impl app (NiaApp) topic:api foryou:impl #mermaid-svg-2L7D9v70lkw3sW4t{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2L7D9v70lkw3sW4t .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2L7D9v70lkw3sW4t .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2L7D9v70lkw3sW4t .error-icon{fill:#552222;}#mermaid-svg-2L7D9v70lkw3sW4t .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2L7D9v70lkw3sW4t .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2L7D9v70lkw3sW4t .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2L7D9v70lkw3sW4t .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2L7D9v70lkw3sW4t .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2L7D9v70lkw3sW4t .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2L7D9v70lkw3sW4t .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2L7D9v70lkw3sW4t .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2L7D9v70lkw3sW4t .marker.cross{stroke:#333333;}#mermaid-svg-2L7D9v70lkw3sW4t svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2L7D9v70lkw3sW4t p{margin:0;}#mermaid-svg-2L7D9v70lkw3sW4t .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2L7D9v70lkw3sW4t text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-2L7D9v70lkw3sW4t .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-2L7D9v70lkw3sW4t .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-2L7D9v70lkw3sW4t .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-2L7D9v70lkw3sW4t .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-2L7D9v70lkw3sW4t #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-2L7D9v70lkw3sW4t .sequenceNumber{fill:white;}#mermaid-svg-2L7D9v70lkw3sW4t #sequencenumber{fill:#333;}#mermaid-svg-2L7D9v70lkw3sW4t #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-2L7D9v70lkw3sW4t .messageText{fill:#333;stroke:none;}#mermaid-svg-2L7D9v70lkw3sW4t .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2L7D9v70lkw3sW4t .labelText,#mermaid-svg-2L7D9v70lkw3sW4t .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-2L7D9v70lkw3sW4t .loopText,#mermaid-svg-2L7D9v70lkw3sW4t .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-2L7D9v70lkw3sW4t .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-2L7D9v70lkw3sW4t .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-2L7D9v70lkw3sW4t .noteText,#mermaid-svg-2L7D9v70lkw3sW4t .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-2L7D9v70lkw3sW4t .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2L7D9v70lkw3sW4t .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2L7D9v70lkw3sW4t .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2L7D9v70lkw3sW4t .actorPopupMenu{position:absolute;}#mermaid-svg-2L7D9v70lkw3sW4t .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-2L7D9v70lkw3sW4t .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2L7D9v70lkw3sW4t .actor-man circle,#mermaid-svg-2L7D9v70lkw3sW4t line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-2L7D9v70lkw3sW4t :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} navigateToTopic(id) TopicNavKey 入栈 topicEntry 显示 TopicScreen
六、数据层怎么共享?
功能模块的 ViewModel 一般不直接连 Retrofit 或 Room,而是依赖:
ViewModel (feature:impl)
↓
core:domain(可选,复杂业务)
↓
core:data(Repository)
↓
core:network / database / datastore
↓
core:model(数据类型)
例如多个页面都要读主题列表 → 用 core:data 里的 TopicsRepository,而不是在 foryou 和 interests 各写一套网络请求。
七、如何新增一个功能模块?(实操清单)
假设要加一个叫 「通知中心 notifications-center」 的功能。
1. 创建目录
feature/notifications-center/
├── api/
│ ├── build.gradle.kts
│ └── src/main/kotlin/.../navigation/NotificationsCenterNavKey.kt
└── impl/
├── build.gradle.kts
└── src/main/kotlin/.../
├── NotificationsCenterScreen.kt
├── NotificationsCenterViewModel.kt
└── navigation/NotificationsCenterEntryProvider.kt
2. 编写 api/build.gradle.kts
kotlin
plugins {
alias(libs.plugins.nowinandroid.android.feature.api)
}
android {
namespace = "com.google.samples.apps.nowinandroid.feature.notificationscenter.api"
}
android.feature.api 插件会自动加上 core:navigation 等公共依赖。
3. 编写 impl/build.gradle.kts
kotlin
plugins {
alias(libs.plugins.nowinandroid.android.feature.impl)
alias(libs.plugins.nowinandroid.android.library.compose)
}
android {
namespace = "com.google.samples.apps.nowinandroid.feature.notificationscenter.impl"
}
dependencies {
implementation(projects.feature.notificationscenter.api)
// 按需:core:domain、其他 feature 的 api 等
}
android.feature.impl 插件会自动引入 core:ui、core:designsystem、Hilt、Navigation3 等。
4. 在 settings.gradle.kts 注册
kotlin
include(":feature:notifications-center:api")
include(":feature:notifications-center:impl")
5. 在 app/build.gradle.kts 依赖并注册导航
kotlin
implementation(projects.feature.notificationscenter.api)
implementation(projects.feature.notificationscenter.impl)
在 NiaApp 的 entryProvider 中增加:
kotlin
notificationsCenterEntry(navigator)
6. 若需要底部 Tab 或顶层入口
在 app 模块里改 TopLevelDestination、底部导航栏等(这些属于「壳」的职责,放在 app 合理)。
八、Gradle 约定插件(为什么各模块 build 文件很短)
项目在 build-logic 里定义了统一插件,避免每个模块重复抄配置:
| 插件 | 用于 |
|---|---|
nowinandroid.android.feature.api |
功能 api 模块 |
nowinandroid.android.feature.impl |
功能 impl 模块 |
nowinandroid.android.library |
普通 core 库 |
nowinandroid.hilt |
依赖注入 |
新建模块时 优先复用这些插件 ,风格和现有 feature/foryou 保持一致。
九、和「分层架构」的关系
模块化 解决 「代码物理上怎么拆文件、怎么编」 ;官方推荐的分层(UI → Domain → Data)解决 「逻辑上谁调用谁」 ;feature 组件化 再约定 「功能之间怎么跳转、少互相依赖 impl」。
在本项目中二者一起用:
- 横向 :
feature:foryou、feature:topic... - 纵向 :
impl里的 UI →core:domain/core:data→core:network
不要为了在 feature 里「省事」而把网络请求写在 Composable 里;该走 Repository 仍然走 core:data。
十、常见问题
Q1:什么时候不必拆 api + impl?
- 功能非常简单、确定不会被别的模块跳转,且团队很小,可以像
feature/settings一样 只有 impl。 - 一旦需要被跳转或希望严格隔离,建议补
api。
Q2:能不能 feature A 的 impl 依赖 feature B 的 impl?
不要。 会破坏边界,编译耦合变重,也容易循环依赖。只依赖 B 的 api。
Q3:改完依赖图怎么看?
打开对应模块的 README.md,或运行:
bash
./gradlew graphUpdate
Q4:本地跑哪个变体?
日常开发用 demoDebug (本地静态数据,无需公网后端)。见根目录 README.md。
十一、自检:你的拆分是否合理?
加模块或改依赖前,可以问自己:
- 这个类是否会被 第二个功能 使用?→ 是则考虑下沉到
core - 是否只是为了 打开另一个页面 ?→ 只依赖对方
api的 NavKey /navigateToXxx core里是否出现了 某个功能独有的文案或布局 ?→ 可能放错了层,应回feature:impl- 依赖箭头是否 指向 core,而没有 feature → app 的反向依赖?
十二、延伸阅读
| 资料 | 说明 |
|---|---|
| ModularizationLearningJourney.md | 本仓库英文模块化学习路径 |
| ArchitectureLearningJourney.md | 架构与 UDF、ViewModel、Repository |
| app/README.md | 完整应用模块依赖图 |
| app-nia-catalog/README.md | 同仓库内只引用 core 的示例 App |
| Android 模块化 | Google 官方概念与收益 |
十三、其他 App 如何配置并使用 core / feature?
NIA 没有 把模块发到 Maven Central,其他 App 只能通过 引用本仓库源码 来用。常见两种做法:
| 做法 | 适用 | 配置量 |
|---|---|---|
| 在本仓库新建 App 模块 | 演示、内部工具、快速试用组件 | 最少 |
| 独立工程 + 挂载 NIA 模块路径 | 业务 App 在另一个 Git 仓库 | 较多 |
无论哪种,都要注意:
- JDK 17+ ,AGP/Kotlin/Compose 版本建议与 NIA 的
gradle/libs.versions.toml对齐。 - NIA 的 Library 带
demo/prod两个 flavor(维度名contentType)。你的 App 若没有同名 flavor,必须在defaultConfig里写:
missingDimensionStrategy("contentType", "demo") - 模块构建依赖
build-logic(includeBuild("../nowinandroid-main/build-logic"))。 - 复用源码请遵守 Apache 2.0(保留文件头版权)。
13.1 方式一:在 NIA 仓库里加新 App(推荐先试)
本仓库 app-nia-catalog 已是范例:只依赖 core:designsystem 和 core:ui,不引 feature。
步骤:
- 复制
app-nia-catalog/→ 例如app-my-shop/。 settings.gradle.kts增加:include(":app-my-shop")。- 改
applicationId、namespace。 app-my-shop/build.gradle.kts里声明依赖并处理 flavor:
kotlin
android {
defaultConfig {
// catalog 已用 demo;新 App 同样指定即可
missingDimensionStrategy("contentType", "demo")
}
}
dependencies {
implementation(projects.core.designsystem)
implementation(projects.core.ui)
// 若要用整块「为你推荐」功能,再加(并做 13.3 的导航注册):
// implementation(projects.feature.foryou.api)
// implementation(projects.feature.foryou.impl)
}
- Run Configuration 选
app-my-shop。
界面示例:
kotlin
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaButton
@Composable
fun ShopHome() {
NiaTheme {
NiaButton(onClick = {}) { Text("购买") }
}
}
13.2 方式二:独立新工程挂载 NIA 模块
目录示例(两个仓库并排):
text
D:/work/
├── nowinandroid-main/
└── MyRetailApp/
├── settings.gradle.kts
├── build.gradle.kts
└── app/build.gradle.kts
(1)settings.gradle.kts
kotlin
pluginManagement {
includeBuild("../nowinandroid-main/build-logic")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories { google(); mavenCentral() }
versionCatalogs {
create("libs") {
from(files("../nowinandroid-main/gradle/libs.versions.toml"))
}
}
}
rootProject.name = "MyRetailApp"
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
val niaRoot = file("../nowinandroid-main")
fun Settings.includeFromNia(path: String) {
include(path)
project(path).projectDir = File(niaRoot, path.removePrefix(":").replace(':', '/'))
}
include(":app")
// --- 按你需要挂载(少挂少编译)---
includeFromNia(":lint") // designsystem 的 lintPublish 需要
includeFromNia(":core:designsystem") // 设计系统
// includeFromNia(":core:model")
// includeFromNia(":core:analytics")
// includeFromNia(":core:ui") // 新闻卡片等
// includeFromNia(":core:data") // Repository,链会更长
// includeFromNia(":feature:foryou:api")
// includeFromNia(":feature:foryou:impl")
规则 :你在
dependencies里写的每个projects.xxx,都必须在上面includeFromNia;并且打开该模块README.md里的依赖图,把传递依赖的模块 一并挂上,否则会报Project not found。
(2)根 build.gradle.kts
kotlin
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.compose) apply false
alias(libs.plugins.hilt) apply false
alias(libs.plugins.ksp) apply false
}
(3)app/build.gradle.kts
kotlin
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.compose)
}
android {
namespace = "com.example.myretail"
compileSdk = 36
defaultConfig {
applicationId = "com.example.myretail"
minSdk = 23
targetSdk = 36
missingDimensionStrategy("contentType", "demo")
}
buildFeatures { compose = true }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
dependencies {
implementation(projects.core.designsystem)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.material3)
}
编译:./gradlew :app:assembleDebug。
13.3 只复用 core 时要挂哪些模块?
| 目标 | 建议挂载 | 参考 |
|---|---|---|
NiaTheme、NiaButton 等设计系统 |
:lint、:core:designsystem |
app-nia-catalog 一部分 |
NewsFeed、新闻卡片等 |
再加 :core:model、:core:analytics、:core:ui |
app-nia-catalog 全部 core 依赖 |
TopicsRepository 等数据 |
再加 :core:data 及 data 依赖图上的 database、network、datastore 等 |
core/data/README.md |
建议 :能只引 designsystem 就不要引 core:ui;能只引 ui 就不要引 data。
13.4 复用 feature 时要额外做什么?
feature 带 界面 + ViewModel + Hilt + 导航 ,比 core 重。以「为你推荐」为例:
1. Gradle 依赖(宿主 app)
kotlin
implementation(projects.feature.foryou.api)
implementation(projects.feature.foryou.impl)
// settings 里还要挂载 impl 传递依赖的 core、topic:api 等(见 feature/foryou/impl/README.md 依赖图)
2. 宿主 App 注册导航(与 NIA 主 App 相同模式)
在 Compose 根处组装 entryProvider(参见 NiaApp.kt):
kotlin
val entryProvider = entryProvider {
forYouEntry(navigator) // 来自 feature/foryou/impl
// topicEntry、interestsEntry ... 若页面内有跳转也要注册
}
NavDisplay(
entries = navigationState.toEntries(entryProvider),
onBack = { navigator.goBack() },
)
3. Hilt
feature/*/impl 里 ViewModel 使用 Hilt,宿主需要:
@HiltAndroidApp的Application- 依赖能提供 Repository 的
core:data等(与 NIA 主 App 一致的 DI 图,或自行写 Test 实现替换)
4. flavor
所有从 NIA 引来的模块仍要选 demo:missingDimensionStrategy("contentType", "demo")。
结论 :跨 App 复用整块 feature ≈ 嵌入一个小子产品;多数团队 只复用 core,业务页在自己工程里按同样 api/impl 模式重写。
13.5 配置检查清单
-
includeBuild了build-logic -
libs.versions.toml来自 NIA 或版本兼容 - 每个
projects.xxx都在settings.gradle.kts里includeFromNia - 宿主 App 写了
missingDimensionStrategy("contentType", "demo") - 用
feature时在宿主注册了xxxEntry+ Hilt - JDK 17,首次编译失败时对照模块
README.md依赖图补模块
13.6 常见报错
| 报错 | 处理 |
|---|---|
| Unable to find matching variant | 补 missingDimensionStrategy("contentType", "demo") |
| Project ':core:xxx' not found | settings 少挂了传递模块 |
只引了 feature:xxx:api 没有界面 |
必须再引 impl 并 xxxEntry 注册 |
插件 nowinandroid.* 找不到 |
检查 includeBuild("../nowinandroid-main/build-logic") |
总结
| 概念 | 记住这一句 |
|---|---|
| 模块化 | 多 Gradle 模块:app / core / feature;对应官方 Modularization |
| feature 组件化 | 业务再拆 api + impl;用 NavKey 跳转,不依赖别人 Screen |
| 跨功能调用 | 只依赖别人的 api ,通过 NavKey + navigateToXxx |
| 共享能力 | 放 core,repository、主题、通用 UI |
| 新增功能 | api + impl → settings.gradle.kts → app 依赖并注册 xxxEntry |
| 其他 App 用 NIA 模块 | 同仓库加 App,或独立工程 includeFromNia + flavor 对齐;feature 还要导航与 Hilt |
本仓库是 模块化工程 ,并在 feature 上采用了 组件化边界 ;你在自己的业务里可以调整模块粒度,但建议保持 依赖方向 与 api/impl 契约 。其他 App 引用 core/feature 见 第十三节。