---
# System prepended metadata

title: '[Software Design] [Android] Kotlin data class Redeclaration 問題與最佳解法'
tags: [Software Design, Android]

---

在使用 Kotlin 撰寫多個 API 對應資料模型時，若同一專案中出現多個相同命名的 `data class`（如 `Team`、`Cup`、`Pivot`），就會發生 **「Redeclaration」** 錯誤。

這篇文章將說明這個問題的原因、幾種常見解法，以及推薦的最佳實作方式。

---

## 問題描述：命名衝突

假設你有兩個 API：

- `/teams/list`
- `/teams/create`

### Create
```kotlin
package com.rainbowt0506.example.data.model.response.team

/**
{
    "data": {
        "name": "草莓一族",
        "level": 1,
        "order": 0,
        "is_top": 0,
        "logo": "https://main-admin.rainbowt0506.app/storage/teams/PCefhKvkLZHIiHwXYV8txQFYgL5yT3ghWWga6tgB.jpg",
        "id": 21761,
        "cups": [
            {
                "id": 8441,
                "name": "2025",
                "pivot": {
                    "team_id": 21761,
                    "cup_id": 8441
                }
            }
        ]
    },
    "status": 200,
    "message": "Success"
}
*/
data class CreateTeamResponse(
    val `data`: Team,
    val message: String,
    val status: Int
)

data class Team(
    val cups: List<Cup>,
    val id: Int,
    val is_top: Int,
    val level: Int,
    val logo: String,
    val name: String,
    val order: Int
)

data class Cup(
    val id: Int,
    val name: String,
    val pivot: Pivot
)

data class Pivot(
    val cup_id: Int,
    val team_id: Int
)
```
### List
```kotlin
package com.rainbowt0506.example.data.model.response.team

import java.io.File

/**
{
    "data": {
        "teams": [
            {
                "id": 19710,
                "name": "starbuck",
                "logo": "https://main-admin.rainbowt0506.app/storage/teams/hoPHfO8fY6ELH8VkyfM6By95fC4hqFsCTExjaNIP.png",
                "level": 2,
                "order": 0,
                "is_top": 1745033227,
                "deleted_at": null,
                "cups": [
                    {
                        "id": 8441,
                        "name": "2025",
                        "level": 0,
                        "order": 0,
                        "is_top": 0,
                        "deleted_at": null,
                        "pivot": {
                            "team_id": 19710,
                            "cup_id": 8441
                        }
                    }
                ]
            },
            {
                "id": 18652,
                "name": "統一",
                "logo": "https://main-admin.rainbowt0506.app/storage/teams/E1oGXtNJR8EY7vGIHNgUua0PPoqaEYeic0yWS239.jpg",
                "level": 4,
                "order": 0,
                "is_top": 0,
                "deleted_at": null,
                "cups": []
            },
            {
                "id": 18651,
                "name": "富邦",
                "logo": "https://main-admin.rainbowt0506.app/storage/teams/hriNB6jVEUb4FoFA8KfpI0pzIdcQ6pKxgnE0FoLu.jpg",
                "level": 4,
                "order": 0,
                "is_top": 1745033236,
                "deleted_at": null,
                "cups": []
            }
        ]
    },
    "status": 200,
    "message": "Success"
}
*/
data class TeamResponse(
    val teamList: TeamList,
    val message: String,
    val status: Int
)

data class TeamList(
    val teams: List<Team>
)

data class Team(
    val cups: List<Cup>,
    val deleted_at: Int,
    val id: Int,
    val is_top: Int,
    val level: Int,
    val logo: String,
    val name: String,
    val order: Int
)

data class Cup(
    val deleted_at: Int,
    val id: Int,
    val is_top: Int,
    val level: Int,
    val name: String,
    val order: Int,
    val pivot: Pivot
)

data class Pivot(
    val cup_id: Int,
    val team_id: Int
)

data class TeamUpdate(
    val id: Int,
    val name: String? = null,
    val level: Int? = null,
    val order: Int? = null,
    val isTop: Int? = null,
    val cupIds: List<Int>? = null,
    val logoFile: File? = null
)
```

---
這兩個 API 的 response 都會回傳 `Team`、`Cup`、`Pivot` 等結構，但欄位內容或格式略有不同，導致我們需要建立各自對應的 `data class`。

然而，若這些 class 被定義在相同 package 下，如：

```kotlin
package com.rainbowt0506.example.data.model.response.team

data class Team(...)
data class Cup(...)
data class Pivot(...)
```

當你嘗試為另一個 API 定義相同名稱的 class 時，就會遇到：

```
Redeclaration: Class 'Team' is already defined in the scope
```

---


## 解法一覽

| 方法 | 說明 | 優點 | 缺點 | 推薦程度 |
|------|------|------|------|----------|
| **1. 拆分 package** | 為不同 API 建立不同 package | 清楚、有組織、易維護 | 初期需要調整結構 | ⭐⭐⭐⭐⭐ |
| 2. 改類名（加前綴/後綴） | 如 `ListTeam`, `CreateTeam` | 實作快，不需調整目錄 | 類名變長，可讀性下降 | ⭐⭐⭐ |
| 3. 合併成一份 class | 所有欄位變 optional | 省事一時 | 高耦合、易錯、不易維護 | ⭐ |

---

## 其他常見但不推薦的做法
### 2. 改類名（加前綴/後綴）

這是在專案初期常見的快速解法，只需要修改 class 名稱本身，不需要變動 package 結構。

#### 範例

```kotlin
data class ListTeam(...)
data class CreateTeam(...)
data class ListCup(...)
data class CreateCup(...)
```

#### 缺點

- **命名失去一致性**：本來 `Team` 是個泛用名詞，現在卻要根據 API 加上前綴，降低語意的純粹性。
- **可讀性下降**：開發者必須時時關注「這是哪個 API 的 Team？」，增加心智負擔。
- **容易失控**：當 API 增多，出現 `EditTeam`、`DetailTeam`、`TransferTeam` 等命名時，整體結構開始混亂。
- **IDE 搜尋不便**：搜尋 `Team` 找不到全部定義，無法直覺了解整體模型。

這也是許多團隊會在初期採用，但後來轉向其他方案的原因。

---

### 3. 合併成一份 class（不推薦）

有些開發者會將不同 API 對應的資料合併成一份 class，透過 `nullable` 屬性或預設值來支援多種格式。

#### 範例

```kotlin
data class Team(
    val id: Int,
    val name: String?,
    val level: Int?,
    val order: Int?,
    val isTop: Int?,
    val logo: String?,
    val cups: List<Cup>?,       // 有些 API 沒有
    val deletedAt: Int?         // 有些 API 沒有
)
```

#### 缺點

- **耦合度過高**：一個 `Team` 類型要負責所有 API 可能出現的欄位，職責過多。
- **不易維護**：API 結構一改，就得回頭改這個共用類別，容易引發 side effect。
- **資料不明確**：哪些欄位是一定有、哪些是選擇性？光看 class 無法分辨。
- **測試困難**：無法針對不同 API 寫獨立的單元測試模型。

這種方式雖然短期內看起來省事，但長期來看維護成本高，容易出 bug。
## ✅ 推薦做法：不同 API 放不同 package

這是最乾淨也最推薦的做法，對於後續擴充與維護極為友善。

### 資料夾結構建議

```
com.rainbowt0506.example.data.model.response.team
├── list/
│   └── TeamResponse.kt
│   └── Team.kt
│   └── Cup.kt
│   └── Pivot.kt
├── create/
│   └── CreateTeamResponse.kt
│   └── Team.kt
│   └── Cup.kt
│   └── Pivot.kt
```

### **為什麼每個 API 一個 package 是推薦做法？**

#### 1. **防止類別衝突**
就像你遇到的 `Team`、`Cup`、`Pivot` 重名問題，在獨立 package 裡根本不會出現。

#### 2. **清楚的上下文**
若有需要同時使用兩個 package 下的 `Team`：
看 package 名稱就知道這個資料類型是給哪個 API 用的：
```kotlin
com.rainbowt0506.example.data.model.response.team.list.Team
com.rainbowt0506.example.data.model.response.team.create.Team
```
→ 不需要靠類名改名（如 ListTeam、CreateTeam）就能知道是哪裡來的。
如此即可避免任何命名衝突，同時保有清晰語意。
#### 3. **維護方便**
未來如果 API 結構改了，只會影響自己那個 package 的 class，不會殃及其他。

#### 4. **支援多版本 API**
假設你之後支援 API v1、v2，package 分開後超級好管理：
```
.team.v1.list
.team.v2.list
```

 

---

## 疑問
### 1. 「資料模型轉換（Model Mapping）」：三種常見的模型橋接策略
#### ✅ **解法一：建立 UI 專用的 ViewModel / DTO（推薦）**

建立一個只給 UI 使用的資料結構，不跟 API 資料耦合，責任清楚。

```kotlin
data class TeamEditUiModel(
    val id: Int,
    val name: String,
    val logoUrl: String,
    val isSelected: Boolean = false,
    val cups: List<Int> = emptyList(),
    val level: Int? = null,
    val isTop: Boolean = false
)
```

轉換方式：

```kotlin
fun Team.toUiModel(): TeamEditUiModel = TeamEditUiModel(
    id = id,
    name = name,
    logoUrl = logo,
    level = level,
    isTop = is_top != 0,
    cups = cups.map { it.id }
)
```

- **優點**：模組清晰、可測試、維護容易。
- **缺點**：初期可能需要寫一些 mapper 轉換邏輯。

---

#### **解法二：共用同一個 `Team` 類別，加 nullable 或 UI 專用欄位（不建議）**

```kotlin
data class Team(
    val id: Int,
    val name: String,
    val logo: String,
    val cups: List<Cup>?,
    val isSelected: Boolean? // UI 要求加入
)
```

- **優點**：快速上手，減少資料類別數量。
- **缺點**：class 變得模糊、肥大、難測試，會破壞模型邊界。

---

#### ❗ **解法三：強轉型、過度使用泛型或 Map（踩雷，==我遇過！！！==）**

某些團隊會用 `Map<String, Any>` 或泛型模型來「偷渡」不同資料：

```kotlin
val teamData: Map<String, Any> = mapOf(
    "id" to 123,
    "name" to "starbuck",
    "logo" to "url...",
    "isSelected" to true
)
```

或者：

```kotlin
class GenericModel(val data: Any)
```

- **優點**：快速又無視型別。
- **缺點**：
  - 型別安全完全破壞，IDE 無法協助檢查錯誤。
  - 測試與維護極困難。
  - 會讓未來接手的工程師想翻桌。

---

## 結語

當你的專案開始整合越來越多 API，資料模型的結構設計就變得非常關鍵。使用「每個 API 一個 package」的方式，不僅能有效解決命名衝突，也能讓整體架構更模組化、更易讀、更好維護。