iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
Software Development

從 Fragment 到 Compose — 老 Android App 重寫的架構取捨系列 第 4

Day 04|DI 取捨:從 ServiceLocator 與 17 分支的工廠到 Hilt

  • 分享至 

  • xImage
  •  

程式碼片段

舊世界的 ServiceLocator:想做的兩件事都沒做到

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 裡加了下列,就可能出事:

  1. 記憶體快取
  2. 共用狀態,例如 StateFlow / LiveData
  3. Mutex 或 lock
  4. Room 資料庫
  5. 自建的 OkHttpClient 客戶端

今天的取捨問題

舊 App 的物件組裝是這樣的:

  • 一個 ServiceLocator 單例,provideRepository(context) 回傳唯一的 AppRepository
  • 一個 ViewModelFactory,建構子只收那一個 repository,create() 裡是 17 個 isAssignableFrom 分支。
  • 定位功能另外有第二個工廠,因為它需要的依賴不一樣,塞不進第一個。
  • 六個 ViewModel 不在任何工廠裡,各自用別的方式建。
  • Activity.getVmFactory() 每次呼叫都 new 一個工廠;約 15 個 Java 檔繞過它自己 new ViewModelFactory(...)
  • 測試要換掉 repository,只能透過單例上的 @VisibleForTesting set

症狀不是「不優雅」,而是四個具體的成本:

  1. 加一個 ViewModel 要改工廠(而且工廠住在另一個 package)。
  2. ViewModel 想多要一個依賴(dispatcher、logger、第二個 repository)做不到,除非改工廠簽章,然後 17 個分支一起改。
  3. 依賴圖沒有任何驗證:缺了什麼、循環了什麼,執行到那一頁才知道。
  4. 多模組之後這個模式直接崩潰:core 模組不能反向依賴 app 裡的 ServiceLocator。

選項與代價

選項 做法 優點 代價
A. Hilt 註解 + KSP 產生 Dagger 圖;@HiltViewModelhiltViewModel() 編譯期驗證整張圖;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)要自己管

上一篇
Day 03|建置骨架:多模組、version catalog、convention plugin,以及 secrets 為什麼不能再進 git
下一篇
網路邊界:後端信封與 `d: Any?` 的約束,`AppResult` 的誕生 | core:network |
系列文
從 Fragment 到 Compose — 老 Android App 重寫的架構取捨5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言