Introducción a Dependency Injection en Kotlin
En el desarrollo moderno con Kotlin, especialmente al crear aplicaciones Android, el principio de Dependency Injection (DI) se ha convertido no solo en una buena práctica, sino en una necesidad. La inyección de dependencias permite escribir código limpio, testeable y escalable, reduciendo el acoplamiento entre los componentes del sistema.
En este artículo analizaremos qué es Dependency Injection, qué problemas resuelve y cómo implementar DI en Kotlin, desde el enfoque manual hasta el uso de frameworks populares como Hilt y Koin.
¿Qué es Dependency Injection (Inyección de Dependencias)?
Dependency Injection es un patrón de diseño en el cual un objeto recibe sus dependencias desde el exterior, en lugar de crearlas por sí mismo. En lugar de escribir val engine = Engine() dentro de la clase, "inyectamos" una instancia ya preparada a través del constructor, un método o un campo.
Veamos un ejemplo simple sin DI:
class Car { private val engine = Engine() // Acoplamiento rígido
fun start() { engine.start() }}Problema: la clase Car está fuertemente acoplada a una implementación concreta de Engine. Es difícil de testear y extender.
Ahora un ejemplo con DI mediante constructor:
class Car(private val engine: Engine) { // La dependencia se pasa desde el exterior
fun start() { engine.start() }}
// Usoval engine = Engine()val car = Car(engine)car.start()Ahora Car no sabe cómo se crea Engine. Podemos reemplazarlo fácilmente por un MockEngine para pruebas o por otra implementación.
¿Por qué es importante DI en Kotlin?
Kotlin, como lenguaje moderno, ofrece excelentes capacidades para DI:
- Null Safety — Kotlin ayuda a evitar NullPointerException al trabajar con dependencias.
- Sintaxis concisa — los constructores con parámetros hacen que DI sea natural.
- Compatibilidad con Java — todos los frameworks DI populares (Dagger, Hilt) soportan Kotlin.
- Estilo funcional — Kotlin permite usar lambdas y delegados para implementar DI.
Formas de implementar DI en Kotlin
1. Inyección manual (Manual DI)
La forma más simple es pasar las dependencias a través del constructor. Para proyectos pequeños esto suele ser suficiente.
class Database(private val connection: Connection)
class UserRepository(private val database: Database)
class UserViewModel(private val userRepository: UserRepository)
// Ensamblaje manual de dependenciasfun main() { val connection = Connection("postgres://localhost:5432") val database = Database(connection) val repository = UserRepository(database) val viewModel = UserViewModel(repository)}Inconveniente: a medida que el proyecto crece, la gestión manual se vuelve engorrosa.
2. Service Locator (Antipatrón o solución temporal)
A veces se usa Service Locator, un objeto central que almacena todas las dependencias. Sin embargo, se considera un antipatrón porque oculta las dependencias reales de la clase.
object ServiceLocator { private val database = Database() private val repository = UserRepository(database) fun provideViewModel() = UserViewModel(repository)}
// Usoval viewModel = ServiceLocator.provideViewModel()3. Frameworks DI: Hilt y Koin
Para proyectos serios es mejor usar soluciones ya preparadas. Veamos las dos más populares para Kotlin/Android.
Hilt (basado en Dagger)
Hilt es la biblioteca DI oficial de Google para Android. Está basada en Dagger, pero con una API simplificada.
// 1. Anotación @HiltAndroidApp en Application@HiltAndroidAppclass MyApplication : Application(