Android 项目模块化与 Feature 组件实践

Android 项目模块化与 Feature 组件实践

本文档结合本仓库(Now in Android)的真实结构,用通俗语言说明:官方所说的模块化(Modularization)在本项目里长什么样、feature 层的组件化约定、日常如何加模块

仓库地址:https://github.com/android/nowinandroid

术语说明

  • 模块化 :按职责拆成多个 Gradle 模块app / core / feature),与 官方 Modularization 一致。
  • 组件化(本文狭义) :在 feature 上用 api + implNavKey 做业务边界,功能之间不直接依赖对方界面。
    国内常把两者统称「组件化」;读 NIA 源码或英文文档时,请优先用 模块化 这个词。
    (本文档内容与官方用语对齐。)

如果你已经熟悉 Android 开发,但第一次接触多模块工程,按本文从「整体 → 规则 → 实操」阅读即可。

更详细的英文说明:ModularizationLearningJourney.md


一、模块化与组件化:分别指什么?

1.1 模块化(本项目的整体形态)

模块化 = 把原来塞在一个 app 里的代码,按职责拆成多个 Gradle 模块(每个模块可单独编译、测试、被依赖)。

可以把 App 想成一栋楼:

比喻 在本项目里对应
大楼外壳、电梯、总前台 app 模块:MainActivityNiaApp、底部导航、把各功能拼起来
一个个独立商铺(首页、书签、设置...) feature/* 功能模块
水电、网络、物业等公共设施 core/* 公共模块:数据、网络、主题、通用 UI 等
夜班保洁、性能测试 syncbenchmarks 等辅助模块

模块化带来的好处:

  • 编译更快:改一个功能时,往往只需重编相关模块。
  • 边界更清晰:按模块划分所有权,减少「全工程乱引用」。
  • 多人协作 :不同人负责不同 featurecore,冲突更少。
  • 可测试:小模块更容易写单元测试。

结论 :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 ------ 总装配车间

职责:

  • ApplicationNiaApplication)、MainActivity
  • 把各 feature 的导航入口注册到 NiaApp
  • 底部 Tab、顶层 AppBar 等「跨功能」的 UI 骨架

原则app 可以依赖所有需要的 featurecore,但业务逻辑不要堆在这里;具体页面应在 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 TopicNewsResource 等纯数据模型(尽量无 Android 依赖)
core:data TopicsRepository 等仓库,统一本地 + 网络数据
core:domain 用例层,组合 data,给 ViewModel 用
core:designsystem 主题、按钮、图标等设计系统
core:ui 跨功能的复合 UI(如新闻卡片列表)
core:navigation Navigator 等导航基础设施

原则core 可以依赖别的 core不能 依赖 featureapp

4. 其他模块

  • sync:work:WorkManager 后台同步
  • benchmarks:启动性能、Baseline Profile
  • app-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 的灵魂)

例如主题详情在 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

NiaAppentryProvider { ... } 把各功能的 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,而不是在 foryouinterests 各写一套网络请求。


七、如何新增一个功能模块?(实操清单)

假设要加一个叫 「通知中心 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:uicore: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)

NiaAppentryProvider 中增加:

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:foryoufeature:topic ...
  • 纵向impl 里的 UI → core:domain / core:datacore: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


十一、自检:你的拆分是否合理?

加模块或改依赖前,可以问自己:

  1. 这个类是否会被 第二个功能 使用?→ 是则考虑下沉到 core
  2. 是否只是为了 打开另一个页面 ?→ 只依赖对方 api 的 NavKey / navigateToXxx
  3. core 里是否出现了 某个功能独有的文案或布局 ?→ 可能放错了层,应回 feature:impl
  4. 依赖箭头是否 指向 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-logicincludeBuild("../nowinandroid-main/build-logic"))。
  • 复用源码请遵守 Apache 2.0(保留文件头版权)。

13.1 方式一:在 NIA 仓库里加新 App(推荐先试)

本仓库 app-nia-catalog 已是范例:只依赖 core:designsystemcore:ui,不引 feature

步骤:

  1. 复制 app-nia-catalog/ → 例如 app-my-shop/
  2. settings.gradle.kts 增加:include(":app-my-shop")
  3. applicationIdnamespace
  4. 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)
}
  1. 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 时要挂哪些模块?

目标 建议挂载 参考
NiaThemeNiaButton 等设计系统 :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,宿主需要:

  • @HiltAndroidAppApplication
  • 依赖能提供 Repository 的 core:data 等(与 NIA 主 App 一致的 DI 图,或自行写 Test 实现替换)

4. flavor

所有从 NIA 引来的模块仍要选 demomissingDimensionStrategy("contentType", "demo")

结论 :跨 App 复用整块 feature ≈ 嵌入一个小子产品;多数团队 只复用 core,业务页在自己工程里按同样 api/impl 模式重写。


13.5 配置检查清单

  • includeBuildbuild-logic
  • libs.versions.toml 来自 NIA 或版本兼容
  • 每个 projects.xxx 都在 settings.gradle.ktsincludeFromNia
  • 宿主 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 没有界面 必须再引 implxxxEntry 注册
插件 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.ktsapp 依赖并注册 xxxEntry
其他 App 用 NIA 模块 同仓库加 App,或独立工程 includeFromNia + flavor 对齐;feature 还要导航与 Hilt

本仓库是 模块化工程 ,并在 feature 上采用了 组件化边界 ;你在自己的业务里可以调整模块粒度,但建议保持 依赖方向api/impl 契约 。其他 App 引用 core/feature第十三节

相关推荐
_qingche5 小时前
H2 数据库到 MySQL 数据迁移
java·数据库·spring boot·mysql·spring·重构·kotlin
summerkissyou19875 小时前
Android-UI-获取屏幕尺寸的方法
android·ui
用户86022504674725 小时前
Kotlin 函数式编程入门与实践指南
android
最爱睡觉睡觉睡觉7 小时前
CSS → Flutter 对照手册
android·前端
xingpanvip7 小时前
星盘接口开发文档:马盘次限盘接口指南
android·开发语言·python·php·lua
用户26190498561578 小时前
JUnit4 完整配置流程
android
用户26190498561578 小时前
JaCoCo 完整配置流程
android
QING6189 小时前
Android面试 —— 八股文之app启动流程
android·面试·app
海鸥-w9 小时前
python(fastapi) 实现更新,新增,删除接口
android·python·fastapi