ServiceLocator 是結合本地跟遠端兩個 data source, 形成 AppRepository 介面,再讓 ViewModel 工廠來取用。
ServiceLocator 想做兩件事:集中組裝並共用同一個 repository、給測試留一個替換口。實際長這樣(已改名、已縮短,但形狀是真的):
package com.example.fieldops.legacy
object ServiceLocator {
@Volatile
var repository: AppRepository? = null
@VisibleForTesting set // 替換口:整個 repo 沒有任何測試寫過它
fun provideRepository(context: Context): AppRepository {
synchronized(this) {
return repository ?: createRepository(context)
}
}
// 建好之後沒有存回 repository,所以每次呼叫都是新的一個
private fun createRepository(context: Context): AppRepository =
DefaultAppRepository(RemoteDataSource, LocalDataSource(context))
}
class LegacyApp : Application() {
lateinit var repository: AppRepository
private set
override fun onCreate() {
super.onCreate()
// 全 App 唯一的呼叫點:「只有一個 repository」靠的是這行只跑一次
repository = ServiceLocator.provideRepository(this)
}
}
repository 沒有被賦值,每次呼叫 fun createRepository() 都是新的東西。
在上面舊的寫法之下,repository 或 data source 裡加了下列,就可能出事:
舊 App 的物件組裝是這樣的:
ServiceLocator 單例,provideRepository(context) 回傳唯一的 AppRepository。ViewModelFactory,建構子只收那一個 repository,create() 裡是 17 個 isAssignableFrom 分支。Activity.getVmFactory() 每次呼叫都 new 一個工廠;約 15 個 Java 檔繞過它自己 new ViewModelFactory(...)。@VisibleForTesting set。症狀不是「不優雅」,而是四個具體的成本:
| 選項 | 做法 | 優點 | 代價 |
|---|---|---|---|
| A. Hilt | 註解 + KSP 產生 Dagger 圖;@HiltViewModel;hiltViewModel() |
編譯期驗證整張圖;Android 元件生命週期內建;多模組每個模組自帶 @Module;官方 Compose / Navigation / WorkManager 整合 |
KSP 增加建置時間;產生碼的錯誤訊息要學著讀;@InstallIn 與 component 階層有學習曲線 |
| B. Koin | Kotlin DSL 宣告 module,執行期解析 | 零 codegen、建置快、寫法直覺;也能多模組 | 缺依賴到執行期才炸(Koin Annotations 可補一部分);Compose 整合是第三方維護;大型圖的啟動成本 |
| C. 手工 DI | 自己寫 AppContainer,建構子注入 |
沒有魔法、教學性最高;新手完全看得懂 | 就是舊 App 的做法做對版:90 個畫面規模下,容器會長成新的 God object;生命週期範圍(Activity / ViewModel)要自己管 |