# Feature Registration Và Navigation Boundaries

> Hiểu app biết có những feature nào qua danh sách tường minh, và điều hướng giữa chúng mà không tạo coupling.

- URL: https://dantech.academy/courses/kotlin-accelerator/engineer-builder/01-production-app-map/lesson-03-understand-feature-discovery
- 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: 8 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-03-understand-feature-discovery.md

## Mục tiêu

- Trace hai đường đăng ký: appFeatureInitializers nạp Koin modules và appFeatureNavigationProviders nạp routes
- Giải thích vì sao feature request navigation qua callbacks thay vì import feature khác
- Giải thích vì sao reflection không dùng được trong project KMP và AppFeaturesTest thay thế vai trò kiểm tra

---

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

Câu hỏi mở đầu: `AppNavHost` lấy menu screen từ đâu?

Bài này trả lời hai câu: app biết có những feature nào bằng cách nào, và một feature mở feature khác ra sao mà không biến chúng thành dependency của nhau. Bạn cũng sẽ thấy vì sao repo bỏ cách "tự động phát hiện feature bằng reflection" khi chuyển sang Kotlin Multiplatform, và bài học đó áp dụng cho mọi project KMP.

## Nhìn Toàn Cảnh Trước

App có ba feature module:

```text
:feature:game
:feature:menu
:feature:paywall
```

`:feature:paywall` đi theo đúng cơ chế bên dưới, nhưng premium gate mặc định tắt nên bài này lấy `game` và `menu` làm ví dụ.

Một feature muốn xuất hiện trong app phải đóng góp hai thứ:

1. Koin modules để tạo ViewModels và helpers của feature.
2. Navigation registration để root `NavHost` biết hiển thị screen nào.

```text
feature module
-> cung cấp FeatureModuleInitializer (Koin modules)
-> cung cấp FeatureNavigationProvider (routes)
-> :shared-app liệt kê cả hai trong AppFeatures.kt
-> feature dùng được trên Android và iOS
```

## Phần 1: Danh Sách Feature Viết Tường Minh

Mở `shared-app/src/commonMain/kotlin/com/creative/kta/shared/AppFeatures.kt`:

```kotlin
val appFeatureInitializers: List<FeatureModuleInitializer> = listOf(
    GameFeatureInitializer(),
    MenuFeatureInitializer(),
    PaywallFeatureInitializer(),
)

val appFeatureNavigationProviders: List<FeatureNavigationProvider> = listOf(
    GameNavigationProvider(),
    MenuNavigationProvider(),
    PaywallNavigationProvider(),
)
```

Hai list này là toàn bộ tuyên bố của app về việc có những feature nào. Thêm feature nghĩa là thêm một dòng vào mỗi list.

File nằm ở `:shared-app` vì đây là module duy nhất depend vào mọi feature, và cả Android lẫn iOS đều đi qua nó. `:app` không depend vào feature nào.

## Phần 2: Đường Thứ Nhất, Koin Modules

Mở:

```text
core/src/commonMain/kotlin/com/creative/kta/core/di/FeatureModuleInitializer.kt
core/src/commonMain/kotlin/com/creative/kta/core/di/FeatureRegistry.kt
```

Contract rất nhỏ:

```kotlin
interface FeatureModuleInitializer {
    val koinModules: List<Module>
}
```

`FeatureRegistry.koinModules(initializers)` gộp `koinModules` của mọi initializer thành một list. Nó `require` list không rỗng và không có initializer trùng, nên lỗi cấu hình nổ ngay lúc khởi động thay vì âm thầm. `appKoinModules` trong `AppKoinModules.kt` gọi hàm này với `appFeatureInitializers`.

Mở `feature/game/src/commonMain/kotlin/com/creative/kta/feature/game/di/GameFeatureModule.kt` và `feature/menu/.../menu/di/MenuFeatureModule.kt`:

| Feature | Provides                                                                                                                |
| ------- | ----------------------------------------------------------------------------------------------------------------------- |
| `game`  | `GameTimer`, `LifelineHelper`, `GameOverlayQueueManager`, `GameFeedbackController`, `GameStateMachine`, `GameViewModel` |
| `menu`  | `MenuViewModel`, `SettingViewModel`, `LeaderboardViewModel`, `MenuOnboardingViewModel`                                  |

Cuối `GameFeatureModule.kt`:

```kotlin
class GameFeatureInitializer : FeatureModuleInitializer {
    override val koinModules: List<Module> = listOf(gameFeatureModule)
}
```

`gameFeatureModule` là `internal`, nên ngoài `:feature:game` không ai đụng được vào nó. Thứ duy nhất feature công khai ra ngoài là initializer. ViewModel của feature nằm trong feature module, nên khi một ViewModel không inject được, bạn biết ngay feature nào chịu trách nhiệm.

`:feature:menu` chứa bốn sub-feature (menu, leaderboard, settings, onboarding), nên `MenuFeatureModule` provide bốn ViewModel. Một Gradle module, một initializer, nhiều flow.

## Phần 3: Đường Thứ Hai, Navigation

Mở `core/src/commonMain/kotlin/com/creative/kta/core/navigation/FeatureNavigationRegistry.kt`. File chứa ba contract:

```kotlin
interface FeatureNavigationRegistrar {
    fun NavGraphBuilder.registerComposable(callbacks: FeatureNavigationCallbacks)
}

interface FeatureNavigationProvider {
    val featureNavigationRegistrar: FeatureNavigationRegistrar
}
```

cùng `FeatureNavigationCallbacks` ở Phần 5, và object `FeatureNavigationRegistry` với hàm `registrars(providers)` kiểm tra không rỗng, không trùng.

Cơ chế:

1. Mỗi feature có một `FeatureNavigationProvider` trả về một `FeatureNavigationRegistrar`.
2. Registrar biết cách thêm routes của feature vào `NavGraphBuilder`.
3. `AppNavHost` gọi `FeatureNavigationRegistry.registrars(appFeatureNavigationProviders)`.
4. Trong `NavHost`, mỗi registrar gọi `registerComposable(navigationCallbacks)`.

Providers nằm ở `GameNavigation.kt` và `MenuNavigation.kt` trong package `navigation` của từng feature. Feature sở hữu cách đăng ký destination của nó. `AppNavHost` sở hữu `NavController` duy nhất.

## Phần 4: Route Data Nằm Trong `:core`

Mở `core/src/commonMain/kotlin/com/creative/kta/core/navigation/Routes.kt`:

```kotlin
@Serializable
data object MenuRouteData

@Serializable
data object GameRouteData

@Serializable
data object PaywallRouteData
```

Chúng `@Serializable` vì app dùng typed navigation của Navigation Compose (bản multiplatform của JetBrains) thay vì string route.

`:core` là nơi đặt route contract mà nhiều module cần hiểu. `:feature:menu` cần biết `GameRouteData` tồn tại, nhưng không cần biết `GameScreen` trông ra sao.

## Phần 5: Callbacks Ngăn Feature Phụ Thuộc Lẫn Nhau

```kotlin
interface FeatureNavigationCallbacks {
    fun navigateToMenu()
    fun navigateToGame(category: String? = null, isPractice: Boolean = false)
    fun navigateToDailyGame()
    fun navigateToPaywall()
    fun navigateBack()
}
```

Feature chỉ nói ý định. Root host quyết định cách thực hiện. Phần thực hiện nằm trong `shared-app/src/commonMain/kotlin/com/creative/kta/shared/navigation/AppNavigationCallbacks.kt`: class `AppNavigationCallbacks` implement contract này, và `AppNavHost` tạo nó qua `rememberNavigationCallbacks(navController, premiumGate)`.

```kotlin
override fun navigateToGame(category: String?, isPractice: Boolean) {
    navController.navigate(GameRouteData)
}
```

`navigateToPaywall` còn là nơi duy nhất kiểm tra `PremiumGateConfig.isEnabled`. Gate tắt thì callback bỏ qua lệnh điều hướng. Một chỗ kiểm tra áp dụng cho mọi nút mở paywall, trên cả hai nền tảng.

### Theo Dấu Một Lần Chuyển Màn Hình

```text
User bấm Play ở Menu
-> MenuViewModel, qua MenuNavigationSideEffectHandler, emit MenuUiEvent.NavigateToGame
-> MenuRoute gọi onNavigateToGame(...)
-> MenuNavigationProvider đã nối onNavigateToGame = callbacks::navigateToGame
-> AppNavigationCallbacks.navigateToGame chạy navController.navigate(GameRouteData)
-> GameNavigationProvider đã register composable<GameRouteData>
-> GameRoute render game
```

Những gì không xảy ra trong flow trên:

- `:feature:menu` không import `GameScreen`. Bạn viết lại toàn bộ UI của game mà menu không phải sửa dòng nào.
- `:feature:menu` không tạo `NavController` riêng. Chỉ có một `NavHostController` ở root, nên back stack không bị rối.
- `:feature:menu` không biết game có transition riêng. `GameNavigation.kt` tự khai báo `enterTransition` và `popExitTransition` cho `GameRouteData`.

## Phần 6: Vì Sao Repo Bỏ Reflection

Trước khi chuyển sang KMP, app tìm feature bằng reflection: `FeatureRegistry` giữ tên class dạng chuỗi và gọi `Class.forName(...)`, kèm keep rules trong `app/proguard-rules.pro` để R8 không xoá các class đó.

Cách đó có ba vấn đề:

| Vấn đề                         | Hậu quả                                                                                          |
| ------------------------------ | ------------------------------------------------------------------------------------------------ |
| Compiler không kiểm tra chuỗi  | Rename `GameFeatureInitializer` thì chuỗi vẫn giữ tên cũ. Code compile sạch, feature biến mất lúc chạy |
| R8 không thấy ai gọi class     | Thiếu keep rule thì release build mất feature trong khi debug vẫn chạy                           |
| Kotlin/Native không có `Class.forName` | Trên iOS, cách này không chạy được                                                       |

Vấn đề thứ ba quyết định tất cả. Code trong `commonMain` không dùng được API reflection của JVM. List tường minh trong `AppFeatures.kt` giải quyết cả ba: compiler kiểm tra tên class, R8 thấy class được tham chiếu nên giữ lại mà không cần keep rule, và code chạy giống nhau trên mọi nền tảng.

Bài học mang theo được: khi một cơ chế dựa vào JVM (reflection, `ServiceLoader`, annotation processing lúc runtime), nó không đi theo bạn sang Kotlin/Native. Trong project KMP, ưu tiên wiring tường minh mà compiler kiểm tra được.

Cái giá của list tường minh là quên thêm một dòng thì feature không xuất hiện. `AppFeaturesTest` (`shared-app/src/commonTest/`) chặn chuyện đó: nó kiểm tra mỗi list có đúng ba entry, chứa game, menu, paywall, và không entry nào trùng.

## Phần 7: Debug Theo Hành Vi

| Hành vi                                            | Nơi kiểm tra trước                                                    |
| -------------------------------------------------- | --------------------------------------------------------------------- |
| ViewModel không inject được                        | Koin module của feature, `appFeatureInitializers`, rồi `AppGraphTest` |
| Một screen không bao giờ xuất hiện                 | `*Navigation.kt` của feature, rồi `appFeatureNavigationProviders`     |
| App crash lúc khởi động với lỗi trùng hoặc rỗng    | `FeatureRegistry` hoặc `FeatureNavigationRegistry` vừa `require` fail |
| Bấm nút paywall không có gì xảy ra                 | Premium gate đang tắt, xem `navigateToPaywall` trong `AppNavigationCallbacks.kt` |
| Feature khác chỉ mở được route bằng cách import UI | Thay import trực tiếp bằng callback cộng route contract trong `:core` |

## Phần 8: Checklist Khi Thêm Feature Mới

1. Tạo module với `alias(libs.plugins.kta.kmp.library)`, thêm hai plugin Compose nếu có UI, và đặt `namespace` trong `kotlin { androidLibrary { ... } }`.
2. Thêm `include(":feature:<tên>")` vào `settings.gradle.kts`.
3. Khai báo dependency của feature: `:core`, `:domain`, `:theme`, `:ui-components`... Không depend vào feature khác.
4. Thêm `implementation(project(":feature:<tên>"))` vào `shared-app/build.gradle.kts`.
5. Tạo Koin module `internal` và một class implement `FeatureModuleInitializer`.
6. Thêm route data vào `Routes.kt` trong `:core` nếu module khác cần điều hướng tới feature.
7. Implement `FeatureNavigationProvider`.
8. Thêm initializer và provider vào hai list trong `AppFeatures.kt`.
9. Cập nhật `AppFeaturesTest`, rồi chạy `./gradlew :shared-app:testAndroidHostTest`.

Bỏ bước 4 thì `AppFeatures.kt` không compile. Bỏ bước 8 thì feature không bao giờ xuất hiện, và chỉ `AppFeaturesTest` đã cập nhật ở bước 9 mới bắt được lỗi này.

## Anti-Patterns

| Anti-pattern                                       | Vì sao hại                                                       |
| -------------------------------------------------- | ---------------------------------------------------------------- |
| Import screen của feature khác                     | Tạo coupling giữa các feature                                    |
| Viết mọi route registration trong `AppNavHost`     | `:shared-app` phình to mỗi khi feature thay đổi                  |
| Truyền `NavController` vào mọi screen              | UI biết quá nhiều về navigation                                  |
| Tạo string route ở nhiều file                      | Mất kiểm tra kiểu của typed navigation                           |
| Đặt danh sách feature trong `:app`                 | iOS không có `:app`, nên iOS sẽ không có feature                 |
| Tìm feature bằng reflection trong code chung       | Không có trên Kotlin/Native, và compiler không kiểm tra được      |

## Checkpoint

Bạn hiểu bài này khi trả lời được:

- Class nào provide Koin bindings của game, và file nào đưa nó vào graph?
- Class nào register navigation của game, và hàm nào gọi nó?
- `GameRouteData` nằm ở module nào, và vì sao không nằm trong `:feature:game`?
- Menu mở game bằng cách nào mà không import class nào của game?
- Vì sao `Class.forName` không dùng được trong project này, và cái gì thay thế nó?

---

- Bài trước: [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)
- Bài tiếp theo: [Trách Nhiệm Từng Layer Và Cách Đọc Tên File](https://dantech.academy/courses/kotlin-accelerator/engineer-builder/01-production-app-map/lesson-04-spot-layer-responsibilities.md)
