Dependency Injection (DI, injeksi dependensi) — teknik di mana objek menerima dependensinya dari luar, bukan membuatnya sendiri. DI adalah implementasi dari prinsip IoC (Inversion of Control) dan menjadi dasar Dagger, Hilt, dan Swinject. Injeksi dependensi mengurangi keterikatan kode, menyederhanakan pengujian, dan membuat arsitektur menjadi fleksibel. Di Android, DI adalah standar melalui Dagger Hilt dari Google, di iOS — melalui Swinject atau injeksi manual. Selengkapnya — di Android DI Guide.
Poin Utama
Dependency Injection — teknik di mana objek menerima dependensi (layanan, repositori, konfigurasi) melalui konstruktor, setter, atau antarmuka, alih-alih membuatnya sendiri dengan new. Tujuan DI adalah mengurangi keterikatan (coupling) antar kelas. Jika kelas membuat dependensinya sendiri, ia terikat erat pada implementasi konkret, yang mempersulit pengujian dan modifikasi. Dengan DI, kelas bekerja dengan abstraksi (protocol/interface), dan implementasi konkret disediakan dari luar.
Tiga cara injeksi — Constructor Injection (melalui init/constructor), Setter Injection (melalui properti/setter), Interface Injection (melalui metode antarmuka). Constructor Injection — cara yang lebih disukai: dependensi terlihat jelas di tanda tangan, objek selalu dibuat dalam keadaan valid. Setter Injection digunakan untuk dependensi opsional dengan nilai default. Interface Injection — jarang, terutama untuk container DI.
| Jenis DI | Cara | Kapan digunakan | Contoh |
|---|---|---|---|
| Constructor | Parameter inisialisasi | Dependensi wajib | init(service: ServiceProtocol) |
| Property | Properti kelas | Dependensi opsional | var service: ServiceProtocol? |
| Method | Parameter metode | Dependensi sementara | func doWork(with service: Service) |
Container DI — pustaka yang mengelola pembuatan dan siklus hidup dependensi. Container berisi registrasi tipe (setiap tipe abstrak dipetakan ke implementasi konkret) dan pabrik untuk membuat objek dengan dependensi yang telah diselesaikan. Di Android — Dagger/Hilt, di iOS — Swinject, Needle, Dip. Container dapat mengelola lingkup (Scope): singleton (satu instance per aplikasi), lingkup fitur (per layar), atau objek baru setiap permintaan.
Dagger Hilt — lapisan di atas Dagger dari Google, pustaka DI standar untuk Android. Hilt menyederhanakan Dagger: menghilangkan pembuatan komponen manual, menambahkan @HiltAndroidApp, @AndroidEntryPoint, dan @Module. Hilt terintegrasi dengan siklus hidup Android: ViewModel, Activity, Fragment, Service, BroadcastReceiver dapat menerima dependensi melalui anotasi. Pembuatan kode terjadi pada tahap kompilasi — Dagger menghasilkan implementasi komponen, yang memberikan overhead runtime nol.
// Application class
@HiltAndroidApp
class MyApp : Application()
// Module — menentukan cara membuat dependensi
@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 menerima dependensi melalui konstruktor
@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 mengaktifkan DI
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
}
Komponen Dagger dan cakupan visibilitas — @Singleton (untuk seluruh aplikasi), @ActivityScoped (untuk Activity), @FragmentScoped (untuk Fragment), @ViewModelScoped (untuk ViewModel). Pemilihan lingkup menentukan masa hidup objek. @Singleton — satu instance per proses, cocok untuk OkHttpClient dan basis data. @ActivityScoped — objek hidup selama Activity hidup, untuk dependensi layar. @ViewModelScoped — baru di Hilt 2.45+, objek hidup selama ViewModel hidup, nyaman untuk lingkup coroutine.
Swinject — framework DI populer untuk iOS dengan sumber terbuka. Swinject menyediakan Container, Assemblies, dan berbagai lingkup. Berbeda dengan Dagger, Swinject bekerja di runtime — dependensi diselesaikan secara dinamis tanpa pembuatan kode. Ini membuat Swinject lebih sederhana dalam konfigurasi, tetapi menyulitkan debugging: kesalahan dependensi yang belum terselesaikan hanya muncul di runtime. Swinject mendukung Constructor Injection, Property Injection, dan Method Injection.
import Swinject
// Assembly — grup registrasi
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 melalui 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
}
}
}
// Konfigurasi DI di AppDelegate atau App
let assembler = Assembler([NetworkAssembly(), ServiceAssembly()])
let viewModel = assembler.resolver.resolve(ProfileViewModel.self)!
// Property Injection untuk UIKit ViewController
container.register(UserViewController.self) { r in
let vc = UserViewController()
vc.viewModel = r.resolve(ProfileViewModel.self)
return vc
}
Lingkup Swinject — .transient (objek baru setiap kali), .container (singleton per container), .graph (default — objek dibagikan dalam satu graf dependensi). Untuk aplikasi iOS, .container dan .transient sudah cukup. Swinject juga mendukung Assembler — pengelompokan Assembly untuk arsitektur modular. Untuk pengujian, Assembly diganti dengan MockAssembly, yang memungkinkan penggantian dependensi tanpa mengubah kode produksi.
DI vs Service Locator — kedua pola memecahkan masalah pengelolaan dependensi, tetapi dengan cara berbeda. DI menyuntikkan dependensi ke objek, Service Locator menyediakan registri global tempat objek meminta dependensinya sendiri. DI mendeklarasikan dependensi secara eksplisit melalui konstruktor (atau setter). Service Locator menyembunyikan dependensi — dependensi diminta di dalam metode, yang membuat tanda tangan kurang informatif. DI lebih mudah diuji: cukup berikan mock di konstruktor. Service Locator memerlukan konfigurasi registri global untuk setiap pengujian.
| Karakteristik | Dependency Injection | Service Locator | Injeksi manual |
|---|---|---|---|
| Kejelasan dependensi | Di konstruktor | Tersembunyi di badan metode | Eksplisit |
| Pengujian | Mock di konstruktor | Konfigurasi Locator | Mock di konstruktor |
| Kompleksitas konfigurasi | Memerlukan container DI | Registri global | Pembuatan manual |
| Overhead runtime | Dagger — compile-time | Runtime lookup | Tidak ada |
DI vs injeksi manual — tanpa container DI, dependensi dibuat secara manual di pabrik atau AppDelegate. Untuk 5-10 kelas, injeksi manual lebih sederhana — tidak perlu mempelajari Dagger atau Swinject. Untuk 50+ kelas, injeksi manual menjadi masalah: konstruktor dengan 5-6 parameter, urutan pembuatan yang rumit, duplikasi kode. Container DI mengotomatiskan proses ini dan memberikan siklus hidup yang jelas. Injeksi manual tanpa container — pilihan yang baik untuk proyek kecil dan prototipe.
Constructor Injection — standar. Selalu gunakan Constructor Injection untuk dependensi wajib. Ini membuat dependensi eksplisit dan objek selalu siap digunakan. Setter Injection — hanya untuk dependensi opsional (misalnya, delegate atau listener). Interface Injection — jangan digunakan, kecuali Anda menulis pustaka DI sendiri. Constructor Injection — satu-satunya cara untuk memastikan objek dibuat dalam keadaan valid.
Satu kelas — satu tanggung jawab. Jika konstruktor kelas memerlukan 5+ parameter, kemungkinan kelas melanggar Single Responsibility Principle. Bagi kelas menjadi beberapa dengan dependensi yang lebih sedikit. Tanda: jika Anda menulis kelas ServiceManager dengan 6 layanan berbeda — ini adalah anti-pola God Object. Pindahkan logika bisnis ke Use Cases (Interactors), masing-masing dengan 1-2 dependensi.
// ❌ Buruk: 6 dependensi — 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
)
// ✅ Baik: Use Cases dengan 1-2 dependensi
class ProfileViewModel @Inject constructor(
private val loadProfileUseCase: LoadProfileUseCase,
private val trackAnalyticsUseCase: TrackAnalyticsUseCase
)
Lingkup dan siklus hidup — pilih lingkup yang tepat untuk setiap dependensi. Singleton: OkHttpClient, basis data, SharedPreferences. Lingkup fitur: repositori, Use Cases (jika tidak memiliki status). Transient: Value Objects, DateFormatter, parser. Kesalahan lingkup — masalah umum: singleton yang menyimpan status layar menyebabkan kebocoran memori. Di Android Hilt, @ActivityScoped memecahkan masalah ini, di Swinject — .container dengan hati-hati.
Pertanyaan yang Sering Diajukan
new menciptakan keterikatan kaku antar kelas — Anda tidak dapat mengganti implementasi tanpa mengubah kode. Pengujian sulit dilakukan: tidak bisa memasukkan mock sebagai pengganti layanan nyata. SRP dilanggar: kelas bertanggung jawab atas logika bisnis dan pembuatan dependensi. DI memecahkan masalah ini melalui injeksi dependensi dari luar dan bekerja dengan abstraksi.
Dagger Hilt — standar dari Google, DI compile-time dengan pembuatan kode, performa lebih baik, dan integrasi dengan Jetpack. Koin — DI runtime, lebih sederhana dalam konfigurasi, tetapi lebih lambat dan dengan kesalahan runtime. Pilih Hilt untuk proyek produksi. Koin cocok untuk prototipe dan aplikasi kecil.
Tidak. Untuk iOS tersedia: Swinject (runtime, populer), Needle (compile-time dari Uber), Dip (ringan), Weaver (berbasis Sourcery). Apple tidak menyediakan container DI bawaan, tetapi injeksi manual melalui init adalah praktik standar. Untuk SwiftUI, DI manual melalui Environment atau @StateObject tanpa pustaka eksternal seringkali sudah cukup.
Ya. Injeksi manual melalui konstruktor — ini adalah DI tanpa framework. Service Locator — alternatif tanpa framework. Pabrik dan Factory Method — juga bentuk DI. Framework (Dagger, Swinject) mengotomatiskan registrasi rutin dan penyelesaian dependensi, tetapi untuk 10-20 kelas, DI manual sudah cukup.
DI — adalah teknik (pola) yang menerapkan prinsip Inversion of Control. Berbeda dengan pola GoF, DI tidak memiliki struktur ketat 3-4 kelas. DI adalah cara mengatur dependensi, bukan pola desain. Container DI (Dagger, Swinject) adalah framework yang mengotomatiskan teknik ini.
Kesimpulan
Kami akan mengembangkan aplikasi seluler turnkey
IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.
Baca juga