beio Logobeio

【Android 架構系列 03】依賴注入 (DI):讓 Hilt 成為 AI 的「接線生」

發布於

01. 前言:最後一哩路的耦合陷阱

這是系列文的第三篇。在前兩篇,我們透過 SRP 把程式碼拆成了小碎片,透過 DIP 定義了介面合約。現在我們滿地都是零件:NewsRepository 介面、NewsRepositoryImpl 實作、NewsViewModel...

但誰負責把它們組裝起來?

如果我們還是在 Activity 裡寫 val repo = NewsRepositoryImpl(client, apiKey),那我們前功盡棄。這篇將介紹如何利用 Hilt (Android 的依賴注入標準庫),讓它成為 AI 的「自動接線生」,解決架構落地的最後一哩路。

我們在上一篇利用 SOLID 原則,成功讓 AI 生成了獨立的 NewsRepository 介面與實作。但在把這些元件串接起來時,我們常會看到 AI 寫出這種「毀滅架構」的程式碼:

// ❌ BadMainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 😱 災難現場:手動組裝依賴
        // 只要這裡改了一個參數,所有依賴它的地方都要改
        val client = OkHttpClient.Builder().build()
        val api = Retrofit.Builder().client(client)...build().create(NewsApi::class.java)
        val repository = NewsRepositoryImpl(api, "API_KEY") // 依賴了具體實作!
        
        // 如果 ViewModel 需要參數,這裡就會變成建構子地獄
        val viewModel = NewsViewModel(repository) 
    }
}

這段代碼打破了 DIP (依賴反轉原則)MainActivity 本應只關心 UI,現在卻被迫知道 OkHttp 怎麼建立、Retrofit 怎麼設定。

如果這時候你叫 AI:「幫我把 OkHttp 換成單例模式,並且在 Debug 版加入 Log 攔截器。」

AI 會直接修改 MainActivity,導致 UI 邏輯與網路設定邏輯混在一起,再次回到 God Activity 的老路。

我們需要一個機制,把「物件的建立」與「物件的使用」完全分開。這就是 Hilt 的工作。

02. Hilt 的角色:AI 的「自動接線生」

想像一下,你的 App 是一個巨大的電話總機系統。

  • ViewModel 是打電話的人(使用者)。
  • Repository 是接電話的人(服務提供者)。
  • Hilt 就是那個接線生

ViewModel 不需要知道 Repository 住在哪裡、電話號碼是多少。它只需要對 Hilt 喊一聲:「給我一個 NewsRepository!」Hilt 就會把線接好。

這對 AI 協作有什麼好處?

當我們引入 Hilt,AI 在寫 ViewModel 時,就完全不需要思考 Repository 是怎麼來的。這大幅縮小了 AI 的上下文視窗 (Context Window)。

03. 實戰演練:指揮 AI 寫 Hilt

我們要如何透過 Prompt,讓 AI 幫我們完成自動裝配?

步驟 1:建構子注入 (Constructor Injection)

首先,我們要教 AI:「不要 new,要 @Inject」

Prompt:

"請修改 NewsRepositoryImpl 和 NewsViewModel。 使用 @Inject 標註建構子。請注意,ViewModel 應該使用 @HiltViewModel。"

AI 生成的代碼:

// data/repository/NewsRepositoryImpl.kt
// AI 知道這個類別可以被注入,且依賴 NewsApi
class NewsRepositoryImpl @Inject constructor(
    private val api: NewsApi
) : NewsRepository { ... }

// presentation/NewsViewModel.kt
// AI 知道它需要一個 NewsRepository,但它不關心這個 Repository 是怎麼來的
@HiltViewModel
class NewsViewModel @Inject constructor(
    private val repository: NewsRepository // 這裡依賴的是 Interface!符合 DIP
) : ViewModel() { ... }

步驟 2:模組綁定 (Hilt Modules)

這是最關鍵的一步。因為 NewsRepository 是介面,Hilt 不知道要用哪個實作(Impl)。我們需要叫 AI 寫一張「配對表」。

Prompt:

"請建立一個 Hilt Module,名為 RepositoryModule。 使用 @Binds 將 NewsRepositoryImpl 綁定到 NewsRepository 介面。 這個 Module 應該安裝在 SingletonComponent 中。"

AI 生成的代碼:

// di/RepositoryModule.kt
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {

    @Binds
    @Singleton
    abstract fun bindNewsRepository(
        impl: NewsRepositoryImpl
    ): NewsRepository
}

步驟 3:提供第三方依賴 (Retrofit/OkHttp)

NewsRepositoryImpl 需要 NewsApi,而 NewsApi 是 Retrofit 產生的,不能用 @Inject。我們需要叫 AI 用 @Provides

Prompt:

"請建立 NetworkModule。

  1. 提供一個單例的 OkHttpClient,加入 LoggingInterceptor。
  2. 提供一個單例的 Retrofit 實體。
  3. 提供 NewsApi 實體。"

AI 生成的代碼:

// di/NetworkModule.kt
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(HttpLoggingInterceptor().apply { level = Level.BODY })
            .build()
    }

    @Provides
    @Singleton
    fun provideNewsApi(client: OkHttpClient): NewsApi {
        return Retrofit.Builder()
            .baseUrl("[https://newsapi.org/](https://newsapi.org/)")
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(NewsApi::class.java)
    }
}

04. 成果:完全解耦的架構

現在,讓我們回頭看看 MainActivity。有了 Hilt,AI 可以把它簡化成什麼樣子?

// ✅ Refactored MainActivity.kt
@AndroidEntryPoint // 告訴 Hilt 這裡需要注入
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        setContent {
            // 魔法發生了!
            // 我們不需要手動建立 Repository,也不用管 Retrofit
            // 只要一行 hiltViewModel(),Hilt 就會自動幫我們組裝好一切
            val viewModel: NewsViewModel = hiltViewModel()
            
            val state by viewModel.uiState.collectAsState()
            NewsScreen(state)
        }
    }
}

這段程式碼的價值在於**「無知」MainActivity 對於網路請求、資料庫、解析邏輯一無所知**。它只認識 ViewModel。

這對 AI 協作意味著什麼?

這意味著,當你想把 OkHttp 換成 Ktor 時,你只需要叫 AI 修改 NetworkModule

MainActivityNewsViewModelNewsRepository 完全不需要改動,甚至連重新編譯都不需要(如果你用的是多模組架構)。

05. 結論:Hilt 是架構的「防腐劑」

依賴注入 (DI) 常常被新手視為「過度設計」,但在 AI 時代,它是必要的。

  1. 它強制解耦: 讓 AI 無法寫出強依賴的爛 Code。
  2. 它簡化 Prompt: 你不需要在 Prompt 裡解釋一堆物件該怎麼初始化,只要說「注入它」。
  3. 它支援測試: 這為我們後續的單元測試章節打下了基礎(測試時可以輕鬆換成 FakeModule)。

現在,我們的「骨架」(SOLID + DI)已經搭建完畢。接下來,我們要開始填充「血肉」了。我們將進入最核心的業務邏輯層,看看如何防止 AI 污染我們的 Domain Logic。

下集預告:【Part 4】核心防禦:Domain 與 Data Layer 設計——防止 AI 污染業務邏輯


Ken Huang

關於作者

Ken Huang

熱衷於嘗試各類新技術的軟體開發者,現階段主力為 Android / iOS 雙平台開發,同時持續深耕前端與後端技術,以成為全端工程師與軟體架構師為目標。

最廣為人知的代表作為 BePTT。開發哲學是「以做身體健康為前提」,致力於在工作與生活平衡的基礎上,打造出擁有絕佳使用體驗的軟體服務。

這裡是用於紀錄與分享開發經驗的空間,希望能透過我的實戰筆記,幫助開發者解決疑難雜症並提升效率。

Android APP DevelopmentiOS APP DevelopmentBePTT CreatorFull Stack Learner