# Trách Nhiệm Từng Layer Và Cách Đọc Tên File

> Đừng trộn UI, business logic và backend code. Và học cách đoán vị trí file chỉ từ tên của nó.

- URL: https://dantech.academy/courses/kotlin-accelerator/engineer-builder/01-production-app-map/lesson-04-spot-layer-responsibilities
- 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: 7 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-04-spot-layer-responsibilities.md

## Mục tiêu

- Đặt đúng trách nhiệm vào từng layer: UI, Route, ViewModel, reducer, use case, repository, service
- Phân biệt repository contract (truy cập dữ liệu) với service contract (capability) và data implementation
- Đọc naming convention của codebase để đoán vị trí và vai trò của một file trước khi mở nó

---

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

Trong app thật, viết được tính năng mới là một nửa. Nửa còn lại là biết trách nhiệm đó nên nằm ở layer nào.

Bài này giúp bạn phân biệt trách nhiệm của UI, Route, ViewModel, reducer/state machine, use case, repository, service, data implementation và shared UI layers.

Phần sau của bài dạy một kỹ năng ít được nói tới: đọc tên file để đoán vai trò và vị trí của nó. Codebase này đặt tên rất nhất quán. Khi nắm được quy ước, bạn tìm code nhanh hơn nhiều so với search mò.

Trong một project Kotlin Multiplatform, câu hỏi "layer nào" đi kèm câu hỏi thứ hai: code này chạy chung cho mọi nền tảng, hay chỉ trên Android hoặc iOS? Phần 5 trả lời câu đó.

## Vấn Đề Thật Sự

Trong app nhỏ, bạn có thể viết gần như mọi thứ vào `ViewModel` và app vẫn chạy. Trong production app, cách đó bắt đầu khó khăn.

Bạn sửa một bug sign-in nhỏ nhưng phải mở UI, ViewModel, Supabase client, analytics, navigation và local storage. Bạn thêm một button nhưng vô tình chạm business rule. Bạn đổi provider AI hint nhưng game screen cũng phải sửa.

Lúc đó các layer không còn ownership rõ ràng.

Đây cũng là điểm nghẽn hay gặp khi vibe coding: build app rất nhanh, nhưng prompt không nói rõ trách nhiệm của từng layer, nên code dồn hết vào một chỗ.

## Phần 1: Layer Map

Dùng bảng này khi bạn không chắc nên đặt code ở đâu:

| Layer                        | Trách nhiệm chính                                                               | Không nên làm gì                                           |
| ---------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| UI composable                | Render `UiState`, nhận input, gọi callbacks.                                    | Gọi backend, giữ business rule, tự quyết định data source. |
| Route composable             | Connect ViewModel với screen, collect one-time effects, nối navigation.         | Nhồi layout hoặc business logic.                           |
| ViewModel                    | Giữ screen state, nhận UI events, dispatch actions, gọi use case/service.       | Tự tạo HTTP client, tự biết Supabase/Firebase SDK detail.  |
| Reducer/state machine        | Biến current state cộng action thành next state và effect một cách predictable. | Gọi I/O, navigation, analytics hoặc backend trực tiếp.     |
| Use case                     | Diễn đạt một business operation cụ thể.                                         | Render UI hoặc biết Compose classes.                       |
| Repository contract          | Định nghĩa app cần đọc/ghi data gì.                                             | Chứa SDK/provider implementation detail.                   |
| Service contract             | Định nghĩa capability như auth, AI hint, audio, haptic, subscription.           | Trả về UI state hoặc phụ thuộc screen.                     |
| Data implementation          | Nói chuyện với Supabase, Firebase, RevenueCat, DataStore, Ktor, audio SDK của từng nền tảng. | Quyết định UI flow.                           |
| `:core`                      | Shared route contracts, base types, feature registries.                         | Sở hữu feature UI.                                         |
| `:kernel`                    | Primitive kỹ thuật thấp nhất, ví dụ `QzDispatchers`.                            | Phụ thuộc vào module khác của app.                         |
| `:ui-components` và `:theme` | Reusable composables, design tokens, spacing, typography, colors.               | Gọi ViewModel hoặc backend.                                |

Người học hay bỏ qua điểm này: Repository và Service khác nhau.

Repository trả lời câu hỏi "data này được lưu và đọc như thế nào?". Service trả lời "capability này được thực hiện như thế nào?".

Ví dụ `LeaderboardRepository` đọc bảng xếp hạng, còn `AuthService` xử lý sign-in và sign-out. Cả hai đều có contract ở `:domain` và implementation ở `:data`.

Reducer, side effect handler và ViewModel của các feature được xây trên thư viện nội bộ ở hai module `:dre-core` và `:dre-viewmodel`. `MenuReducer` implement interface `Reducer` của `:dre-core`, các handler implement `SideEffectHandler`, và `MenuViewModel` kế thừa `DreStoreViewModel` của `:dre-viewmodel`. Vì mọi feature dùng chung một bộ khung, shape của các thư mục ViewModel ở Phần 4 mới giống nhau đến vậy.

## Phần 2: Ví Dụ về Email Sign In

Trace flow này:

```text
Menu screen
-> MenuViewModel
-> SignInWithEmailUseCase
-> AuthService
-> AuthServiceSupabaseImpl
-> Supabase
```

Trách nhiệm đúng nên là:

| Piece                      | Owner                                                             |
| -------------------------- | ----------------------------------------------------------------- |
| Email form UI              | `:ui-components` auth components hoặc `:feature:menu` screen code |
| User bấm sign in           | `MenuRoute` hoặc `MenuScreen` callback vào `MenuViewModel`        |
| Loading/error/dialog state | `MenuViewModel` và reducer                                        |
| Business operation         | `SignInWithEmailUseCase` trong `:domain`                          |
| Auth capability            | `AuthService` contract trong `:domain`                            |
| Supabase detail            | `AuthServiceSupabaseImpl` trong `:data`                           |

Nếu `MenuViewModel` tự tạo Supabase client, nó đang ôm quá nhiều: screen state, business flow và transport detail.

Hậu quả rất cụ thể: đổi auth provider thì menu phải sửa. Thêm retry policy thì menu phải biết. Reuse sign-in ở onboarding thì copy logic.

## Phần 3: Ví Dụ về AI Hint

Trace flow này:

```text
Game feature
-> GameViewModel
-> lifeline/action handling
-> AIHintService
-> AIHintServiceImpl
-> GeminiHintProvider hoặc LocalHintProvider
-> GeminiHintClient (SupabaseGeminiHintClient), chỉ ở nhánh Gemini
-> Edge Function gemini-hint trên Supabase
-> Gemini API
```

Với user, đây chỉ là một hành động: "cho tôi một gợi ý". Với codebase, nó đi qua nhiều layer:

| Trách nhiệm                                    | Layer hợp lý                           |
| ---------------------------------------------- | -------------------------------------- |
| Render nút Ask AI                              | Game screen composable                 |
| Nhận user intent                               | `GameViewModel`                        |
| Quyết định game state đang cho phép hint không | Reducer/state machine                  |
| Diễn đạt capability "get hint"                 | `AIHintService` contract               |
| Chọn Gemini hay local fallback                 | Data implementation và provider wiring |

Screen nên render nút Ask AI. ViewModel nên điều phối state. Data layer mới biết request đi đâu và lỗi thì làm gì. `GeminiHintProvider` chỉ chạy khi flag `feature_ai_hints` trong Remote Config đang bật và người chơi đã đăng nhập. Nó gửi câu hỏi và các đáp án qua `SupabaseGeminiHintClient` tới Edge Function `gemini-hint`, và khi gặp lỗi tạm thời thì thử lại tối đa `max_retries` lần (đọc từ Remote Config). Gọi thất bại thì `AIHintServiceImpl` chuyển sang `LocalHintProvider`. Gemini API key không nằm trong app: nó là secret của Supabase, và chỉ function `gemini-hint` đọc được.

## Phần 4: Đọc Tên File Như Đọc Bản Đồ

Đây là kỹ năng điều hướng nhanh nhất trong codebase production. Mỗi hậu tố tên file cho bạn biết vai trò và module của nó trước khi bạn mở file.

### Nhóm tiền tố

| Tiền tố       | Nghĩa                                             | Module                         |
| ------------- | ------------------------------------------------- | ------------------------------ |
| `Qzds*`       | Design system component hoặc token dùng chung     | `:ui-components` hoặc `:theme` |
| `I*ViewModel` | Interface của ViewModel, dùng cho test và preview | `:feature:*`                   |
| `*Impl`       | Implementation cụ thể của một contract            | thường là `:data`              |

Tiền tố `Qzds` xuất hiện ở rất nhiều file. Khi bạn thấy nó, bạn biết ngay: đây là thứ dùng chung, không thuộc riêng feature nào, và sửa nó sẽ ảnh hưởng nhiều screen.

### Nhóm hậu tố trong một feature

| Hậu tố               | Vai trò                                                    |
| -------------------- | ---------------------------------------------------------- |
| `*RouteData`         | Route contract, `@Serializable`, nằm ở `:core`             |
| `*Navigation`        | Feature navigation provider, register destination          |
| `*Route`             | Composable nối ViewModel với screen, xử lý one-time effect |
| `*Screen`            | Composable render thuần từ `UiState`                       |
| `*UiState`           | Immutable data class mô tả những gì screen hiển thị        |
| `*Mapper`            | Chuyển state nội bộ thành `UiState`                        |
| `*Contract`          | Khai báo State, Action, SideEffect của một màn hình        |
| `*Reducer`           | Hàm thuần biến state cộng action thành state mới           |
| `*SideEffectHandler`, `*UiEffectHandler` | Nơi effect được thực thi thật, có I/O  |
| `*ViewModel`         | Coordinator nối tất cả những thứ trên lại                  |

Mở `feature/menu/.../viewmodel/` bạn sẽ thấy đúng bộ này: `MenuContract.kt`, `MenuReducer.kt`, `MenuViewModel.kt`, cộng thư mục `handler/` chứa 4 side effect handler.

Rồi mở `feature/menu/.../settings/viewmodel/`. Lại đúng bộ đó: `SettingContract.kt`, `SettingReducer.kt`, `SettingUiEffectHandler.kt`, `SettingViewModel.kt`.

Rồi `leaderboard/viewmodel/`. Vẫn bộ đó.

Codebase không có mười cách tổ chức cho mười màn hình. Nó có đúng một shape, lặp lại ở mọi nơi.

Khi bạn hiểu shape đó một lần, bạn đọc được mọi feature còn lại. Và khi bạn viết feature mới, bạn đã biết cần tạo file nào.

## Phần 5: Layer Và Nền Tảng

Mọi layer trong bảng Phần 1 đều nằm trong `commonMain`: UI composable, Route, ViewModel, reducer, use case, contract, và phần lớn data implementation. Android và iOS dùng chung một bản.

Chỉ những implementation phải gọi SDK riêng của một nền tảng mới tách ra. Ví dụ contract `AudioManager` nằm trong `commonMain`, còn implementation có hai bản:

| Contract (commonMain) | Android (`DataModule.android.kt`) | iOS (`DataModule.ios.kt`) |
| --------------------- | --------------------------------- | ------------------------- |
| `AudioManager`        | `SoundPoolAudioManager`           | `AvAudioManager`          |
| `HapticManager`       | `VibratorHapticManager`           | `UiKitHapticManager`      |
| `AnalyticsManager`    | `FirebaseAnalyticsManager`        | `NoOpAnalyticsManager`    |

Feature chỉ biết `AudioManager`. Koin chọn implementation theo nền tảng. Trong UI dùng chung không có nhánh `if (platform)` nào: một composable, hai Koin binding.

Khi thêm code chạm tới SDK nền tảng, đặt contract ở `commonMain` và hai implementation ở `androidMain`/`iosMain`. Mẹo này áp dụng cho mọi project KMP, không riêng repo này.

## Phần 6: ViewModel Không Làm Tất Cả

Một hiểu lầm phổ biến: vì reducer và handler được ViewModel điều phối, nên mọi thứ phải đưa vào ViewModel.

`ViewModel` chỉ điều phối: nhận UI events, expose `UiState`, dispatch actions, gọi reducer, và execute effects qua collaborator đã inject.

Business rule phức tạp vẫn nên nằm trong use case. Data access vẫn nên đi qua repository/service contract. Provider detail vẫn nằm trong `:data`.

## Bad Smells

| Smell                                            | Hướng tốt hơn                                     |
| ------------------------------------------------ | ------------------------------------------------- |
| Composable import Supabase hoặc Firebase classes | Đưa service call về sau domain/data layers.       |
| ViewModel tự tạo `HttpClient`                    | Inject repository/service implementation.         |
| Reducer hoặc state machine gọi I/O trực tiếp     | Emit side effect, execute ở handler.              |
| Repository trả về `UiState`                      | Trả về domain model, map sang UI ở feature layer. |
| Service implementation nằm trong `:domain`       | Giữ domain là contract, implementation ở `:data`. |
| Feature copy một button có thể dùng lại nơi khác | Extract sang `:ui-components` với tiền tố `Qzds`. |
| Feature A import screen của Feature B            | Dùng route contracts và callbacks.                |
| Composable chia nhánh theo nền tảng              | Đặt contract ở `commonMain`, bind implementation theo nền tảng qua Koin. |

## Checkpoint

Mở `feature/menu/.../viewmodel/` và `feature/menu/.../settings/viewmodel/` cạnh nhau.

Bạn hoàn tất bài này khi trả lời được:

- Hai thư mục đó có chung shape gì?
- File nào là nơi duy nhất được phép gọi I/O trong shape đó?
- Chỉ nhìn tên `LeaderboardUiState.kt`, bạn đoán được nó chứa gì và không chứa gì?
- `AIHintService` và `AIHintServiceImpl` khác nhau ở đâu, và mỗi cái nằm module nào?
- Nếu bạn cần một button dùng chung cho cả menu và game, bạn đặt nó ở đâu và đặt tên thế nào?
- Vì sao `AudioManager` có hai implementation, và feature có cần biết điều đó không?

---

- Bài trước: [Feature Registration Và Navigation Boundaries](https://dantech.academy/courses/kotlin-accelerator/engineer-builder/01-production-app-map/lesson-03-understand-feature-discovery.md)
- Bài tiếp theo: [Thay Đổi An Toàn Đầu Tiên](https://dantech.academy/courses/kotlin-accelerator/engineer-builder/01-production-app-map/lesson-05-make-your-first-change.md)
