CreationExtras.inventoryApplication() 這有點跳

「看官方範例或 Codelab 時,很多人看到 CreationExtras.inventoryApplication() 這行都會一臉黑人問號:這三小?為什麼在一個框架層的 CreationExtras 上,會突然冒出一個自己 App 的自訂擴充函式?這思維跳躍也太大了吧?別慌,這背後其實是 Android 官方為了拯救五年前寫 Factory 寫到想死的歷史遺留代價。」

在 Android 架構演進史裡,ViewModel 的實例化 一直是一塊令人頭痛的牛皮癬。

因為 ViewModel 的生命週期是由系統框架(ViewModelStoreOwner)統一託管的,當螢幕旋轉或配置變更時,系統必須保證 ViewModel 不被銷毀重造。
這意味著:你絕對不能自己 new MyViewModel()!

一旦 ViewModel 需要建構子參數(例如 Repository 或 SavedStateHandle),你就必須透過 ViewModelProvider.Factory 來轉交依賴。而這段歷史,寫滿了 Android 工程師的血淚樣板。


歷史包袱:五年前我們是怎麼寫 Factory 的?

在 AndroidX Lifecycle 2.5.0 以前,官方的 ViewModelProvider.Factory 介面只有一個陽春方法:

// 舊時代的陽春 Factory 介面
public interface Factory {
    public <T extends ViewModel> T create(Class<T> modelClass);
}

注意看,這個 create(Class<T>) 只有一個 modelClass 參數!
如果你想傳遞 Application 或 UserRepository 給 ViewModel,唯一的辦法就是把依賴塞進 Factory 的建構子裡:

// 舊時代的樣板地獄:每個 ViewModel 都要配一套專屬 Factory
class UserViewModelFactory(
    private val repository: UserRepository,
    private val application: Application
) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(UserViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return UserViewModel(repository, application) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

舊做法的三大痛點:

  1. 樣板程式碼爆炸:專案裡有 50 個 ViewModel,你就得寫 50 個 Factory 類別,或者寫一個巨大的 if-else 分發器。
  2. 記憶體洩漏風險:為了傳參數,開發者常常把 Activity 或 Context 當作參數塞給 Factory,一旦持有不慎,直接引發記憶體洩漏。
  3. 單元測試困難(Framework 耦合):繼承 AndroidViewModel(application) 會讓 ViewModel 硬生生綁死 Android SDK 的 Context,不掛 Robolectric 根本無法在純 JVM 環境跑單元測試。

救贖降臨:Lifecycle 2.5.0 的 CreationExtras

為了一勞永逸消滅上述痛點,Google 在 androidx.lifecycle:lifecycle-viewmodel:2.5.0 正式重構了 Factory 介面,引入了 CreationExtras:

// 現代 Factory 介面:多帶了一個 extras 參數!
fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T

CreationExtras 本質是什麼?

CreationExtras 的底層本質上是一個**「型別安全的異質字典(Type-safe Heterogeneous Map)」**!

它由一系列的 CreationExtras.Key<T> 與具體實例映射組成。當你在 Activity、Fragment 或 Compose 呼叫 viewModel() 時,Android 系統會在幕後自動把當前元件的環境資訊打包進 CreationExtras:

┌─────────────────────────────────────────────────────────────┐
│ 系統預設自動注入的 CreationExtras 字典                      │
├─────────────────────────────────────┬───────────────────────┤
│ Key 名稱                            │ 存放之內容實例        │
├─────────────────────────────────────┼───────────────────────┤
│ AndroidViewModelFactory.APPLICATION_KEY │ Application 實例  │
│ SAVED_STATE_REGISTRY_OWNER_KEY      │ SavedStateRegistryOwner│
│ VIEW_MODEL_STORE_OWNER_KEY          │ ViewModelStoreOwner   │
│ DEFAULT_ARGS_KEY                    │ Bundle 啟動參數       │
└─────────────────────────────────────┴───────────────────────┘

因為字典裡已經自動躺著 APPLICATION_KEY 與 SAVED_STATE_REGISTRY_OWNER_KEY,你在建構 ViewModel 時,根本不需要手動從外部傳入 Application,直接從 extras 就能型別安全地取出來!


謎底揭曉:為什麼 CreationExtras.inventoryApplication() 這有點跳?

現在我們回到標題這句讓許多人困惑的程式碼:

/**
 * 查詢 Application 物件並轉型為自訂的 InventoryApplication
 */
fun CreationExtras.inventoryApplication(): InventoryApplication =
    (this[AndroidViewModelFactory.APPLICATION_KEY] as InventoryApplication)

很多初學者看到這段會覺得:「奇怪,CreationExtras 是官方庫的類別,InventoryApplication 是我專案自訂的類別,怎麼會寫成 CreationExtras 的擴充函式?這邏輯跳躍也太大了!」

搭配 viewModelFactory DSL 來看,真相大白:

Google 同時提供了 viewModelFactory DSL,讓我們不必再宣告一堆冗長的 Factory 類別:

object AppViewModelProvider {
    val Factory = viewModelFactory {
        // 定義 GameViewModel 的建構工廠
        initializer {
            // 注意!在 initializer 的 Lambda 區塊內,this 的接收者型態正是 CreationExtras!
            val app = inventoryApplication() // 這裡就是呼叫擴充函式!
            GameViewModel(app.container.itemsRepository)
        }

        // 定義 SettingsViewModel 的建構工廠
        initializer {
            val app = inventoryApplication()
            SettingsViewModel(app.container.settingsRepository)
        }
    }
}

看清楚了這個呼叫鏈,所有迷霧瞬間散開:

  1. 在 initializer { ... } 的閉包中,Receiver(接收者)型態正是 CreationExtras。
  2. 原本我們要寫出囉嗦又醜陋的長字串取值與強制轉型:
    val app = this[AndroidViewModelFactory.APPLICATION_KEY] as InventoryApplication
  3. 透過在 CreationExtras 上掛載一個名為 inventoryApplication() 的擴充函式,我們把這段重複、脆弱的型別轉換封裝成了一句乾淨俐落的呼叫。
  4. 拿到 InventoryApplication 後,再透過其內建的依賴容器(container.itemsRepository,即手動服務定位器模式)將 Repository 灌進 ViewModel。

這不是什麼黑魔法,只是一次標準的 Kotlin 擴充函式 + 接收者閉包(Receiver Closure) 的語法糖整合。


架構權衡:CreationExtras vs. Hilt

在現代 Android 商業專案中,我們該在何時使用 CreationExtras?

┌─────────────────┬──────────────────────────────────────────┬──────────────────────────────────────────┐
│ 評估維度        │ CreationExtras + viewModelFactory        │ Hilt / Dagger                            │
├─────────────────┼──────────────────────────────────────────┼──────────────────────────────────────────┤
│ 依賴注入機制    │ 手動依賴注入 / 服務定位器 (Manual DI)     │ 自動化編譯期程式碼生成依賴注入 (Annotation)│
│ 適用場景        │ 小型專案、KMP 跨平台、官方教學 Codelab    │ 中大型商用專案、團隊協作、模組化架構     │
│ 樣板程式碼      │ 需手動撰寫 Provider 與擴充函式           │ 極低,只需 @HiltViewModel 與 @Inject     │
│ 編譯速度影響    │ 零額外負擔 (純原生 Kotlin 語法)          │ KSP/KAPT 註解處理器需耗費額外編譯時間    │
│ 學習曲線        │ 平緩,理解 Map 鍵值與擴充函式即可        │ 陡峭,需理解 Component、Scope 與 Binds   │
└─────────────────┴──────────────────────────────────────────┴──────────────────────────────────────────┘

工業級實戰建議:

  • 大型商用 App:請老老實實上 Hilt。在 ViewModel 標註 @HiltViewModel,直接由 Dagger 在編譯期生成 Factory,完全省去維護 CreationExtras 的心智負擔。
  • Kotlin Multiplatform (KMP) 或輕量庫:Hilt 深度綁死 Android 平台,在 KMP 或單純不想引入肥大依賴注入庫的小型模組中,CreationExtras + 擴充函式是架構解耦的最優解。

工位大實話與收工

  1. 不要再手寫繼承 ViewModelProvider.Factory 的獨立類別了:利用 viewModelFactory { initializer { ... } } DSL 可以在一個檔案內優雅集中管理所有手動工廠。
  2. 向 AndroidViewModel 說再見:透過 CreationExtras 在建構時取出 Application 或 Repository,ViewModel 本身只需繼承純粹的 ViewModel(),徹底解耦 Android SDK,讓單元測試回歸純 JVM 毫秒級秒殺。

感謝觀看,收工。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *