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 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 Type | Method | When to Use | Example |
|---|---|---|---|
| Constructor | Initializer parameters | Required dependencies | init(service: ServiceProtocol) |
| Property | Class property | Optional dependencies | var service: ServiceProtocol? |
| Method | Method parameter | Temporary dependencies | func 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 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.
// 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 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.
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 — 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.
| Characteristic | Dependency Injection | Service Locator | Manual Injection |
|---|---|---|---|
| Dependency visibility | In constructor | Hidden in method body | Explicit |
| Testing | Mock in constructor | Locator setup | Mock in constructor |
| Setup complexity | Requires DI container | Global registry | Manual creation |
| Runtime overhead | Dagger — compile-time | Runtime lookup | None |
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.
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.
// ❌ 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
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 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.
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.
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.
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
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.
Read also