Guía de estudio 09 · Práctica 9

Inyección de dependencias y Hilt en Android

Inyección de dependencias, inversión de dependencias, Hilt, Dagger y KSP

20 min de lectura Tema de la Práctica 9 Independiente del ejemplo de la práctica

Propósito de estudio

Una aplicación Android tiene objetos que necesitan otros objetos para funcionar: un ViewModel necesita acceder a datos, un repositorio puede necesitar un servicio de red y ese servicio depende de un cliente HTTP. Al crecer el proyecto, la forma de construir y conectar esos objetos se convierte en una decisión de arquitectura.

Esta guía explica cómo reducir el acoplamiento entre clases, cómo implementar la inyección de dependencias (DI) de forma manual y cómo delegar la creación del grafo de dependencias a Hilt, una solución basada en Dagger. Los ejemplos utilizan Kotlin y Jetpack Compose y pueden comprenderse sin haber desarrollado una aplicación específica de tareas.

Al finalizar el estudio deberías poder leer un constructor y determinar sus dependencias, diseñar un contrato con varias implementaciones, configurar los elementos fundamentales de Hilt y diagnosticar errores de construcción del grafo.

Lo imprescindible para el examen

Reconocer acoplamiento y DI; implementar contratos con distintas implementaciones; interpretar Hilt, @Inject, @Binds y @Provides; distinguir componente de alcance de instancia.

Se evalúa la capacidad de explicar, interpretar fragmentos y justificar decisiones. Las versiones puntuales de librerías, URLs, nombres de proyecto y comandos accesorios quedan como contexto de consulta.

Criterios transversales para interpretar esta guía

Principio transferible. Conviene distinguir la responsabilidad de una tecnología de la forma concreta en que se utilizó en la práctica. Un ejemplo de ViewModel, Retrofit, Hilt o WebSocket ilustra una solución posible; su configuración exacta no es una regla universal.

Decisión del proyecto. Los nombres, rutas, versiones, intervalos, tamaños y elecciones de librerías del caso original sirven para interpretar el código. Salvo indicación expresa, no se exige memorizarlos para el examen.

Alcance del código. Los fragmentos son recursos didácticos; pueden omitir imports, dependencias, permisos, detalles de manejo de errores o infraestructura. Comprender un fragmento no equivale a comprobar que compila como proyecto independiente.

Referencias cruzadas. La síntesis transversal de la colección reúne las distinciones entre estado y evento, persistencia, reintentos, idempotencia y límites de las pruebas.

1. El problema: una clase construye lo que necesita

Considera una pantalla de pedidos. El ViewModel obtiene pedidos desde un repositorio y este solicita información a un servicio.

class PedidosViewModel : ViewModel() {
    private val repositorio = PedidosRepository()
}

class PedidosRepository {
    private val servicio = ServicioPedidos()

    suspend fun obtenerPedidos(): List<Pedido> = servicio.consultar()
}

Las clases parecen sencillas, pero cada una decide cómo construir la siguiente:

PedidosViewModel --crea--> PedidosRepository --crea--> ServicioPedidos

Esto genera varias dificultades:

  • Si el servicio necesita nuevas dependencias, puede ser necesario modificar clases que solo deberían utilizarlo.
  • Para ejecutar una demostración sin servidor, es difícil sustituir el origen de los pedidos.
  • Una prueba del ViewModel no puede aislarlo fácilmente de la red.
  • La configuración queda dispersa entre consumidores, en lugar de concentrarse en un lugar.

El defecto principal es el acoplamiento de construcción: quien usa una dependencia también asume la responsabilidad de fabricarla.

Dependencia, acoplamiento y cohesión

Una dependencia es un objeto o servicio que otra clase necesita. El acoplamiento expresa cuánto conocimiento tiene una clase sobre otras, y la cohesión describe hasta qué punto sus responsabilidades están relacionadas.

Recibir una dependencia suele disminuir el acoplamiento a su construcción. No significa eliminar toda dependencia: una clase necesita conocer, como mínimo, las operaciones que invoca.

2. Inyección de dependencias

La inyección de dependencias consiste en proporcionar a una clase los objetos que necesita desde el exterior, en lugar de obligarla a construirlos.

class PedidosRepository(private val servicio: ServicioPedidos) {
    suspend fun obtenerPedidos(): List<Pedido> = servicio.consultar()
}

class PedidosViewModel(
    private val repositorio: PedidosRepository
) : ViewModel()

Ahora las flechas expresan necesidad, no creación:

PedidosViewModel --necesita--> PedidosRepository --necesita--> ServicioPedidos

Una tercera parte será responsable de crear esos objetos en el orden apropiado y conectarlos. Este enfoque facilita cambiar implementaciones y permite que una clase tenga una responsabilidad más limitada.

Tres formas de inyección

Forma Idea Uso habitual
Constructor Recibir la dependencia al crear el objeto Opción preferida para dependencias obligatorias
Propiedad o campo Entregarla después de construir el objeto Algunas clases creadas por Android
Método Entregarla al ejecutar una operación Dependencias ocasionales o contextuales

En Kotlin, la inyección por constructor hace explícitas las dependencias y evita instancias parcialmente configuradas. Hilt también ofrece inyección de campos en determinadas clases Android.

Importante: DI puede implementarse completamente a mano. Hilt es una herramienta que automatiza una parte del trabajo, no la definición de DI.

3. Interfaces y sustitución de implementaciones

Supongamos que queremos obtener pedidos reales en producción y pedidos simulados durante una demostración.

Primero definimos el contrato que necesita el consumidor:

interface PedidosRepository {
    suspend fun obtenerPedidos(): List<Pedido>
}

Después proporcionamos implementaciones alternativas:

class PedidosRepositoryRemoto(
    private val servicio: ServicioPedidos
) : PedidosRepository {
    override suspend fun obtenerPedidos(): List<Pedido> = servicio.consultar()
}

class PedidosRepositorySimulado : PedidosRepository {
    override suspend fun obtenerPedidos(): List<Pedido> = listOf(
        Pedido(id = 1, descripcion = "Pedido de demostración")
    )
}

El ViewModel recibe el tipo del contrato:

class PedidosViewModel(
    private val repositorio: PedidosRepository
) : ViewModel() {

    fun cargar() {
        viewModelScope.launch {
            val pedidos = repositorio.obtenerPedidos()
            // Actualizar el estado observable de la pantalla.
        }
    }
}

De esta manera, la lógica del ViewModel no necesita conocer si el repositorio accede a una API, utiliza Room o devuelve datos fijos.

¿Es obligatorio utilizar interfaces?

No. DI también funciona al inyectar clases concretas. Una interfaz resulta útil cuando existe una abstracción relevante, se anticipan implementaciones alternativas o se quiere sustituir una dependencia al probar un componente. Crear una interfaz idéntica para cada clase, sin una necesidad clara, puede aumentar la complejidad.

Tampoco basta con declarar interfaces para lograr una arquitectura desacoplada. El contrato debe expresar lo que necesita el consumidor sin exponer detalles innecesarios del mecanismo de datos.

Relación con el principio de inversión de dependencias

En una arquitectura por capas, una regla de negocio puede depender de un contrato definido en una capa estable, mientras que una implementación de infraestructura cumple ese contrato. Así, los detalles de Retrofit o Room no tienen que determinar la forma de las reglas de negocio.

La inversión de dependencias es un principio de diseño; la inyección de dependencias es una técnica para proporcionar objetos. Se relacionan, pero tienen significados distintos.

4. Inyección manual: el contenedor

Sin librerías adicionales, un contenedor concentra la construcción de los objetos:

class AppContainer {
    private val servicio = ServicioPedidos()

    val pedidosRepository: PedidosRepository =
        PedidosRepositoryRemoto(servicio)
}

Para cambiar el origen de los datos bastaría modificar el punto de composición:

class AppContainer {
    val pedidosRepository: PedidosRepository = PedidosRepositorySimulado()
}

No hace falta editar el ViewModel ni los componentes de la pantalla.

En Android, el contenedor puede alojarse en una clase Application:

class MiAplicacion : Application() {
    val container by lazy { AppContainer() }
}

Si se crea una clase Application personalizada, debe declararse mediante android:name en el elemento <application> del manifiesto.

¿Quién construye un ViewModel con parámetros?

Android administra los ViewModels mediante un ViewModelStoreOwner. Al requerir argumentos propios, debe existir una forma de proporcionar esas dependencias. Una alternativa manual es viewModelFactory:

val Factory = viewModelFactory {
    initializer {
        PedidosViewModel(
            (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]
                as MiAplicacion).container.pedidosRepository
        )
    }
}

En Compose, un consumidor puede solicitarlo mediante viewModel(factory = Factory).

Esta solución es razonable para aplicaciones pequeñas, pero al aumentar el número de servicios, repositorios y ViewModels, la composición manual puede exigir más configuración y mantenimiento.

5. De DI manual a Hilt

Hilt es una biblioteca de inyección de dependencias para Android construida sobre Dagger. Dagger genera código para construir un grafo de dependencias; Hilt aporta componentes y convenciones adaptados al ciclo de vida de Android.

La diferencia principal puede resumirse así:

Construcción manual Con Hilt
El contenedor declara cómo crear objetos Los constructores y módulos describen cómo crearlos
Una Factory crea cada ViewModel Hilt integra la creación de ViewModels
El equipo mantiene el código de composición Dagger/Hilt genera gran parte del código
La elección de implementación se escribe en el contenedor La elección puede declararse con un módulo

Hilt no elimina las decisiones arquitectónicas. El equipo sigue definiendo contratos, implementaciones, dependencias, alcances y límites entre capas.

6. Gradle, KSP y generación de código

Hilt requiere el plugin de Gradle, las dependencias correspondientes y un procesador de anotaciones. En un proyecto que utiliza KSP, la estructura conceptual es:

plugins {
    // Plugins de Android y Kotlin Compose del proyecto.
    alias(libs.plugins.ksp)
    alias(libs.plugins.hilt)
}

dependencies {
    implementation(libs.hilt.android)
    ksp(libs.hilt.compiler)
    implementation(libs.hilt.lifecycle.viewmodel.compose)
}

Los alias suponen que el catálogo libs.versions.toml ya los define. Las versiones exactas deben elegirse de acuerdo con el Android Gradle Plugin, Kotlin, KSP, Hilt y el conjunto de dependencias del proyecto.

hilt-android aporta la integración de Hilt en ejecución. hilt-compiler, declarado con ksp(...), procesa anotaciones durante la compilación y genera código que la aplicación necesita. Por eso la dependencia del compilador no se registra como una biblioteca ordinaria con implementation(...).

En materiales de distintas fechas pueden aparecer kapt, KSP e imports diferentes de hiltViewModel. Al estudiar, identifica la función de cada pieza antes de memorizar una coordenada o una versión.

7. Las piezas fundamentales de Hilt

7.1 @HiltAndroidApp: entrada de la aplicación

@HiltAndroidApp
class MiAplicacion : Application()

La anotación activa la generación de los componentes de Hilt vinculados a la aplicación. La clase personalizada debe permanecer registrada en el manifiesto.

7.2 @AndroidEntryPoint: integración con Android

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent { PedidosApp() }
    }
}

Permite que Hilt administre la inyección de dependencias de la Activity y facilite el acceso a ViewModels integrados con Hilt. Según el tipo de componente Android, existen requisitos de integración específicos.

7.3 @Inject constructor: clases que Hilt sabe crear

class ServicioPedidos @Inject constructor() {
    suspend fun consultar(): List<Pedido> = emptyList()
}

class PedidosRepositoryRemoto @Inject constructor(
    private val servicio: ServicioPedidos
) : PedidosRepository {
    override suspend fun obtenerPedidos(): List<Pedido> = servicio.consultar()
}

Hilt puede inferir que para crear PedidosRepositoryRemoto necesita un ServicioPedidos y seguir el grafo hasta encontrar cómo obtener cada pieza. Si una dependencia obligatoria no tiene una forma conocida de construcción, se producirá un error de compilación del grafo.

7.4 @Module, @InstallIn y @Binds: elegir una implementación

Una interfaz no tiene constructor. Si el consumidor pide PedidosRepository, debemos indicar qué implementación entregar:

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

    @Binds
    abstract fun vincularPedidos(
        implementacion: PedidosRepositoryRemoto
    ): PedidosRepository
}

@Binds establece el vínculo entre contrato e implementación. La implementación debe ser construible por Hilt, normalmente gracias a @Inject constructor o a otra provisión válida.

@InstallIn(SingletonComponent::class) indica en qué componente se instala el módulo. No significa por sí mismo que todos los objetos proporcionados sean instancias únicas: para ello deben declararse los alcances adecuados, como @Singleton cuando corresponda.

7.5 @HiltViewModel y hiltViewModel()

@HiltViewModel
class PedidosViewModel @Inject constructor(
    private val repositorio: PedidosRepository
) : ViewModel() {
    // Estado y lógica de presentación.
}

Desde una pantalla Compose integrada con Hilt:

@Composable
fun PedidosRoute(
    viewModel: PedidosViewModel = hiltViewModel()
) {
    // Obtener el estado del ViewModel y entregarlo a componentes de UI.
}

La función hiltViewModel() obtiene el ViewModel dentro de su alcance correspondiente. En aplicaciones con Navigation Compose, el ViewModelStoreOwner del destino o del grafo determina su ciclo de vida, de forma coherente con lo estudiado sobre navegación.

No debe asumirse que cada recomposición crea un ViewModel nuevo.

8. @Binds y @Provides: dos necesidades diferentes

@Binds es apropiado cuando queremos decir: para este contrato, utiliza esta implementación, y Hilt ya sabe construir la implementación.

@Provides es apropiado cuando necesitamos escribir explícitamente cómo construir una instancia, por ejemplo porque procede de una biblioteca externa o requiere configuración especializada.

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

    @Provides
    fun proporcionarRetrofit(): Retrofit = Retrofit.Builder()
        .baseUrl("https://ejemplo.org/api/")
        .build()
}

En una aplicación real, la construcción de Retrofit puede incluir un cliente OkHttp configurado, conversores y otros parámetros. El fragmento solo muestra la forma de @Provides.

Situación Opción habitual
Clase propia con constructor inyectable @Inject constructor
Interfaz y una implementación elegida @Binds
Objeto de biblioteca externa o construcción especial @Provides

Hilt admite otros mecanismos, pero estos cubren la progresión central de la práctica.

9. Alcances y ciclo de vida

La inyección de dependencias plantea dos preguntas distintas: cómo se construye un objeto y cuánto tiempo se comparte.

Por omisión, una provisión sin alcance no garantiza que se entregue la misma instancia en todas las solicitudes. El alcance restringe la instancia al componente de Hilt correspondiente.

Alcance o componente Idea principal
SingletonComponent con @Singleton Compartir una instancia en el componente de aplicación
ActivityRetainedComponent y su alcance Conservar dependencias durante recreaciones de configuración de una Activity
ViewModelComponent con @ViewModelScoped Compartir una dependencia dentro de un ViewModel
Provisión sin anotación de alcance No se garantiza una única instancia compartida

Ejemplo: un repositorio con caché en memoria podría necesitar un alcance compartido; un objeto de uso breve no siempre lo necesita. Elegir @Singleton por costumbre puede mantener objetos o recursos más tiempo del necesario.

Hilt tampoco reemplaza el almacenamiento permanente: una instancia compartida sigue viviendo dentro del proceso de la aplicación. Si el proceso termina, su contenido en memoria desaparece salvo que se haya persistido por otra vía.

10. Datos reales, simulaciones y pruebas

La posibilidad de cambiar implementaciones fue el experimento central de la práctica original. Conviene distinguir tres usos:

  • Demostración: devolver una colección fija sin conectarse a un servidor.
  • Prueba unitaria: proporcionar una implementación controlada que permita comprobar el comportamiento del ViewModel.
  • Prueba de integración: verificar cómo colaboran servicios reales o componentes configurados de manera similar a producción.

Una implementación simulada puede tener el mismo contrato que la remota:

class PedidosRepositorySimulado @Inject constructor() : PedidosRepository {
    override suspend fun obtenerPedidos(): List<Pedido> = listOf(
        Pedido(id = 7, descripcion = "Compra de prueba")
    )
}

Para usarla en el ejemplo anterior, se cambia el vínculo del módulo:

@Binds
abstract fun vincularPedidos(
    implementacion: PedidosRepositorySimulado
): PedidosRepository

La pantalla y el ViewModel permanecen iguales. Esto demuestra el valor de centralizar la elección de dependencias.

En pruebas de proyectos mayores, Hilt también ofrece herramientas para reemplazar módulos y vínculos de forma específica para el entorno de pruebas. Cambiar manualmente un módulo de producción puede servir como demostración, pero no es la estrategia más sólida para mantener pruebas automatizadas repetibles.

Conexión transversal: alcance y decisiones

Principio, herramienta y alcance

La inyección de dependencias consiste en recibir colaboradores desde fuera; Hilt automatiza su provisión, pero DI puede hacerse manualmente. Tampoco es obligatorio introducir una interfaz por cada clase. @InstallIn(SingletonComponent::class) determina dónde se instala un binding, pero una sola instancia compartida exige el alcance correspondiente, como @Singleton, o la construcción apropiada.

La facilidad para sustituir una implementación por un fake es una capacidad de diseño; la prueba debe precisar qué está verificando (guía 11).

11. Diagnóstico de errores de Hilt

La mayoría de los problemas iniciales aparecen durante la compilación, cuando Dagger intenta construir el grafo. Conviene leer qué tipo de objeto falta, quién lo solicitó y por qué no existe una provisión válida.

Síntoma Qué investigar
MissingBinding de una interfaz Falta un vínculo con @Binds o una provisión con @Provides
MissingBinding de una clase concreta No tiene constructor inyectable ni provisión disponible
Falta @HiltAndroidApp La clase Application no está integrada o no es la registrada en el manifiesto
Falta @AndroidEntryPoint El componente Android que usa Hilt no está habilitado
Error al crear un ViewModel con constructor no vacío Se está usando un mecanismo de creación que no suministra dependencias
Referencia no resuelta a hiltViewModel Revisar dependencia e import compatibles con la versión del proyecto
Mensaje sobre el procesador de Hilt Comprobar plugin, procesador y configuración KSP
DuplicateBindings Existen provisiones en conflicto para una misma dependencia
Error de dependencia circular Las clases se necesitan mutuamente sin un diseño que resuelva el ciclo

Cómo leer un MissingBinding

Supongamos que el compilador informa que PedidosRepository no puede proporcionarse y muestra que esa interfaz es solicitada por PedidosViewModel.

El razonamiento es:

  1. El ViewModel declara una dependencia PedidosRepository.
  2. Es una interfaz y no puede instanciarse directamente.
  3. Hay que encontrar un módulo que vincule esa interfaz con una implementación.
  4. La implementación también debe poder construirse junto con todas sus dependencias.

El error puede aparecer en un archivo generado, como los componentes que crea Dagger. Normalmente se corrige el código fuente o la configuración, no el archivo generado.

12. Decisiones de diseño y errores conceptuales

DI no convierte automáticamente una clase en comprobable

Si una clase recibe un repositorio pero mezcla su lógica con llamadas directas a estado global, funciones estáticas o recursos de Android, todavía puede ser difícil probarla. La inyección facilita el aislamiento, pero hay que diseñar límites claros.

No todas las dependencias requieren una interfaz

Una interfaz tiene un costo de lectura y mantenimiento. Úsala cuando aporta una frontera clara, una sustitución necesaria o una abstracción estable.

@InstallIn y @Singleton cumplen funciones diferentes

Instalar un módulo en SingletonComponent no vuelve singleton a todas sus provisiones. La configuración del alcance requiere una decisión explícita.

Hilt no decide qué implementación es correcta

Si existen una versión remota y otra simulada, Hilt necesita un vínculo. Un error de compilación al faltar ese vínculo es preferible a que el programa elija silenciosamente una opción equivocada.

La inyección tampoco resuelve la navegación ni la persistencia

Un ViewModel creado con Hilt mantiene las reglas normales de ciclo de vida. Un repositorio inyectado que guarda datos solamente en memoria sigue perdiéndolos cuando termina el proceso.

13. Ejercicio integrado de interpretación

Lee el siguiente diseño:

interface PerfilRepository {
    suspend fun cargarPerfil(): Perfil
}

class PerfilRepositoryRemoto @Inject constructor(
    private val api: PerfilApi
) : PerfilRepository {
    override suspend fun cargarPerfil(): Perfil = api.obtenerPerfil()
}

@HiltViewModel
class PerfilViewModel @Inject constructor(
    private val repository: PerfilRepository
) : ViewModel()

Situación A. PerfilApi es una interfaz generada por Retrofit, sin constructor propio y sin módulo proveedor.

Situación B. PerfilApi ya tiene una provisión válida, pero no existe un módulo para PerfilRepository.

Situación C. Se agregan módulos válidos para ambas piezas y la app funciona. Después se quiere probar el ViewModel con datos predeterminados.

Interpretación: en A, Hilt no sabe cómo obtener la instancia de PerfilApi; se necesita una provisión como @Provides. En B, no sabe qué implementación de PerfilRepository utilizar; un @Binds puede resolverlo. En C, se puede sustituir el repositorio con una implementación controlada para la prueba sin modificar el ViewModel.

14. Autoevaluación

Comprensión conceptual

  1. ¿Cuál es el problema de que un ViewModel construya directamente el repositorio que utiliza?
  2. ¿Qué es inyección de dependencias y cuál es la ventaja de recibirlas por constructor?
  3. ¿Cómo se relacionan inyección de dependencias e inversión de dependencias?
  4. ¿Por qué un contenedor manual puede ser suficiente en una aplicación pequeña?
  5. ¿Es obligatorio crear una interfaz para cada clase que se inyecta?
  6. ¿Qué diferencia existe entre Dagger y Hilt?
  7. ¿Qué hace KSP y por qué el compilador de Hilt no se declara como una dependencia ordinaria de ejecución?

Interpretación de código

  1. En class A @Inject constructor(val b: B), ¿qué necesita saber Hilt para construir A?
  2. ¿Qué operación representa un método abstracto anotado con @Binds?
  3. ¿Qué información aporta @InstallIn(SingletonComponent::class) y qué no garantiza?
  4. ¿Por qué una interfaz de Retrofit suele necesitar una provisión mediante @Provides?
  5. ¿Qué cambia cuando viewModel(factory = Factory) se sustituye por hiltViewModel() en una aplicación configurada correctamente?
  6. ¿Cuándo conviene una dependencia anotada con @Singleton?
  7. ¿Qué problema anuncia un error [Dagger/MissingBinding]?

Aplicación y diagnóstico

  1. Necesitas mostrar datos simulados sin modificar un ViewModel. ¿Cómo diseñarías el repositorio?
  2. Hilt conoce RepositorioRemoto, pero el ViewModel solicita Repositorio como interfaz. ¿Qué falta?
  3. El constructor del repositorio necesita Retrofit, pero el proyecto no explica cómo crearlo. ¿Qué harías?
  4. El compilador presenta DuplicateBindings para una interfaz. ¿Qué revisarías?
  5. Si un repositorio es @Singleton, ¿sus datos en memoria sobreviven a la terminación del proceso?
  6. Una app utiliza Hilt en una Activity, pero falla al iniciarse porque no encuentra la aplicación de Hilt. ¿Qué comprobarías?

15. Respuestas posibles

  1. El ViewModel asume la responsabilidad de construir la infraestructura; esto dificulta sustituir el repositorio y probar la lógica de forma aislada.
  2. Consiste en recibir los objetos necesarios desde el exterior. El constructor declara las dependencias obligatorias y permite crear la clase con implementaciones alternativas.
  3. La inversión de dependencias es un principio para dirigir las relaciones entre módulos hacia abstracciones apropiadas; DI es una técnica que suministra objetos según esas relaciones.
  4. Permite concentrar la construcción en un lugar sin añadir una herramienta de generación de código. Puede resultar claro y fácil de depurar cuando el grafo es pequeño.
  5. No. Hilt puede inyectar clases concretas. Una interfaz es útil cuando existe un contrato significativo o la necesidad de sustituir implementaciones.
  6. Dagger genera el grafo de dependencias; Hilt construye sobre Dagger y ofrece integración y componentes preparados para Android.
  7. KSP procesa el código durante la compilación. hilt-compiler genera código de apoyo y se registra en la configuración del procesador, por ejemplo ksp(...).
  8. Debe poder obtener una instancia de B, ya sea mediante otro constructor inyectable o mediante una provisión compatible.
  9. Vincula una solicitud de un tipo, normalmente una interfaz, con una implementación concreta ya construible.
  10. Indica el componente que recibe el módulo. Por sí sola, la anotación no hace singleton a las dependencias que proporciona.
  11. Retrofit crea la implementación de esa interfaz dinámicamente; no existe un constructor de la interfaz que pueda anotarse con @Inject.
  12. Hilt suministra el ViewModel y sus dependencias mediante su integración con el mecanismo de ViewModels de Android. El alcance continúa dependiendo del ViewModelStoreOwner correspondiente.
  13. Cuando es necesario compartir una sola instancia en el componente de aplicación, por ejemplo un servicio configurado centralmente; debe justificarse por sus responsabilidades y recursos.
  14. Dagger no encontró una manera válida de proporcionar alguno de los tipos solicitados dentro del grafo.
  15. Definiría un contrato estable, una implementación real y otra simulada; configuraría cuál recibe el ViewModel desde el contenedor o los módulos.
  16. Falta vincular la interfaz con la implementación, por ejemplo con @Binds instalado en el componente apropiado.
  17. Añadiría un módulo con una función @Provides que construya Retrofit con la configuración necesaria.
  18. Buscaría vínculos o funciones proveedoras que estén entregando el mismo tipo sin una diferenciación válida.
  19. No. @Singleton comparte una instancia mientras vive el componente correspondiente en el proceso; no proporciona persistencia en disco.
  20. Comprobaría @HiltAndroidApp, la clase Application registrada en el manifiesto, @AndroidEntryPoint y la configuración de Gradle.

16. Síntesis de estudio

La arquitectura de una aplicación Android mejora cuando las clases declaran lo que necesitan y la responsabilidad de construirlas se concentra en un punto controlado. La inyección de dependencias puede realizarse a mano mediante un contenedor y una Factory. Hilt automatiza la construcción del grafo mediante anotaciones y generación de código, con especial integración para Activities y ViewModels.

Para preparar el examen, conviene dominar la secuencia completa: detectar acoplamiento, definir dependencias explícitas, introducir contratos cuando aporten valor, elegir implementaciones, construir el grafo y diagnosticar las provisiones que faltan.