# Đọc Module Graph

> Đọc project Kotlin Multiplatform như một hệ thống: module, chiều dependency và source set.

- URL: https://dantech.academy/courses/kotlin-accelerator/engineer-builder/01-production-app-map/lesson-01-read-module-graph
- Khóa học: [Mobile Action Pro](https://dantech.academy/courses/kotlin-accelerator.md)
- Module: Kiến trúc Production mobile app
- Thời lượng: 6 phút
- Độ khó: beginner
- Truy cập: Miễn phí, đọc toàn bài
- English: https://dantech.academy/en/courses/kotlin-accelerator/engineer-builder/01-production-app-map/lesson-01-read-module-graph.md

## Mục tiêu

- Đọc settings.gradle.kts để thấy 14 module thật và nhận ra folder nào không thuộc build
- Giải thích mỗi module sở hữu phần gì, vì sao :shared-app giữ danh sách feature, và chiều dependency giữa các module
- Phân biệt commonMain, androidMain, iosMain và nhận ra module được khai báo qua convention plugin kta.kmp.library

---

## Mục Tiêu Bài Học

Bạn đọc được bản đồ của một production app Kotlin Multiplatform (KMP): module nào tồn tại, mỗi module sở hữu phần gì, dependency đi theo chiều nào, và code nào chạy trên nền tảng nào.

Sau bài này, nhìn vào một screen mới, một use case, một repository implementation hay một shared UI component, bạn biết module nào nên sở hữu nó. Khi nhận một project KMP khác, bạn biết đọc file nào trước để dựng lại bản đồ đó.

## Vì Sao Phải Đọc Module Graph Trước

Trước khi tìm hiểu một dự án, tôi luôn xem họ tổ chức module thế nào. Có dự án dùng single module, có dự án chia module theo business domain, có dự án chia theo layer (feature, domain, data). Mỗi cách có ưu và nhược điểm riêng, và lựa chọn phụ thuộc vào dự án và đội ngũ.

Project KMP có thêm một trục nữa: code nào dùng chung cho mọi nền tảng, code nào chỉ dành cho Android hoặc iOS. Folder trên đĩa không trả lời được hai câu hỏi này. Build file mới trả lời được.

## Phần 1: Folder Không Phải Module

Mở `settings.gradle.kts`. Danh sách `include(...)` có 14 module:

```text
:app
:dre-core
:dre-viewmodel
:kernel
:core
:data
:domain
:secrets
:theme
:ui-components
:feature:game
:feature:menu
:feature:paywall
:shared-app
```

Nhìn cây thư mục ở root, bạn còn thấy `build-logic/`, `iosApp/`, `ai/`, `scripts/`, `graphics/`, `docs/`... Trong số đó, chỉ `build-logic/` tham gia build Gradle, qua dòng `includeBuild("build-logic")`. `iosApp/` là Xcode project: nó dùng output của Gradle nhưng không phải Gradle module.

| Loại folder                                 | Ý nghĩa                                                       |
| ------------------------------------------- | ------------------------------------------------------------- |
| Có trong `include(...)`                     | Gradle module thật                                            |
| `includeBuild("build-logic")`               | Composite build cung cấp convention plugin                    |
| `iosApp/`                                   | App shell iOS, Xcode build nó và link với framework Kotlin    |
| Không xuất hiện trong `settings.gradle.kts` | Không thuộc build. Đừng đọc chúng khi đang trace một bug của app |

Quy tắc: `settings.gradle.kts` là source of truth. Với mọi project Gradle (Android thuần hay KMP), đọc file này đầu tiên.

## Phần 2: Mỗi Module Sở Hữu Phần Gì

| Module                         | Sở hữu phần gì                                                                                     | File nên mở đầu tiên                                 |
| ------------------------------ | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `:app`                         | Android entry point: `Application`, `Activity`, WorkManager, notification channels                 | `App.kt`, `LauncherActivity.kt`                      |
| `iosApp/`                      | iOS entry point bằng Swift, rất mỏng                                                               | `iOSApp.swift`, `ContentView.swift`                  |
| `:shared-app`                  | Phần app dùng chung cho cả hai nền tảng: khởi động, danh sách feature, root navigation, Splash. Build ra framework `Shared` cho iOS | `StartSharedApp.kt`, `AppFeatures.kt`, `AppNavHost.kt` |
| `:core`                        | Contract dùng chung giữa các module: route data, feature registries, base types, Compose resources | `FeatureRegistry.kt`, `Routes.kt`                    |
| `:kernel`                      | Nền tảng kỹ thuật thấp nhất, ví dụ dispatchers                                                     | `QzDispatchers.kt`                                   |
| `:dre-core`, `:dre-viewmodel`  | Thư viện state management nội bộ: `Reducer`, `SideEffectHandler`, `DreStoreViewModel`              | `Reducer.kt`, `DreStoreViewModel.kt`                 |
| `:domain`                      | Use cases, repository và service contracts, business models                                        | `domain/.../usecase/`, `domain/.../service/`         |
| `:data`                        | Implementation cho các contract: Supabase, Ktor, DataStore, RevenueCat...                          | `DataModule.kt`                                      |
| `:secrets`                     | Ranh giới truy cập secrets                                                                         | `SecretManager.kt`                                   |
| `:theme`                       | Design system tokens và app theme                                                                  | `QzdsTheme.kt`, `QzdsColorTokens.kt`                 |
| `:ui-components`               | Compose components dùng chung giữa nhiều screen                                                    | `ui-components/.../ui/component/`                    |
| `:feature:*`                   | User flows: screens, feature ViewModels, navigation entry của feature                             | `feature/game`, `feature/menu`                       |

`:feature:paywall` có trong build nhưng premium gate mặc định tắt, nên app không điều hướng tới paywall.

## Phần 3: Chiều Dependency

Mở `app/build.gradle.kts` và `shared-app/build.gradle.kts`, rồi so phần dependencies:

```text
:app        -> :shared-app, :domain, :secrets
               (debug: :theme, :ui-components, :feature:paywall; test: :core)
:shared-app -> :feature:game, :feature:menu, :feature:paywall, :core, :data, :domain, ...
:feature:*  -> :core, :domain, :kernel, :theme, :ui-components
               (game và menu thêm :dre-core, :dre-viewmodel)
:data       -> :domain, :core, :kernel, :secrets
:domain     -> :dre-core
```

```mermaid
%%{init: {"theme": "base", "themeVariables": {"background": "transparent", "primaryColor": "#1e293b", "primaryTextColor": "#e2e8f0", "primaryBorderColor": "#475569", "secondaryColor": "#1e293b", "tertiaryColor": "#0f172a", "lineColor": "#64748b", "textColor": "#e2e8f0", "clusterBkg": "#0f172a", "clusterBorder": "#475569", "titleColor": "#cbd5e1", "edgeLabelBackground": "#334155", "fontSize": "15px"}, "flowchart": {"wrappingWidth": 420}}}%%
flowchart TB
    classDef ui fill:#1e293b,stroke:#3b82f6,stroke-width:2px,color:#e2e8f0
    classDef vm fill:#1e293b,stroke:#10b981,stroke-width:2px,color:#e2e8f0
    classDef dom fill:#1e293b,stroke:#f59e0b,stroke-width:2px,color:#e2e8f0
    classDef data fill:#1e293b,stroke:#8b5cf6,stroke-width:2px,color:#e2e8f0
    classDef root fill:#1e293b,stroke:#94a3b8,stroke-width:2px,color:#e2e8f0
    classDef bad fill:#1e293b,stroke:#ef4444,stroke-width:2px,color:#e2e8f0
    classDef good fill:#1e293b,stroke:#10b981,stroke-width:2px,color:#e2e8f0
    classDef note fill:#0f172a,stroke:#475569,stroke-dasharray:4 3,color:#cbd5e1
    accTitle: Module graph của repo
    accDescr: Mũi tên nghĩa là phụ thuộc vào. app phụ thuộc shared-app. shared-app phụ thuộc ba feature module và data. Feature phụ thuộc domain, core, kernel, theme, ui-components, không phụ thuộc data. data phụ thuộc domain, core, kernel, secrets. domain chỉ phụ thuộc dre-core.

    APP[":app<br/>Android"]:::root --> SHARED[":shared-app<br/>shell chung, danh sách feature"]:::root
    FEAT[":feature:game<br/>:feature:menu<br/>:feature:paywall"]:::ui
    SHARED --> FEAT
    SHARED --> DATA[":data"]:::data
    FEAT --> DOMAIN[":domain"]:::dom
    DATA --> DOMAIN
    subgraph UIB["UI nền"]
        UIC[":ui-components"]:::ui --> THEME[":theme"]:::ui
    end
    FEAT --> UIB
    FEAT --> CORE[":core"]:::root
    FEAT --> KERNEL[":kernel"]:::root
    DATA --> CORE
    DATA --> KERNEL
    DATA --> SECRETS[":secrets"]:::root
    CORE --> KERNEL
    FEAT --> DREVM[":dre-viewmodel"]:::vm
    DREVM --> DRECORE[":dre-core"]:::vm
    DOMAIN --> DRECORE
    DRECORE ~~~ NOTE["Mũi tên = phụ thuộc vào.<br/>Không có mũi tên Feature -> :data.<br/>Lược bớt: :app còn phụ thuộc thẳng :domain, :secrets;<br/>:shared-app còn phụ thuộc thẳng các module nền;<br/>:ui-components dùng :core;<br/>chỉ :feature:game và :feature:menu dùng :dre-*."]:::note
```

Hai điểm đáng nhớ:

- `:app` không depend vào feature module nào (trừ `debugImplementation(project(":feature:paywall"))` cho một màn hình debug). `:app` cũng không khai báo `:data`: `:data` vào APK qua `:shared-app`. `:shared-app` mới là nơi biết danh sách feature. Lý do: iOS không có `:app`. Nếu danh sách feature nằm trong `:app`, iOS sẽ không có feature nào.
- Feature không depend vào `:data` và không depend vào nhau. Feature chỉ biết contract trong `:domain` và `:core`. Menu mở được màn game mà không import class nào của game, nhờ navigation contract trong `:core` và danh sách provider ở `:shared-app`.

Mũi tên chỉ đi một chiều, từ app shell xuống feature, xuống domain. Khi Gradle từ chối cho bạn import một class, thường đó là boundary đang làm đúng việc của nó.

## Phần 4: Một Module, Nhiều Source Set

Mở `feature/menu/src/`. Bạn thấy `commonMain`, `commonTest` và `androidHostTest`. Mở `data/src/`, bạn thấy thêm `androidMain` và `iosMain`.

| Source set        | Chạy ở đâu                    | Ví dụ trong repo                                                    |
| ----------------- | ----------------------------- | ------------------------------------------------------------------- |
| `commonMain`      | Android và iOS                | Toàn bộ screens, ViewModels, use cases, `DataModule.kt`             |
| `androidMain`     | Chỉ Android                   | `DataModule.android.kt`: Firebase, `SoundPoolAudioManager`          |
| `iosMain`         | Chỉ iOS                       | `DataModule.ios.kt`: `AvAudioManager`, `NoOpAnalyticsManager`       |
| `commonTest`      | JVM và iOS Simulator          | Test của logic dùng chung                                           |
| `androidHostTest` | JVM trên máy bạn              | Test cần thư viện chỉ có trên JVM, ví dụ MockK                      |

Phần lớn code nằm trong `commonMain`. Code chỉ rơi xuống `androidMain` hoặc `iosMain` khi nó phải gọi SDK của một nền tảng. Ngay cả khi đó, contract vẫn nằm trong `commonMain` và mỗi nền tảng cung cấp một implementation qua Koin.

Feature module của repo này không có `androidMain` hay `iosMain`. Toàn bộ UI viết một lần bằng Compose Multiplatform.

## Phần 5: Tên Module Có Thể Nói Dối Về Scope

Mở `feature/menu/src/commonMain/kotlin/com/creative/kta/feature/`. Bạn sẽ không chỉ thấy `menu/`:

```text
feature/menu/.../feature/
├── menu/          Menu screen, MenuViewModel, dialog layer
├── settings/      Settings screen, time picker, SettingViewModel
├── leaderboard/   Leaderboard popup, LeaderboardViewModel
└── onboard/       Onboarding overlay, MenuOnboardingViewModel
```

Một Gradle module đang chứa bốn sub-feature. Cả bốn flow đều bắt đầu từ menu, dùng chung dependency và luôn ship cùng nhau. Tách thành bốn module sẽ tăng build time và độ phức tạp mà không đổi lại gì.

Một module có thể chứa nhiều feature, miễn là chúng gắn chặt với nhau.

## Phần 6: Hiểu Boundary Giữa `:core`, `:domain` Và `:data`

| Nhu cầu                                                         | Module    |
| --------------------------------------------------------------- | --------- |
| Route data class, feature registry, base type dùng chung        | `:core`   |
| Dispatchers và các primitive kỹ thuật thấp nhất                 | `:kernel` |
| Business operation hoặc interface mà feature cần                | `:domain` |
| Code nói chuyện với Supabase, Ktor, DataStore, SDK nền tảng     | `:data`   |

Ví dụ: `AuthService` là contract, nằm ở `:domain`. `AuthServiceSupabaseImpl` là implementation, nằm ở `:data`. Feature yêu cầu authentication mà không cần biết Supabase.

`:domain` được giữ thuần Kotlin: không Compose, không Koin, không Ktor, không Supabase. Nhờ vậy nó compile cho mọi target mà không cần gì thêm, và test của nó chạy nhanh.

Trong thực tế, ranh giới domain và data không phải lúc nào cũng tuyệt đối. Quá khắt khe dễ dẫn tới over-engineering. Hãy cân nhắc giữa maintainability và tốc độ ship feature.

## Phần 7: Hiểu UI Ownership

- `:feature:*` sở hữu product flows: menu, game, settings, leaderboard, onboarding.
- `:ui-components` sở hữu building blocks dùng lại: buttons, dialogs, auth form components, game answer options, menu widgets.
- `:theme` sở hữu color, typography, spacing, shape, elevation, motion.

Composable chỉ có nghĩa trong một flow thì giữ trong feature module. Nhiều flow cần nó thì đưa vào `:ui-components`.

## Phần 8: Module Được Khai Báo Như Thế Nào

Mở `feature/game/build.gradle.kts`:

```kotlin
plugins {
    alias(libs.plugins.kta.kmp.library)
    alias(libs.plugins.compose.multiplatform)
    alias(libs.plugins.kotlin.compose)
}
```

`kta.kmp.library` không phải plugin của Google hay JetBrains. Nó là convention plugin của chính repo, class `KmpLibraryConventionPlugin` trong `build-logic/`. Plugin này áp cho mọi module trừ `:app`, và đặt một lần cho tất cả:

- plugin Kotlin Multiplatform và plugin Android KMP library của AGP 9,
- `jvmToolchain(17)`, `compileSdk`, `minSdk`,
- target Android cùng hai target iOS `iosArm64()` và `iosSimulatorArm64()`,
- host test cho Android (`withHostTest`), nguồn gốc của task `testAndroidHostTest`,
- detekt.

Vì vậy build file của mỗi module rất ngắn: nó khai báo `namespace` trong `kotlin { androidLibrary { ... } }` và danh sách dependency. Module UI thêm hai plugin Compose.

`build-logic/` vẫn còn các plugin `kta.android.library`, `kta.android.feature` và `kta.android.compose` từ thời project còn là Android thuần. Hiện không module nào dùng chúng. Khi đọc một project lạ, kiểm tra plugin nào thật sự được áp trong build file, đừng suy ra từ những gì tồn tại trong `build-logic/`.

Muốn hiểu cấu hình của mọi module, đọc `KmpLibraryConventionPlugin` một lần. Đây là lợi ích chính của convention plugin, và bạn sẽ gặp lại mô hình này ở hầu hết project KMP nhiều module.

## Phần 9: Phân Loại Code Trước Khi Thêm Code

Trước khi thêm code mới, tự hỏi:

1. Đây là entry point của một nền tảng? `:app` cho Android, `iosApp/` cho iOS. Giữ cả hai thật mỏng.
2. Đây là wiring dùng chung cho cả app (khởi động, danh sách feature, root navigation)? `:shared-app`.
3. Đây là contract kỹ thuật dùng chung giữa các module? `:core`, hoặc `:kernel` nếu nó ở tầng thấp nhất.
4. Đây là business rule hoặc use case? `:domain`.
5. Đây là implementation gọi dịch vụ bên ngoài? `:data`.
6. Đây là UI primitive dùng lại? `:ui-components`.
7. Đây là product flow hoặc screen? `:feature:*` tương ứng.
8. Đây là visual token? `:theme`.
9. Đây là secret handling? Phía sau `:secrets`.

Sau khi chọn module, hỏi thêm: code này có gọi SDK của riêng một nền tảng không? Không thì đặt trong `commonMain`. Có thì đặt contract trong `commonMain` và implementation trong `androidMain`/`iosMain`.

Checklist này tránh lỗi phổ biến: bỏ mọi thứ vào `:app` chỉ vì dễ tìm. Trong một project KMP, code bỏ vào `:app` là code iOS không bao giờ thấy.

## Checkpoint

Mở `settings.gradle.kts`, rồi mở một file từ `:app`, `:shared-app`, `:domain`, `:data` và một module `:feature:*`.

Bạn hoàn tất bài này khi giải thích được bằng lời của mình:

- vì sao `scripts/` và `iosApp/` không có trong `settings.gradle.kts`,
- vì sao danh sách feature nằm trong `:shared-app` thay vì `:app`,
- màn hình leaderboard nằm trong Gradle module nào,
- vì sao `DataModule.android.kt` và `DataModule.ios.kt` cùng tồn tại,
- vì sao build file của các feature module ngắn như vậy.

---

- Bài trước: [Chạy App Trên iOS (macOS + Xcode)](https://dantech.academy/courses/kotlin-accelerator/engineer-builder/00-setup-app-running/lesson-06-run-app-on-ios.md)
- Bài tiếp theo: [Luồng Khởi Động Trên Android Và iOS](https://dantech.academy/courses/kotlin-accelerator/engineer-builder/01-production-app-map/lesson-02-trace-app-startup.md)
