Dependency Injection: What It Is, Dependency Injection in iOS and Android

Author: IT Sectr Published: 2026-02-18 Reading time: 9 min

Dependency Injection (DI) is a technique where an object receives its dependencies from the outside rather than creating them itself. DI is an implementation of the IoC (Inversion of Control) principle and is the foundation of Dagger, Hilt and Swinject. Dependency injection reduces code coupling, simplifies testing and makes architecture flexible. On Android, DI is standard through Google's Dagger Hilt; on iOS, through Swinject or manual injection. Learn more in the Android DI Guide.

Key Takeaways

  • Dependency Injection — dependencies are passed to the object from outside, not created internally
  • Inversion of Control — DI implements the IoC principle, control flow is transferred to the container
  • Dagger Hilt — the DI standard for Android, based on Google's Dagger
  • Swinject — a popular DI framework for iOS and Swift
  • Reduced coupling — a class depends on abstractions, not concrete implementations

What Is Dependency Injection: Essence and Types of DI

Dependency Injection is a technique where an object receives its dependencies (services, repositories, configurations) through a constructor, setter or interface, rather than creating them itself with new. The goal of DI is to reduce coupling between classes. If a class creates dependencies itself, it is tightly bound to specific implementations, making testing and modification difficult. With DI, the class works with an abstraction (protocol/interface), and the concrete implementation is supplied from the outside.

Three ways of injection — Constructor Injection (via init/constructor), Setter Injection (via property/setter), Interface Injection (via an interface method). Constructor Injection is the preferred approach: dependencies are clearly visible in the signature, and the object is always created in a valid state. Setter Injection is used for optional dependencies with default values. Interface Injection is rare, mainly for DI containers.

DI TypeMethodWhen to UseExample
ConstructorInitializer parametersRequired dependenciesinit(service: ServiceProtocol)
PropertyClass propertyOptional dependenciesvar service: ServiceProtocol?
MethodMethod parameterTemporary dependenciesfunc doWork(with service: Service)

DI container — a library that manages the creation and lifecycle of dependencies. The container holds type registrations (each abstract type mapped to a concrete implementation) and a factory for creating objects with resolved dependencies. On Android — Dagger/Hilt, on iOS — Swinject, Needle, Dip. The container can manage scope: singleton (one instance per application), feature scope (per screen) or a new object on each request.

Dagger Hilt: DI for Android with Code Generation

Dagger Hilt is a wrapper around Google's Dagger, the standard DI library for Android. Hilt simplifies Dagger: it removes manual component creation, adds @HiltAndroidApp, @AndroidEntryPoint and @Module. Hilt integrates with the Android lifecycle: ViewModel, Activity, Fragment, Service, BroadcastReceiver can receive dependencies through annotations. Code generation happens at compile time — Dagger generates component implementations, resulting in zero runtime overhead.

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

// Module — defines how to create dependencies
@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 receives dependency through 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 enables DI
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()
}

Dagger components and scopes — @Singleton (entire application), @ActivityScoped (per Activity), @FragmentScoped (per Fragment), @ViewModelScoped (per ViewModel). The choice of scope determines the object's lifetime. @Singleton — one instance per process, suitable for OkHttpClient and databases. @ActivityScoped — the object lives as long as the Activity, for screen-level dependencies. @ViewModelScoped — new in Hilt 2.45+, the object lives as long as the ViewModel, convenient for coroutine scopes.

Swinject: DI for iOS in Swift

Swinject is a popular open-source DI framework for iOS. Swinject provides Container, Assemblies and various scopes. Unlike Dagger, Swinject works at runtime — dependencies are resolved dynamically without code generation. This makes Swinject easier to set up, but debugging is more difficult: an unresolved dependency error only appears at runtime. Swinject supports Constructor Injection, Property Injection and Method Injection.

swift
import Swinject

// Assembly — a group of registrations
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 via 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
        }
    }
}

// DI setup in AppDelegate or App
let assembler = Assembler([NetworkAssembly(), ServiceAssembly()])
let viewModel = assembler.resolver.resolve(ProfileViewModel.self)!

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

Swinject scopes — .transient (new object every time), .container (singleton per container), .graph (default — the object is shared within a single dependency graph). For iOS applications, .container and .transient are sufficient. Swinject also supports Assembler — grouping Assembly for modular architecture. For testing, Assembly is replaced with MockAssembly, enabling dependency substitution without changing production code.

DI vs Service Locator and Manual Injection

DI vs Service Locator — both patterns solve dependency management, but differently. DI injects dependencies into the object; Service Locator provides a global registry from which the object requests dependencies itself. DI explicitly declares dependencies through the constructor (or setter). Service Locator hides dependencies — they are requested inside the method, making the signature less informative. DI is easier to test: just pass a mock to the constructor. Service Locator requires setting up the global registry for each test.

CharacteristicDependency InjectionService LocatorManual Injection
Dependency visibilityIn constructorHidden in method bodyExplicit
TestingMock in constructorLocator setupMock in constructor
Setup complexityRequires DI containerGlobal registryManual creation
Runtime overheadDagger — compile-timeRuntime lookupNone

DI vs manual injection — without a DI container, dependencies are created manually in factories or AppDelegate. For 5-10 classes, manual injection is simpler — no need to learn Dagger or Swinject. For 50+ classes, manual injection becomes problematic: constructors with 5-6 parameters, complex creation order, code duplication. A DI container automates these processes and provides clear lifecycle management. Manual injection without a container is a good choice for small projects and prototypes.

Dependency Injection Best Practices

Constructor Injection — standard. Always use Constructor Injection for required dependencies. This makes dependencies explicit and the object always ready to work. Setter Injection — only for optional dependencies (e.g., delegate or listener). Interface Injection — do not use it unless you are writing your own DI library. Constructor Injection is the only way to guarantee that an object is created in a valid state.

One class — one responsibility. If a class constructor requires 5+ parameters, the class likely violates the Single Responsibility Principle. Split the class into several with fewer dependencies. A sign: if you write a class ServiceManager with 6 different services — this is the God Object antipattern. Extract business logic into Use Cases (Interactors), each with 1-2 dependencies.

kotlin
// ❌ Bad: 6 dependencies — 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
)

// ✅ Good: Use Cases with 1-2 dependencies
class ProfileViewModel @Inject constructor(
    private val loadProfileUseCase: LoadProfileUseCase,
    private val trackAnalyticsUseCase: TrackAnalyticsUseCase
)

Scope and lifecycle — choose the right scope for each dependency. Singletons: OkHttpClient, database, SharedPreferences. Feature scope: repositories, Use Cases (if they are stateless). Transient: Value Objects, DateFormatter, parsers. Scope errors are a common problem: a singleton holding screen state leads to leaks. In Android, Hilt's @ActivityScoped solves this; in Swinject, use .container with caution.

Frequently Asked Questions

Why do I need DI if I can just use new?

new creates a tight coupling between classes — you cannot swap the implementation without changing the code. Testing becomes difficult: you cannot inject a mock instead of a real service. SRP is violated: the class is responsible for both business logic and creating dependencies. DI solves these problems by injecting dependencies from the outside and working with abstractions.

Dagger Hilt or Koin — which to choose for Android?

Dagger Hilt is the standard from Google, compile-time DI with code generation, better performance and Jetpack integration. Koin is runtime DI, easier to set up, but slower with runtime errors. Choose Hilt for production projects. Koin is suitable for prototypes and small applications.

Is Swinject the only DI option for iOS?

No. For iOS there are: Swinject (runtime, popular), Needle (compile-time from Uber), Dip (lightweight), Weaver (Sourcery-based). Apple does not provide a built-in DI container, but manual injection via init is standard practice. For SwiftUI, manual DI through Environment or @StateObject without external libraries is often sufficient.

Can I use DI without a framework?

Yes. Manual injection through the constructor is DI without a framework. Service Locator is an alternative without a framework. Factories and Factory Method are also forms of DI. A framework (Dagger, Swinject) automates routine registration and dependency resolution, but for 10-20 classes, manual DI is enough.

Is DI a pattern or a principle?

DI is a technique (template) that implements the Inversion of Control principle. Unlike GoF patterns, DI does not have a strict 3-4 class structure. DI is a way of organizing dependencies, not a design pattern. DI containers (Dagger, Swinject) are frameworks that automate this technique.

Summary

  • DI — a technique of injecting dependencies from the outside via constructor, setter or method
  • Dagger Hilt — the DI standard for Android with compile-time code generation and @HiltViewModel
  • Swinject — runtime DI for iOS with Container, Assembly and scopes
  • Constructor Injection — the preferred approach for required dependencies
  • Scope — Singleton for stateless services, Feature scope for screen-level dependencies
  • Testing — DI simplifies replacing dependencies with mocks without changing code

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also