Dependency Injection (DI, függőséginjektálás) — technika, amelynek során egy objektum kívülről kapja meg a függőségeit, ahelyett hogy maga hozná létre azokat. A DI az IoC (Inversion of Control) elv megvalósítása, és a Dagger, Hilt és Swinject alapját képezi. A függőséginjektálás csökkenti a kód csatolását, egyszerűsíti a tesztelést és rugalmassá teszi az architektúrát. Android-ban a DI szabvány a Google Dagger Hilt-jén keresztül, iOS-ben — Swinject vagy manuális injektálás útján. Bővebben — Android DI Guide.
Főbb pontok
Dependency Injection — technika, amelynek során egy objektum a függőségeit (szolgáltatások, adattárak, konfigurációk) a konstruktoron, setteren vagy interfészen keresztül kapja, ahelyett hogy maga hozná létre azokat new-val. A DI célja az osztályok közötti csatolás (coupling) csökkentése. Ha egy osztály maga hozza létre a függőségeit, szorosan kötődik a konkrét implementációkhoz, ami megnehezíti a tesztelést és a módosítást. DI esetén az osztály egy absztrakcióval (protocol/interface) dolgozik, a konkrét implementáció pedig kívülről érkezik.
Három injektálási mód — Constructor Injection (init/constructor révén), Setter Injection (tulajdonság/setter révén), Interface Injection (interfész metódusán keresztül). Constructor Injection — az előnyben részesített mód: a függőségek egyértelműen látszanak a szignatúrában, az objektum mindig érvényes állapotban jön létre. A Setter Injection opcionális függőségekhez használatos alapértelmezett értékkel. Interface Injection — ritka, főleg DI konténerekhez.
| DI típusa | Mód | Mikor használjuk | Példa |
|---|---|---|---|
| Constructor | Inicializáló paraméterek | Kötelező függőségek | init(service: ServiceProtocol) |
| Property | Osztály tulajdonsága | Opcionális függőségek | var service: ServiceProtocol? |
| Method | Metódus paramétere | Átmeneti függőségek | func doWork(with service: Service) |
DI konténer — könyvtár, amely kezeli a függőségek létrehozását és életciklusát. A konténer tartalmazza a típusok regisztrációját (minden absztrakt típus egy konkrét implementációhoz van rendelve) és egy gyárat az objektumok létrehozásához feloldott függőségekkel. Android-ban — Dagger/Hilt, iOS-ben — Swinject, Needle, Dip. A konténer kezelheti a hatókört (Scope): singleton (egy példány alkalmazásonként), funkció hatókör (képernyőnként) vagy új objektum minden kérésnél.
Dagger Hilt — réteg a Google Dagger-je fölött, a szabványos DI könyvtár Android-hoz. A Hilt leegyszerűsíti a Dagger-t: eltávolítja a komponensek manuális létrehozását, hozzáadja a @HiltAndroidApp, @AndroidEntryPoint és @Module elemeket. A Hilt integrálódik az Android életciklussal: ViewModel, Activity, Fragment, Service, BroadcastReceiver annotációkon keresztül kaphatnak függőségeket. A kódgenerálás a fordítás során történik — a Dagger létrehozza a komponensek implementációit, ami nulla runtime overhead-et eredményez.
// Application class
@HiltAndroidApp
class MyApp : Application()
// Module — meghatározza, hogyan kell létrehozni a függőségeket
@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 a függőséget a konstruktoron keresztül kapja
@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 engedélyezi a DI-t
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
}
Dagger komponensek és láthatósági hatókörök — @Singleton (a teljes alkalmazásra), @ActivityScoped (Activity-re), @FragmentScoped (Fragment-re), @ViewModelScoped (ViewModel-re). A hatókör kiválasztása határozza meg az objektum élettartamát. @Singleton — egy példány folyamatonként, alkalmas OkHttpClient és adatbázis számára. @ActivityScoped — az objektum addig él, amíg az Activity él, képernyőfüggőségekhez. @ViewModelScoped — újdonság a Hilt 2.45+-ban, az objektum addig él, amíg a ViewModel él, kényelmes a korutin hatókörökhöz.
Swinject — népszerű DI keretrendszer iOS-hez nyílt forráskóddal. A Swinject Container-t, Assemblies-t és különböző hatóköröket biztosít. A Dagger-rel ellentétben a Swinject runtime-ban működik — a függőségek dinamikusan oldódnak fel kódgenerálás nélkül. Ez egyszerűbbé teszi a Swinject konfigurálását, de megnehezíti a hibakeresést: a fel nem oldott függőség hibája csak runtime-ban jelenik meg. A Swinject támogatja a Constructor Injection, Property Injection és Method Injection módszereket.
import Swinject
// Assembly — regisztrációs csoport
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 segítségével
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 konfiguráció AppDelegate-ben vagy App-ben
let assembler = Assembler([NetworkAssembly(), ServiceAssembly()])
let viewModel = assembler.resolver.resolve(ProfileViewModel.self)!
// Property Injection UIKit ViewController számára
container.register(UserViewController.self) { r in
let vc = UserViewController()
vc.viewModel = r.resolve(ProfileViewModel.self)
return vc
}
Swinject hatókörök — .transient (minden alkalommal új objektum), .container (singleton konténerenként), .graph (alapértelmezett — az objektum megosztásra kerül egy függőségi gráfon belül). iOS alkalmazásokhoz a .container és .transient elegendő. A Swinject támogatja az Assembler-t is — az Assembly-k csoportosítását moduláris architektúrához. Tesztekhez az Assembly-k MockAssembly-re cserélhetők, ami lehetővé teszi a függőségek cseréjét a termelési kód módosítása nélkül.
DI vs Service Locator — mindkét minta megoldja a függőségkezelés problémáját, de különböző módon. A DI függőségeket injektál az objektumba, a Service Locator egy globális nyilvántartást biztosít, ahonnan az objektum maga kéri le a függőségeket. A DI explicit módon deklarálja a függőségeket a konstruktoron (vagy setteren) keresztül. A Service Locator elrejti a függőségeket — azok a metódus belsejében kerülnek lekérésre, ami kevésbé informatívvá teszi a szignatúrát. A DI könnyebben tesztelhető: elég egy mock-ot tenni a konstruktorba. A Service Locator a globális nyilvántartás konfigurálását igényli minden teszthez.
| Jellemző | Dependency Injection | Service Locator | Manuális injektálás |
|---|---|---|---|
| Függőségek egyértelműsége | A konstruktorban | Rejtve a metódus törzsében | Explicit |
| Tesztelés | Mock a konstruktorban | Locator konfigurálása | Mock a konstruktorban |
| Konfiguráció összetettsége | DI konténert igényel | Globális nyilvántartás | Manuális létrehozás |
| Runtime overhead | Dagger — compile-time | Runtime lookup | Nincs |
DI vs manuális injektálás — DI konténer nélkül a függőségek manuálisan jönnek létre gyárakban vagy AppDelegate-ben. 5-10 osztály esetén a manuális injektálás egyszerűbb — nem igényli a Dagger vagy Swinject tanulását. 50+ osztály esetén a manuális injektálás problémává válik: 5-6 paraméteres konstruktorok, bonyolult létrehozási sorrend, kódismétlés. A DI konténer automatizálja ezeket a folyamatokat és tiszta életciklust biztosít. Manuális injektálás konténer nélkül — jó választás kis projektekhez és prototípusokhoz.
Constructor Injection — szabvány. Mindig használjon Constructor Injection-t a kötelező függőségekhez. Ez explicité teszi a függőségeket és az objektum mindig munkára kész. Setter Injection — csak opcionális függőségekhez (pl. delegate vagy listener). Interface Injection — ne használja, kivéve ha saját DI könyvtárat ír. Constructor Injection — az egyetlen mód annak garantálására, hogy az objektum érvényes állapotban jön létre.
Egy osztály — egy felelősség. Ha egy osztály konstruktora 5+ paramétert igényel, az osztály valószínűleg megsérti a Single Responsibility Principle elvet. Oszsza fel az osztályt több, kevesebb függőséggel rendelkező részre. Jel: ha egy ServiceManager osztályt ír 6 különböző szolgáltatással — ez a God Object anti-minta. Vigye át az üzleti logikát Use Cases (Interactors) egységekbe, mindegyik 1-2 függőséggel.
// ❌ Rossz: 6 függőség — 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
)
// ✅ Jó: Use Cases 1-2 függőséggel
class ProfileViewModel @Inject constructor(
private val loadProfileUseCase: LoadProfileUseCase,
private val trackAnalyticsUseCase: TrackAnalyticsUseCase
)
Hatókör és életciklus — válassza ki a megfelelő hatókört minden függőséghez. Singleton: OkHttpClient, adatbázis, SharedPreferences. Funkció hatókör: adattárak, Use Cases (ha nincs állapotuk). Transient: Value Objects, DateFormatter, elemzők. Hatókör hiba — gyakori probléma: a képernyő állapotát tároló singleton memóriaszivárgáshoz vezet. Android Hilt-ben a @ActivityScoped megoldja ezt a problémát, Swinject-ben — a .container óvatossággal.
Gyakran Ismételt Kérdések
A new szoros kapcsolatot hoz létre az osztályok között — nem lehet kicserélni az implementációt a kód módosítása nélkül. Nehéz tesztelni: nem lehet mock-ot tenni a valós szolgáltatás helyére. Az SRP sérül: az osztály felelős az üzleti logikáért és a függőségek létrehozásáért is. A DI megoldja ezeket a problémákat a függőségek kívülről történő injektálásával és az absztrakciókkal való munkával.
Dagger Hilt — a Google szabványa, compile-time DI kódgenerálással, jobb teljesítmény és Jetpack integráció. Koin — runtime DI, egyszerűbb konfiguráció, de lassabb és runtime hibákkal. Válassza a Hilt-et termelési projektekhez. A Koin prototípusokhoz és kis alkalmazásokhoz alkalmas.
Nem. iOS-hez elérhető: Swinject (runtime, népszerű), Needle (compile-time az Uber-től), Dip (könnyűsúlyú), Weaver (Sourcery alapú). Az Apple nem biztosít beépített DI konténert, de a manuális injektálás init-en keresztül szabványos gyakorlat. SwiftUI esetén gyakran elegendő a manuális DI Environment vagy @StateObject segítségével külső könyvtárak nélkül.
Igen. Manuális injektálás a konstruktoron keresztül — ez DI keretrendszer nélkül. Service Locator — alternatíva keretrendszer nélkül. Gyárak és Factory Method — szintén a DI egy formája. A keretrendszer (Dagger, Swinject) automatizálja a rutin regisztrációt és a függőségek feloldását, de 10-20 osztályhoz a manuális DI elegendő.
A DI — egy technika (minta), amely az Inversion of Control elvet valósítja meg. A GoF mintákkal ellentétben a DI-nek nincs szigorú 3-4 osztályból álló szerkezete. A DI a függőségek szervezésének módja, nem tervezési minta. A DI konténerek (Dagger, Swinject) keretrendszerek, amelyek automatizálják ezt a technikát.
Összefoglalás
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is