Dependency Injection: какво е това, инжектиране на зависимости в iOS и Android

Автор: IT Sectr Публикувано: 2026-02-18 Време за четене: 9 мин

Dependency Injection (DI, инжектиране на зависимости) — техника, при която обектът получава своите зависимости отвън, вместо да ги създава сам. DI е имплементация на принципа IoC (Inversion of Control) и стои в основата на Dagger, Hilt и Swinject. Инжектирането на зависимости намалява свързаността на кода, опростява тестването и прави архитектурата гъвкава. В Android DI е стандарт чрез Dagger Hilt от Google, в iOS — чрез Swinject или ръчно инжектиране. Повече — в Android DI Guide.

Основни положения

  • Dependency Injection — зависимостите се предават на обекта отвън, а не се създават вътре
  • Inversion of Control — DI имплементира принципа IoC, управлението на потока се предава на контейнера
  • Dagger Hilt — стандартът DI за Android, базиран на Dagger от Google
  • Swinject — популярна DI рамка за iOS и Swift
  • Намаляване на свързаността — класът зависи от абстракции, а не от конкретни имплементации

Какво е Dependency Injection: същност и видове DI

Dependency Injection — техника, при която обектът получава зависимости (услуги, хранилища, конфигурации) чрез конструктор, setter или интерфейс, вместо да ги създава сам чрез new. Целта на DI е да намали свързаността (coupling) между класове. Ако класът сам създава зависимостите си, той е тясно свързан с конкретни имплементации, което затруднява тестването и модификацията. При DI класът работи с абстракция (protocol/interface), а конкретната имплементация се доставя отвън.

Три начина на инжектиране — Constructor Injection (чрез init/constructor), Setter Injection (чрез свойство/setter), 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
// Application class
@HiltAndroidApp
class MyApp : Application()

// Module — определя как да се създават зависимости
@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, удобно за coroutine обхвати.

Swinject: DI за iOS в Swift

Swinject — популярна DI рамка за iOS с отворен код. Swinject предоставя Container, Assemblies и различни обхвати. За разлика от Dagger, Swinject работи в runtime — зависимостите се разрешават динамично без генериране на код. Това прави Swinject по-лесен за конфигуриране, но затруднява дебъгването: грешка от неразрешена зависимост се проявява едва в runtime. 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 или App
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 декларира изрично зависимостите чрез конструктор (или setter). 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+ параметъра, вероятно класът нарушава Single Responsibility Principle. Разделете класа на няколко с по-малко зависимости. Признак: ако пишете клас 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
)

Обхват и lifecycle — изберете правилния обхват за всяка зависимост. Сингълтъни: OkHttpClient, база данни, SharedPreferences. Обхват на функция: хранилища, 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, по-лесен за конфигуриране, но по-бавен и с грешки в runtime. Изберете Hilt за продукционни проекти. Koin е подходящ за прототипи и малки приложения.

Swinject единственото DI за iOS ли е?

Не. За iOS са налични: Swinject (runtime, популярен), Needle (compile-time от Uber), Dip (лек), Weaver (базиран на Sourcery). 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 — техника за инжектиране на зависимости отвън чрез конструктор, setter или метод
  • Dagger Hilt — стандарт DI за Android с compile-time генериране на код и @HiltViewModel
  • Swinject — runtime DI за iOS с Container, Assembly и обхвати
  • Constructor Injection — предпочитаният начин за задължителни зависимости
  • Обхват — Singleton за услуги без състояние, обхват на функция за екранни зависимости
  • Тестване — DI опростява замяната на зависимости с mock без промяна на кода

Ще разработим мобилно приложение под ключ

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също