「看官方範例或 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")
}
}
舊做法的三大痛點:
- 樣板程式碼爆炸:專案裡有 50 個 ViewModel,你就得寫 50 個 Factory 類別,或者寫一個巨大的
if-else分發器。 - 記憶體洩漏風險:為了傳參數,開發者常常把 Activity 或 Context 當作參數塞給 Factory,一旦持有不慎,直接引發記憶體洩漏。
- 單元測試困難(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)
}
}
}
看清楚了這個呼叫鏈,所有迷霧瞬間散開:
- 在
initializer { ... }的閉包中,Receiver(接收者)型態正是CreationExtras。 - 原本我們要寫出囉嗦又醜陋的長字串取值與強制轉型:
val app = this[AndroidViewModelFactory.APPLICATION_KEY] as InventoryApplication - 透過在
CreationExtras上掛載一個名為inventoryApplication()的擴充函式,我們把這段重複、脆弱的型別轉換封裝成了一句乾淨俐落的呼叫。 - 拿到
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+ 擴充函式是架構解耦的最優解。
工位大實話與收工
- 不要再手寫繼承
ViewModelProvider.Factory的獨立類別了:利用viewModelFactory { initializer { ... } }DSL 可以在一個檔案內優雅集中管理所有手動工廠。 - 向
AndroidViewModel說再見:透過CreationExtras在建構時取出 Application 或 Repository,ViewModel 本身只需繼承純粹的ViewModel(),徹底解耦 Android SDK,讓單元測試回歸純 JVM 毫秒級秒殺。
感謝觀看,收工。

發佈留言