Dependency Injection: qué es, inyección de dependencias en iOS y Android

Autor: IT Sectr Publicado: 2026-02-18 Tiempo de lectura: 9 min

Dependency Injection (DI, inyección de dependencias) es una técnica mediante la cual un objeto recibe sus dependencias desde el exterior en lugar de crearlas por sí mismo. DI es una implementación del principio IoC (Inversión de Control) y es la base de Dagger, Hilt y Swinject. La inyección de dependencias reduce el acoplamiento del código, simplifica las pruebas y hace que la arquitectura sea flexible. En Android, DI es estándar a través de Dagger Hilt de Google; en iOS, a través de Swinject o inyección manual. Más información en la Guía de DI de Android.

Puntos clave

  • Dependency Injection — las dependencias se pasan al objeto desde fuera, no se crean internamente
  • Inversion of Control — DI implementa el principio IoC, el flujo de control se transfiere al contenedor
  • Dagger Hilt — el estándar DI para Android, basado en Dagger de Google
  • Swinject — un framework DI popular para iOS y Swift
  • Reducción del acoplamiento — una clase depende de abstracciones, no de implementaciones concretas

Qué es Dependency Injection: esencia y tipos de DI

Dependency Injection es una técnica mediante la cual un objeto recibe dependencias (servicios, repositorios, configuraciones) a través de un constructor, setter o interfaz, en lugar de crearlas él mismo con new. El objetivo de DI es reducir el acoplamiento entre clases. Si una clase crea dependencias por sí misma, está fuertemente vinculada a implementaciones concretas, lo que dificulta las pruebas y la modificación. Con DI, la clase trabaja con una abstracción (protocolo/interfaz) y la implementación concreta se proporciona desde el exterior.

Tres formas de inyección — Constructor Injection (a través de init/constructor), Setter Injection (a través de propiedad/setter), Interface Injection (a través de un método de interfaz). Constructor Injection es el método preferido: las dependencias son claramente visibles en la firma y el objeto siempre se crea en un estado válido. Setter Injection se usa para dependencias opcionales con valor predeterminado. Interface Injection es raro, principalmente para contenedores DI.

Tipo de DIMétodoCuándo usarloEjemplo
ConstructorParámetros del inicializadorDependencias obligatoriasinit(service: ServiceProtocol)
PropertyPropiedad de claseDependencias opcionalesvar service: ServiceProtocol?
MethodParámetro de métodoDependencias temporalesfunc doWork(with service: Service)

Contenedor DI — una biblioteca que gestiona la creación y el ciclo de vida de las dependencias. El contenedor contiene registros de tipos (cada tipo abstracto asignado a una implementación concreta) y una fábrica para crear objetos con dependencias resueltas. En Android — Dagger/Hilt, en iOS — Swinject, Needle, Dip. El contenedor puede gestionar el ámbito (scope): singleton (una instancia por aplicación), ámbito de funcionalidad (por pantalla) o un nuevo objeto en cada solicitud.

Dagger Hilt: DI para Android con generación de código

Dagger Hilt es un envoltorio sobre Dagger de Google, la biblioteca DI estándar para Android. Hilt simplifica Dagger: elimina la creación manual de componentes, añade @HiltAndroidApp, @AndroidEntryPoint y @Module. Hilt se integra con el ciclo de vida de Android: ViewModel, Activity, Fragment, Service, BroadcastReceiver pueden recibir dependencias mediante anotaciones. La generación de código ocurre en tiempo de compilación — Dagger genera implementaciones de componentes, lo que proporciona una sobrecarga de runtime cero.

kotlin
// Application class
@HiltAndroidApp
class MyApp : Application()

// Module — define cómo crear dependencias
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().build()

    @Provides
    @Singleton
    fun provideApiService(client: OkHttpClient): ApiService {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com")
            .client(client)
            .build()
            .create(ApiService::class.java)
    }
}

// ViewModel recibe dependencia a través del constructor
@HiltViewModel
class MainViewModel @Inject constructor(
    private val apiService: ApiService
) : ViewModel() {
    private val _state = MutableStateFlow(MainState.Loading)
    val state: StateFlow<MainState> = _state.asStateFlow()

    fun loadData() {
        viewModelScope.launch {
            _state.value = MainState.Success(apiService.getData())
        }
    }
}

// Activity — @AndroidEntryPoint habilita DI
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()
}

Componentes y ámbitos de Dagger — @Singleton (toda la aplicación), @ActivityScoped (por Activity), @FragmentScoped (por Fragment), @ViewModelScoped (por ViewModel). La elección del ámbito determina el tiempo de vida del objeto. @Singleton — una instancia por proceso, adecuado para OkHttpClient y bases de datos. @ActivityScoped — el objeto vive mientras vive la Activity, para dependencias de pantalla. @ViewModelScoped — novedad en Hilt 2.45+, el objeto vive mientras vive el ViewModel, conveniente para ámbitos de corrutinas.

Swinject: DI para iOS en Swift

Swinject es un framework DI popular de código abierto para iOS. Swinject proporciona Container, Assemblies y varios ámbitos. A diferencia de Dagger, Swinject funciona en tiempo de ejecución — las dependencias se resuelven dinámicamente sin generación de código. Esto hace que Swinject sea más fácil de configurar, pero la depuración es más difícil: un error de dependencia no resuelta solo aparece en tiempo de ejecución. Swinject admite Constructor Injection, Property Injection y Method Injection.

swift
import Swinject

// Assembly — un grupo de registros
class NetworkAssembly: Assembly {
    func assemble(container: Container) {
        container.register(NetworkServiceProtocol.self) { _ in
            NetworkService()
        }.inObjectScope(.container) // singleton

        container.register(UserRepositoryProtocol.self) { r in
            UserRepository(
                networkService: r.resolve(NetworkServiceProtocol.self)!
            )
        }
    }
}

// ViewModel mediante Constructor Injection
class ProfileViewModel: ObservableObject {
    private let repository: UserRepositoryProtocol

    init(repository: UserRepositoryProtocol) {
        self.repository = repository
    }

    @Published var user: User?
    func loadUser() {
        repository.fetchUser { [weak self] user in
            self?.user = user
        }
    }
}

// Configuración de DI en AppDelegate o App
let assembler = Assembler([NetworkAssembly(), ServiceAssembly()])
let viewModel = assembler.resolver.resolve(ProfileViewModel.self)!

// Property Injection para UIKit ViewController
container.register(UserViewController.self) { r in
    let vc = UserViewController()
    vc.viewModel = r.resolve(ProfileViewModel.self)
    return vc
}

Ámbitos de Swinject — .transient (nuevo objeto cada vez), .container (singleton por contenedor), .graph (por defecto — el objeto se comparte dentro de un mismo grafo de dependencias). Para aplicaciones iOS, .container y .transient son suficientes. Swinject también admite Assembler — agrupación de Assembly para una arquitectura modular. Para pruebas, Assembly se reemplaza por MockAssembly, lo que permite la sustitución de dependencias sin cambiar el código de producción.

Comparación de DI con Service Locator e inyección manual

DI vs Service Locator — ambos patrones resuelven la gestión de dependencias, pero de manera diferente. DI inyecta dependencias en el objeto; Service Locator proporciona un registro global del que el objeto solicita las dependencias por sí mismo. DI declara explícitamente las dependencias a través del constructor (o setter). Service Locator oculta las dependencias — se solicitan dentro del método, lo que hace que la firma sea menos informativa. DI es más fácil de probar: basta con pasar un mock al constructor. Service Locator requiere configurar el registro global para cada prueba.

CaracterísticaDependency InjectionService LocatorInyección manual
Visibilidad de dependenciasEn el constructorOcultas en el cuerpo del métodoExplícitas
PruebasMock en el constructorConfiguración del LocatorMock en el constructor
Complejidad de configuraciónRequiere contenedor DIRegistro globalCreación manual
Sobrecarga en runtimeDagger — tiempo de compilaciónBúsqueda en runtimeNinguna

DI vs inyección manual — sin contenedor DI, las dependencias se crean manualmente en fábricas o AppDelegate. Para 5-10 clases, la inyección manual es más simple — no requiere aprender Dagger o Swinject. Para 50+ clases, la inyección manual se vuelve problemática: constructores con 5-6 parámetros, orden de creación complejo, duplicación de código. Un contenedor DI automatiza estos procesos y proporciona una gestión clara del ciclo de vida. La inyección manual sin contenedor es una buena opción para proyectos pequeños y prototipos.

Mejores prácticas de Dependency Injection

Constructor Injection — estándar. Utilice siempre Constructor Injection para dependencias obligatorias. Esto hace que las dependencias sean explícitas y el objeto esté siempre listo para funcionar. Setter Injection — solo para dependencias opcionales (por ejemplo, delegate o listener). Interface Injection — no lo use a menos que esté escribiendo su propia biblioteca DI. Constructor Injection es la única forma de garantizar que un objeto se cree en un estado válido.

Una clase — una responsabilidad. Si el constructor de una clase requiere 5+ parámetros, probablemente la clase viola el Principio de Responsabilidad Única. Divida la clase en varias con menos dependencias. Una señal: si escribe una clase ServiceManager con 6 servicios diferentes — esto es el antipatrón God Object. Extraiga la lógica de negocio en Use Cases (Interactors), cada uno con 1-2 dependencias.

kotlin
// ❌ Malo: 6 dependencias — God Object
class ProfileViewModel @Inject constructor(
    private val api: ApiService,
    private val db: Database,
    private val analytics: Analytics,
    private val prefs: Preferences,
    private val location: LocationProvider,
    private val notification: NotificationManager
)

// ✅ Bueno: Use Cases con 1-2 dependencias
class ProfileViewModel @Inject constructor(
    private val loadProfileUseCase: LoadProfileUseCase,
    private val trackAnalyticsUseCase: TrackAnalyticsUseCase
)

Ámbito y ciclo de vida — elija el ámbito correcto para cada dependencia. Singletons: OkHttpClient, base de datos, SharedPreferences. Ámbito de funcionalidad: repositorios, Use Cases (si no tienen estado). Transient: Value Objects, DateFormatter, analizadores. Los errores de ámbito son un problema común: un singleton que almacena el estado de la pantalla provoca fugas. En Android, @ActivityScoped de Hilt resuelve esto; en Swinject, use .container con precaución.

Preguntas frecuentes

¿Por qué necesito DI si puedo simplemente usar new?

new crea un acoplamiento fuerte entre clases — no se puede cambiar la implementación sin modificar el código. Probar se vuelve difícil: no se puede inyectar un mock en lugar de un servicio real. Se viola SRP: la clase es responsable tanto de la lógica de negocio como de crear dependencias. DI resuelve estos problemas inyectando dependencias desde el exterior y trabajando con abstracciones.

Dagger Hilt o Koin — ¿cuál elegir para Android?

Dagger Hilt es el estándar de Google, DI en tiempo de compilación con generación de código, mejor rendimiento e integración con Jetpack. Koin es DI en tiempo de ejecución, más fácil de configurar, pero más lento y con errores en runtime. Elija Hilt para proyectos de producción. Koin es adecuado para prototipos y aplicaciones pequeñas.

¿Es Swinject la única opción DI para iOS?

No. Para iOS están disponibles: Swinject (runtime, popular), Needle (tiempo de compilación de Uber), Dip (ligero), Weaver (basado en Sourcery). Apple no proporciona un contenedor DI integrado, pero la inyección manual a través de init es una práctica estándar. Para SwiftUI, a menudo es suficiente la DI manual a través de Environment o @StateObject sin bibliotecas externas.

¿Se puede usar DI sin un framework?

Sí. La inyección manual a través del constructor es DI sin framework. Service Locator es una alternativa sin framework. Las fábricas y Factory Method también son formas de DI. Un framework (Dagger, Swinject) automatiza el registro rutinario y la resolución de dependencias, pero para 10-20 clases, la DI manual es suficiente.

¿DI es un patrón o un principio?

DI es una técnica (plantilla) que implementa el principio de Inversión de Control. A diferencia de los patrones GoF, DI no tiene una estructura estricta de 3-4 clases. DI es una forma de organizar dependencias, no un patrón de diseño. Los contenedores DI (Dagger, Swinject) son frameworks que automatizan esta técnica.

Resumen

  • DI — una técnica de inyección de dependencias desde el exterior a través de constructor, setter o método
  • Dagger Hilt — el estándar DI para Android con generación de código en tiempo de compilación y @HiltViewModel
  • Swinject — DI en runtime para iOS con Container, Assembly y ámbitos
  • Constructor Injection — el método preferido para dependencias obligatorias
  • Ámbito — Singleton para servicios sin estado, ámbito de funcionalidad para dependencias de pantalla
  • Pruebas — DI simplifica el reemplazo de dependencias por mocks sin cambiar el código

Desarrollaremos una aplicación móvil llave en mano

IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.

Discutir el proyecto

Lea también