¿Por qué inyección de dependencias?

Sin DI, cada clase crea sus propias dependencias:

// Sin DI — acoplamiento fuerte, imposible de testear
class ProductosViewModel : ViewModel() {
    private val dao = AppDatabase.getInstance(context).productoDao()  // ¿context de dónde?
    private val api = Retrofit.Builder().baseUrl("...").build().create(ProductoApi::class.java)
    private val repo = ProductoRepositoryImpl(dao, api)
    private val useCase = GetProductosDisponiblesUseCase(repo)
}

Con DI, las dependencias se reciben desde afuera — quien crea el objeto se encarga de proveer todo lo necesario. Hilt automatiza esa construcción:

// Con Hilt — limpio, testeable
@HiltViewModel
class ProductosViewModel @Inject constructor(
    private val getProductos: GetProductosDisponiblesUseCase
) : ViewModel() { ... }

Setup de Hilt

// build.gradle (project)
plugins {
    id("com.google.dagger.hilt.android") version "2.51" apply false
}

// build.gradle (app)
plugins {
    id("com.google.dagger.hilt.android")
    id("com.google.devtools.ksp")
}
dependencies {
    implementation("com.google.dagger:hilt-android:2.51")
    ksp("com.google.dagger:hilt-compiler:2.51")
    // Para ViewModel
    implementation("androidx.hilt:hilt-navigation-fragment:1.2.0")
}
// Anotá la Application class — obligatorio
@HiltAndroidApp
class MiApp : Application()

// Y registrala en el Manifest:
// android:name=".MiApp"

// Anotá Activities y Fragments que reciben inyecciones:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() { ... }

@AndroidEntryPoint
class ProductosFragment : Fragment() { ... }

@Inject — inyección automática

Si una clase tiene un constructor anotado con @Inject, Hilt sabe cómo crearla automáticamente:

// Hilt puede construir esto automáticamente
class GetProductosDisponiblesUseCase @Inject constructor(
    private val repository: ProductoRepository
) {
    operator fun invoke() = repository.getProductos()
        .map { it.filter { p -> p.disponible } }
}

// Y esto también (si ProductoRepositoryImpl también tiene @Inject)
class ProductoRepositoryImpl @Inject constructor(
    private val dao: ProductoDao,
    private val api: ProductoApi
) : ProductoRepository { ... }

Módulos y @Provides — para lo que no podés anotar

No podés anotar con @Inject clases de librerías externas (Retrofit, Room, OkHttp). Para esas usás un módulo:

@Module
@InstallIn(SingletonComponent::class)  // vive mientras viva la app
object NetworkModule {

    @Provides
    @Singleton
    fun provideOkHttp(): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(HttpLoggingInterceptor().apply {
                level = HttpLoggingInterceptor.Level.BODY
            })
            .connectTimeout(30, TimeUnit.SECONDS)
            .build()
    }

    @Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.ejemplo.com/")
            .client(okHttpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    @Provides
    @Singleton
    fun provideProductoApi(retrofit: Retrofit): ProductoApi {
        return retrofit.create(ProductoApi::class.java)
    }
}

@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {

    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(context, AppDatabase::class.java, "app.db").build()
    }

    @Provides
    fun provideProductoDao(db: AppDatabase): ProductoDao = db.productoDao()
}

@Binds — para interfaces

Cuando tenés una interfaz (ProductoRepository) y su implementación (ProductoRepositoryImpl), usás @Binds para decirle a Hilt cuál implementación usar:

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {

    // "Cuando alguien pida ProductoRepository, dales ProductoRepositoryImpl"
    @Binds
    @Singleton
    abstract fun bindProductoRepository(
        impl: ProductoRepositoryImpl
    ): ProductoRepository
}

Scopes — duración de las instancias

// @Singleton — una sola instancia en toda la app (ApplicationComponent)
@Singleton class MiServicio @Inject constructor(...)

// @ActivityRetainedScoped — una por Activity, sobrevive rotaciones
// Es el scope de los ViewModels con Hilt

// @ActivityScoped — una por Activity, se destruye con la Activity
@ActivityScoped class MiHelper @Inject constructor(...)

// @ViewModelScoped — una por ViewModel (con Hilt ViewModel)
@ViewModelScoped class MiUseCaseConEstado @Inject constructor(...)

// @FragmentScoped — una por Fragment
@FragmentScoped class MiAnalytics @Inject constructor(...)

Cuidado con los scopesUn objeto con scope más amplio (@Singleton) no puede depender de uno con scope más corto (@ActivityScoped). Si lo intentás, Hilt falla en tiempo de compilación.

Hilt + ViewModel — sin Factory

// Con Hilt, anotás el ViewModel con @HiltViewModel
@HiltViewModel
class ProductosViewModel @Inject constructor(
    private val getProductos: GetProductosDisponiblesUseCase,
    private val comprar: ComprarProductoUseCase,
    private val savedStateHandle: SavedStateHandle  // también inyectable
) : ViewModel() { ... }

// En el Fragment — igual que antes, sin cambios
@AndroidEntryPoint
class ProductosFragment : Fragment() {
    private val viewModel: ProductosViewModel by viewModels()
    // Hilt ya sabe cómo construir ProductosViewModel con todas sus deps
}

SavedStateHandleHilt inyecta automáticamente el SavedStateHandle en los ViewModels. Usalo para leer argumentos de Navigation (Safe Args los pone ahí) o para persistir estado simple entre process death.