Dependency Injection: що це таке, впровадження залежностей в iOS та Android

Автор: IT Sectr Опубліковано: 2026-02-18 Час читання: 9 хв

Впровадження залежностей (DI) — техніка, за якої об'єкт отримує свої залежності ззовні, а не створює їх самостійно. DI є реалізацією принципу IoC (Inversion of Control) і лежить в основі Dagger, Hilt та Swinject. Впровадження залежностей зменшує зв'язність коду, спрощує тестування та робить архітектуру гнучкою. В Android DI — стандарт через Dagger Hilt від Google, в iOS — через Swinject або ручне впровадження. Детальніше — у Android DI Guide.

Головне

  • Впровадження залежностей — залежності передаються об'єкту ззовні, а не створюються всередині
  • Inversion of Control — DI реалізує принцип IoC, управління потоком передається контейнеру
  • Dagger Hilt — стандарт DI для Android, заснований на Dagger від Google
  • Swinject — популярний DI-фреймворк для iOS та Swift
  • Зменшення зв'язності — клас залежить від абстракцій, а не від конкретних реалізацій

Що таке Dependency Injection: суть та види DI

Впровадження залежностей — техніка, за якої об'єкт отримує залежності (сервіси, репозиторії, конфігурації) через конструктор, сеттер або інтерфейс, а не створює їх сам через new. Мета DI — зменшити зв'язаність (coupling) між класами. Якщо клас створює залежності сам, він жорстко прив'язаний до конкретних реалізацій, що ускладнює тестування та модифікацію. При DI клас працює з абстракцією (protocol/interface), а конкретна реалізація підставляється ззовні.

Три способи впровадження — Constructor Injection (через init/constructor), Setter Injection (через властивість/сеттер), Interface Injection (через метод інтерфейсу). Constructor Injection — найкращий спосіб: залежності явно видно в сигнатурі, об'єкт завжди створюється у валідному стані. Setter Injection використовується для опціональних залежностей зі значенням за замовчуванням. Interface Injection — рідко, в основному для DI-контейнерів.

Вид DIСпосібКоли використовуватиПриклад
ConstructorПараметри ініціалізатораОбов'язкові залежностіinit(service: ServiceProtocol)
PropertyВластивість класуОпціональні залежностіvar service: ServiceProtocol?
MethodПараметр методуТимчасові залежностіfunc doWork(with service: Service)

DI-контейнер — бібліотека, що керує створенням та життєвим циклом залежностей. Контейнер містить реєстрацію типів (кожен абстрактний тип зіставлений з конкретною реалізацією) та фабрику для створення об'єктів з вирішеними залежностями. В Android — Dagger/Hilt, в iOS — Swinject, Needle, Dip. Контейнер може керувати скоупом (Scope): синглтон (один інстанс на додаток), скоуп фічі (на екран) або новий об'єкт при кожному запиті.

Dagger Hilt: DI для Android з кодогенерацією

Dagger Hilt — обгортка над Dagger від Google, стандартна бібліотека DI для Android. Hilt спрощує Dagger: прибирає ручне створення компонентів, додає @HiltAndroidApp, @AndroidEntryPoint та @Module. Hilt інтегрується з життєвим циклом Android: ViewModel, Activity, Fragment, Service, BroadcastReceiver можуть отримувати залежності через анотації. Кодогенерація відбувається на етапі компіляції — Dagger генерує імплементації компонентів, що дає нульовий runtime overhead.

kotlin
// Клас додатку
@HiltAndroidApp
class MyApp : Application()

// Модуль — визначає, як створювати залежності
@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 отримує залежність через конструктор
@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 вмикає DI
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()
}

Dagger компоненти та області видимості — @Singleton (на весь додаток), @ActivityScoped (на Activity), @FragmentScoped (на Fragment), @ViewModelScoped (на ViewModel). Вибір скоупу визначає час життя об'єкта. @Singleton — один інстанс на процес, підходить для OkHttpClient та баз даних. @ActivityScoped — об'єкт живе поки живе Activity, для екранних залежностей. @ViewModelScoped — новинка Hilt 2.45+, об'єкт живе поки живе ViewModel, що зручно для корутинних скоупів.

Swinject: DI для iOS на Swift

Swinject — популярний DI-фреймворк для iOS з відкритим кодом. Swinject надає Container, Assemblies та різні скоупи. На відміну від Dagger, Swinject працює в рантаймі — залежності вирішуються динамічно без кодогенерації. Це робить Swinject простішим у налаштуванні, але складнощі з Debug: помилка невирішеної залежності проявляється тільки в рантаймі. Swinject підтримує Constructor Injection, Property Injection та Method Injection.

swift
import Swinject

// Assembly — група реєстрацій
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 через 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 в AppDelegate або додатку
let assembler = Assembler([NetworkAssembly(), ServiceAssembly()])
let viewModel = assembler.resolver.resolve(ProfileViewModel.self)!

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

Swinject скоупи — .transient (новий об'єкт щоразу), .container (синглтон на контейнер), .graph (за замовчуванням — об'єкт розділяється в межах одного графа залежностей). Для iOS-додатків достатньо .container та .transient. Swinject також підтримує Assembler — групування Assembly для модульної архітектури. Для тестів Assembly замінюються на MockAssembly, що дає підміну залежностей без зміни продакшен-коду.

Порівняння DI з Service Locator та ручним впровадженням

DI vs Service Locator — обидва патерни вирішують задачу управління залежностями, але по-різному. DI впроваджує залежності в об'єкт, Service Locator надає глобальний реєстр, з якого об'єкт сам запитує залежності. DI явно оголошує залежності через конструктор (або сеттер). Service Locator приховує залежності — вони запитуються всередині методу, що робить сигнатуру менш інформативною. DI легше тестується: достатньо підставити mock у конструктор. Service Locator вимагає налаштування глобального реєстру для кожного тесту.

ХарактеристикаDependency InjectionService LocatorРучне впровадження
Явність залежностейВ конструкторіПриховані в тілі методуЯвні
ТестуванняMock у конструкторНалаштування LocatorMock у конструктор
Складність налаштуванняВимагає DI-контейнерГлобальний реєстрРучне створення
Runtime overheadDagger — compile-timeRuntime lookupНемає

DI vs ручне впровадження — без DI-контейнера залежності створюються вручну в фабриках або AppDelegate. Для 5-10 класів ручне впровадження простіше — не вимагає вивчення Dagger або Swinject. Для 50+ класів ручне впровадження стає проблемою: конструктори з 5-6 параметрами, складний порядок створення, дублювання коду. DI-контейнер автоматизує ці процеси та дає чіткий lifecycle. Ручне впровадження без контейнера — хороший вибір для невеликих проєктів та прототипів.

Найкращі практики Dependency Injection

Constructor Injection — стандарт. Завжди використовуйте Constructor Injection для обов'язкових залежностей. Це робить залежності явними і об'єкт завжди готовий до роботи. Setter Injection — тільки для опціональних залежностей (наприклад, delegate або listener). Interface Injection — не використовуйте, якщо тільки не пишете власну DI-бібліотеку. Constructor Injection — єдиний спосіб гарантувати, що об'єкт створюється у валідному стані.

Один клас — одна відповідальність. Якщо конструктор класу вимагає 5+ параметрів, ймовірно, клас порушує принцип єдиної відповідальності. Розділіть клас на декілька з меншою кількістю залежностей. Ознака: якщо ви пишете клас ServiceManager з 6 різними сервісами — це антипатерн God Object. Винесіть бізнес-логіку в Use Cases (Interactors), кожен з 1-2 залежностями.

kotlin
// ❌ Погано: 6 залежностей — 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
)

// ✅ Добре: Use Cases з 1-2 залежностями
class ProfileViewModel @Inject constructor(
    private val loadProfileUseCase: LoadProfileUseCase,
    private val trackAnalyticsUseCase: TrackAnalyticsUseCase
)

Scope та lifecycle — вибирайте правильний скоуп для кожної залежності. Синглтони: OkHttpClient, база даних, SharedPreferences. Feature-скоуп: репозиторії, Use Cases (якщо вони не мають стану). Transient: Value Objects, DateFormatter, парсери. Помилка скоупінгу — часта проблема: синглтон, що зберігає стан екрану, призводить до витоків. В Android Hilt @ActivityScoped вирішує цю проблему, в Swinject — .container з обережністю.

Часті запитання

Навіщо потрібен DI, якщо можна просто створювати new?

new створює жорсткий зв'язок між класами — ви не можете підмінити реалізацію без зміни коду. Важко тестувати: не можна вставити mock замість реального сервісу. Порушується SRP: клас відповідає і за бізнес-логіку, і за створення залежностей. DI вирішує ці проблеми через впровадження залежностей ззовні та роботу з абстракціями.

Dagger Hilt чи Koin — що обрати для Android?

Dagger Hilt — стандарт від Google, compile-time DI з кодогенерацією, краща продуктивність та інтеграція з Jetpack. Koin — runtime DI, простіший у налаштуванні, але повільніший та з помилками в рантаймі. Обирайте Hilt для продакшен-проєктів. Koin підходить для прототипів та маленьких додатків.

Swinject — єдиний DI для iOS?

Ні. Для iOS доступні: Swinject (runtime, популярний), Needle (compile-time від Uber), Dip (легковаговий), Weaver (Sourcery-based). Apple не надає вбудований DI-контейнер, але ручне впровадження через init — стандартна практика. Для SwiftUI часто достатньо ручного DI через Environment або @StateObject без зовнішніх бібліотек.

Чи можна використовувати DI без фреймворку?

Так. Ручне впровадження через конструктор — це DI без фреймворку. Service Locator — альтернатива без фреймворку. Фабрики та Factory Method — теж форма DI. Фреймворк (Dagger, Swinject) автоматизує рутинну реєстрацію та вирішення залежностей, але для 10-20 класів ручного DI достатньо.

DI — це патерн чи принцип?

DI — це техніка (шаблон), що реалізує принцип Inversion of Control. На відміну від GoF-патернів, DI не має суворої структури з 3-4 класів. DI — спосіб організації залежностей, а не патерн проєктування. DI-контейнери (Dagger, Swinject) — це фреймворки, що автоматизують цю техніку.

Підсумки

  • DI — техніка впровадження залежностей ззовні через конструктор, сеттер або метод
  • Dagger Hilt — стандарт DI для Android з compile-time кодогенерацією та @HiltViewModel
  • Swinject — runtime DI для iOS з Container, Assembly та скоупами
  • Constructor Injection — найкращий спосіб для обов'язкових залежностей
  • Scope — Singleton для безстановних сервісів, Feature-скоуп для екранних залежностей
  • Тестування — DI спрощує підміну залежностей на mock без зміни коду

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також