¿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.