Dependency Injection Kullanımı

İbrahim Ethem Şen
6 min readMay 6, 2023

--

Kişisel veya profesyonel olarak uygulamalar geliştirirken kodlarımızı daha iyi hale getirmek için çeşitli tekniklere başvuruyoruz. Uygulamalarımızı geliştirirken de yapılarımız arasında bağımlılıklar oluşturuyoruz. Bağımlılıklarımızı yönetmek için Dependecy Injection’a başvuruyoruz.Dependency injection’a alternatif olarak service locator tasarım deseni kullanılabilir ama Service locator yazımızın konusu değildir. Ama aklınızda bulunmasında sakınca yok. Dependency Injection’unun avantajlarına ve uygulamalarımızda nasıl kullanacağımıza bakalım. Kullanımlar Manuel ve Dagger,Hilt,Koin ve Anvil üzerinde genel bilgi verecektir.

Gerçek hayattan örnekle gidelim. Uygulamamızda bir geliştirici bir de kullanıcı tarafı vardır. Geliştiriciler kullanıcıların istekleri için gerekli özellikler hazırlamakla sorumludur. Android I/O veya Kotlin Conf’da olduğumuzu düşünelim. Sunumları dinlemek için giden bizler uygulamalarımıza gelen kullanıcılardır. Konferanslarda sunum yapacak kişileri ise yazılımcılar olarak düşünebiliriz.

Peki konferans salonunun hazırlanması ve diğer bazı teferruatlı işlemleri ne olarak değerlendireceğiz ? Bunlar bizlerin bağımlılıklardır. Bir bina ve salonlara ihtiyacımız var. Her konuşmacının ihtiyacı olan teknik alt yapı,ikramlar ve daha bir sürü tekrarlanan işlem. Bunların hepsini konuşmacılar yapabilir(Manuel) veya organizasyonlara(Dagger,Hilt…) yaptırabilirler böylelikle bağımlılıklarımızın hazırlanması kolaylaştırılabilir.

Resmi Belgeler

Uygulama tarafına geri döndüğümüzde Clean Architecture mimarisini benimseyelim. Görselde ki gibi bir akışımız olacak. ViewModel’lerin Repository’e onların ise DataSource’a ihtiyacı var.

Konferansımıza kaldığımız yerden devam edelim. Konuşmacıların konferans da sorumluluk aldığını düşünelim Yani Manuel Dependency Injection her şeyi kendimiz yapacağız. Bu durumda bize kimse yardım etmeyecek saf kod.

Konferansların belirli grafikleri vardır kullanıcıların sunum saatleri,binanın içindeki salonların bağlantıları vs. Uygulamalarımız kullanıcılar için belirli akışlar(Kayıt-Giriş-Bottom Navigation vs.)sunar. Bu akışlar bağımlılıklarımızıda barındırır. Akışlara örnek olarak bir Login işlemi üzerinden gidelim. Fragment -> ViewModel -> Repository-> DataSource’u verebiliriz. Oluşturmaya başlayalım. Burada resmi belgelerden ilerleyeceğim.

Bağımlılıklarımızı oluşturmaya başlayalım activity -> viewmodel -> repository -> datasource -> retrofit

class UserRepository(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
) { ... }

class UserLocalDataSource { ... }
class UserRemoteDataSource(
private val loginService: LoginRetrofitService
) { ... }

class LoginActivity: Activity() {
private lateinit var loginViewModel: LoginViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val retrofit = Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(LoginService::class.java)

val remoteDataSource = UserRemoteDataSource(retrofit)
val localDataSource = UserLocalDataSource()

val userRepository = UserRepository(localDataSource, remoteDataSource)

loginViewModel = LoginViewModel(userRepository)
}
}

Bir kullanım senaryosu için gerekli kodu oluşturduk. Activity için ViewModel onun için ise gerekli Repository ve DataSource nesnelerini oluşturduk. Ama burada dikkat etmemiz gereken ilk nokta bunların sıralı gerçekleştirilmesi. DataSource olmadan bir Repository oluşturamayız. Uygulamada sadece bir ekranda bunu kullanmayacağız. Bir konferansımız yok her konferans salonu(Uygulama Ekranı) için aynı kodları tekrarlamamız gerekli.

Birden fazla ViewModel-Repository… ve daha bir çok kodu tekrar yazmamak için ortak olan yapıları Container’lar da birleştiriyoruz. Böylelikle yaptığımız bazı tekrarlamalar azalıyor.

class MyApplication : Application() {
val appContainer = AppContainer()
}

class AppContainer {
private val retrofit = Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(LoginService::class.java)

private val remoteDataSource = UserRemoteDataSource(retrofit)
private val localDataSource = UserLocalDataSource()

val userRepository = UserRepository(localDataSource, remoteDataSource)
}
class LoginActivity: Activity() {

private lateinit var loginViewModel: LoginViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val appContainer = (application as MyApplication).appContainer
loginViewModel = LoginViewModel(appContainer.userRepository)
}
}

Uygulama düzeyinde (Application) Container’ın bir instance’ını oluşturduğumuz için artık uygulama içinde buna istediğimiz yerden erişebiliyoruz.Ama bu tam olarak yeterli değil Android ve Kotlin konferanslarında farklı konseptlerde konferans salonları olmasını istiyoruz. Ekranlarımızı isterlere göre oluşturabilecek bir yapıya ihtiyacımız var.

Bunun için Factory tasarım desenini kullanarak bir kod hazırlayabiliriz.

interface Factory<T> {
fun create(): T
}
class LoginViewModelFactory(private val userRepository: UserRepository) : Factory {
override fun create(): LoginViewModel {
return LoginViewModel(userRepository)
}
}

Artık kodumuzda tekrarlanan sayılar biraz daha azaldı. Ama her zaman uygulama düzeyinde(Application) çalışmıyoruz.Yani Feature,belirli bir akış veya yapıya her zaman ihtiyacımız yok. İhtiyacımız olmadığı durumlarda bunları uygulamamızdan kaldırmalıyız ki gereksiz bellek kullanımına sebep olmayalım. Bunun için konferans alanında sabah kahvaltısı için açık büfe olduğunu düşünebiliriz.Bina alanı içindeki yerimiz sabit öğleden sonra veya akşam saatlerinde kahvaltı için bir alana ihtiyacımız yok.Burası kaldırılmalı ki o alan tekrar ihtiyaca göre kullanılabilsin.

Uygulama özellikleri,akışı veya yapıları için bu durumları ayrı Container’lar halinde tutup bunlarla işimiz bittiğinde kaldırmalıyız. Örneğin kullanıcı login olduktan sonra LoginContainer’ına gerek yok

class AppContainer {
val userRepository = UserRepository(localDataSource, remoteDataSource)

var loginContainer: LoginContainer? = null
}

Manuel olarak işlemleri yaptığımız için kullandığımız bağımlılıkların temizlenmesini de bizim yapmamız gerekli.

Özetlemek gerekirse konuşmacı(Yazılımcı) olarak konferanslarda ki tüm sorumlulukları kendimiz yaptık. Bir organizasyon şeması(Mimari) belirledik. Buradaki bağımlılıkları belirledik(Repository-DataSource…) ve oluşturduk.Alanımız kısıtlı olduğu için kullanacağımız zaman dilimi içinde gerekli standları(LoginContainer …) kurduk ve ihtiyacımız bitince kaldırdık.

Şimdi bu durumu bizim yerimize yapabilecek olan organizasyonlara bakalım.

Dagger :

Google tarafından desteklenen gelişmiş bir DI kütüphanesidir. Projenin ölçeği arttıkça artan karmaşıklığı Dagger ile sınırlayabiliriz. Manuel olarak yaptığımız kodları arka planda kendisi otomatik olarak üretir ve yerine koyar.

  • Manuel olarak uyguladığımız AppContainer gibi grafikleri otomatik oluşturur.
  • Factory tasarım deseni ile oluşturduğumuz bağımlılıkları oluşturur.
  • Alt bileşenler ve kapsamların kullanılıp kullanılamayacağını ve bellekte temizleme işlemlerini bizim yerimize yapar.

Dagger’a bağımlılıkları nasıl oluşturacağını bildirdiğimiz taktirde bunları otomatik olarak bizim yerimize yapar. Build Time’da kodu gözden geçirir böylelikle Run time’da exception almamızın önüne geçer. Run time’da gerekli yerlerde bağımlılıkları oluşturur. Bunu organizasyonun çalışma prensibi gibi düşünelim konferans başlamadan(Build Time) gerekli kontrolleri sağlıyor. Eğer bir sorun varsa dinleyicilerin haberi bile olmadan onu çözebiliriz.

Uygulamalarımızın özellikleri,kullanıcı akışları için gerekli grafikleri oluşturmamızda kolaylık sağlar.Bunların hangi kapsamda çalışması gerektiğini belirleyebiliriz.

@Component
interface ApplicationGraph {
fun repository(): UserRepository
}

Dagger arka planda kodları üretmek için kapt’ı kullanır. Kapt diğer popüler kütüphanelerle de kullanılmaktadır aşina gelebilir. Bunu belirtmemin sebebi yazının ilerleyen bir kısmında kıyaslama yapacağız.

plugins {
id 'kotlin-kapt'
}

dependencies {
implementation 'com.google.dagger:dagger:2.x'
kapt 'com.google.dagger:dagger-compiler:2.x'
}

Hilt :

Dagger üzerine kurulmuş Google tarafından resmi olarak android uygulamalarında kullanılması önerilen daha az karmaşıklığa DI kütüphanesidir. Dagger ile birlikte kullanılabilirler. Build time’da Dagger bileşenlerini oluşturur. Dagger gibi bağımlılıkları Run Time’da oluşturur.

@HiltAndroidApp
class ExampleApplication : Application() { ... }

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() { ... }

Dagger’da grafikleri yani akışı belirtmemiz gerekliyken Hilt de gerekli değildir bunu bizim yerimize yapar.

Hilt basitleştirilmesiyle birlikte bazı kısıtlamaları birlikte getirir.

  • AppCompatActivity ve ComponentActivity’yi kalıtım alanları destekler
  • Fragment’da androidx.Fragment’i kalıtım alanları destekler
  • Retained Fragments’leri desteklemez.

Avantajları ise

  • Dagger ile kullanıldığı için altyapıyı basitleştirir.
  • Kurulum,okunabilirlik ve uygulamalar arasında kod paylaşımını kolaylaştırır
  • Test, Binding vs. için hazır yapılar sunar kolaylıklar sağlar.
  • Standart kodu azaltır.

Dagger gibi kapt’ı kullanarak kodları üretir.

plugins {
kotlin("kapt")
id("com.google.dagger.hilt.android")
}
dependencies {
implementation("com.google.dagger:hilt-android:2.44")
kapt("com.google.dagger:hilt-android-compiler:2.44")
}

Anvil :

Square tarafından Dagger modüllerini ve bileşenlerini otomatik olarak birleştirerek DI kolaylaştıran bir “Kotlin Derleyici Eklentisidir”. Anvil reposunda “Hilt kullanıyorsanız Anvil kullanmanıza gerek yok” ifadesi yazar. Ama Dagger kullanıyorsak ve işlemlerimizde kolaylıklara ihtiyacımız varsa ?

Büyük bir projemizin olduğunu(500+ modül) düşünelim. Projemizde Dagger kullanılmakta. Modülleri birleştirecek ve farklı özellik ve akışlar için Container kullanabileceğimiz yapılara ihtiyacımız var.

@Module
@ContributesTo(AppScope::class)
class DaggerModule { .. }

@ContributesTo(AppScope::class)
interface ComponentInterface {
fun getSomething(): Something
fun injectActivity(activity: MyActivity)
}

@MergeComponent(AppScope::class)
interface AppComponent

Anvil’i kullana biliriz.Dagger üzerine kurulu olduğu için Dagger ile çalışarak kolaylıklar sağlamakta. Kapt büyük projelerde arka planda yaptığı işlemlerle büyük bir build time oluşturmakta. Anvil bu durumlar için kapt’la birlikte bize bazı kolaylıklar daha sağlamakta.

Dagger Factory Generation

Anvil grafikleri ayrıştırma ve bağımlılıkları değiştirme de de başarılıdır. Bununla birlikte bazı dezavantajları da vardır.

  • Bytecode’un düzeltilmesini sağlayamaz
  • Yalnızca yeni kod üretir.
  • Oluşturulan kodlara IDE içinden başvurulamaz.

Koin :

Kotlin’e özel olarak tasarlanmış bir DI kütüphanesidir. Dagger dan farklı olarak compile time da annotation işleme ve kod oluşturma kullanmaz. Runtime da reflection da kullanmaz bunun yerine inline ve refeined gibi Kotlin’in kendi yapılarını kullanır. Kotlin DSL ile dependency injection’ları hazırlamak için API sunar.

Inline ve refeined’ın fonksiyonlarda kullanımı için Kotlin Inline Function Examples yazıma bakabilirsiniz.. Dagger ve Hilt’e göre daha az özellik sunmaktadır. Ama basit ve kolaylıkla kullanılabilir. Kotlin Multi Platform için geliştirilmektedir.

startKoin {
// Application
modules(coffeeAppModule)
}

DI kütüphane ve araçları aslında farklı şekillerde çalışsada arka planda bizlerin yapması tekrar tekrar yapması gereken yapıları kurar. Her biri ayrı bir yazının konusu olabilir. Projelerimizde kullanmadan önce neye ihtiyacımız olduğunu isterlerimizi iyi bilmeliyiz.

  • Modülleri mi birleştireceğiz ?
  • Hafif ve basit bir kütüphaneye mi ihtiyacımız var ?
  • Platformlar arasında mı çalışacağız ?

ve daha bir çok soruyu sorabiliriz.

Görüş öneri veya eleştirileriniz var ise LinkedIn veya Twitter’dan bildirirseniz sevinirim.

--

--