Android Hilt Dependency Injection Kullanımı -1
Herkese merhaba,
Günümüz projelerin standardı haline gelmiş olan dependency injection kavramı ve bu kavramın en popüler kütüphanelerinden biri olan Hilt üzerine konuşacağız.
Yazılım geliştirme süreçlerinde projelerin kodlarının sürdürebilirliği, esnekliği, yeniden kullanılabilirliği vb. gibi kavramlar yazılımcıların her zaman göz önünde bulundurması gereken konulardır. Bu sebeptendir ki hayatımızda pattern, principle ve hatta anti pattern vesaire gibi terimler bulunmaktadır. Bütün bu terimler aslında bizleri tek bir noktaya çıkarmak için vardır. Nasıl daha iyi kod yazabilir ve çıktı alabiliriz. İşte bu noktadan itibaren yukarıda da bahsettiğim “kodların sürdürebilirliği, esnekliği, yeniden kullanılabilirliği” kısmının tanıdık bir dosttan yani SOLID prensiplerinin tanımından geldiğini belki fark etmişsinizdir. Burada uzun uzadıya SOLID anlatımına girmek istemiyorum. Zaten bu konu ile ilgili internette sayısız kaynak bulunmaktadır. Bizi SOLID prensiplerinden ilgilendiren kısım SOLID’ın “D” harfi olan “Dependency Inversion” olacaktır. Peki çok kısa bir şekilde bahsetmek gerekirse “Dependency Inversion” neydi?
Dependency Inversion
Yüksek seviye sınıflar(genel SOLID anlatımlarında ve Freeman’ın kitabında modüle denmekte ama biz işleri daha basitleştirmek için sınıf olarak bahsedeceğiz burada) alt seviye sınıflara bağımlı olmamalıdır. Ve bu ilişki soyutlamalar ile yapılmalıdır.
Özetle bir sınıfın içinde başka bir sınıfın objesini direkt olarak oluşturup bağımlılık yaratılmamalıdır. Buradan da dependency injection’a bağlamak gerekirse, dependency injection, dependency inversion prensibinin uygulanmasıdır diyebiliriz.
Dependency injection yapmak için aslında bir kütüphane kullanımına ihtiyacımız yoktur. Manuel bir şekilde de bunu ele alabiliriz. Manuel bir şekilde dependency injection uygulamak için sınıfların kullanacağı objeleri, bu sınıfların içinde yaratılmaması ve bu işlemlerin dışarıda yapılıp constructor’a paslanması şeklinde kısaca özetleyebiliriz. Konumuz manuel dependecy injection olmadığı için çok detaya girmeyeceğiz ve sizlere direkt olarak developer.android’de bu konu ile ilgili verilmiş bir örneği paylaşıyorum. Bu örnek üzerinden aslında neden Hilt gibi bir kütüphane kullanmamız gerekeceğini anlayacağız.
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)
// In order to satisfy the dependencies of LoginViewModel, you have to also
// satisfy the dependencies of all of its dependencies recursively.
// First, create retrofit which is the dependency of UserRemoteDataSource
val retrofit = Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(LoginService::class.java)
// Then, satisfy the dependencies of UserRepository
val remoteDataSource = UserRemoteDataSource(retrofit)
val localDataSource = UserLocalDataSource()
// Now you can create an instance of UserRepository that LoginViewModel needs
val userRepository = UserRepository(localDataSource, remoteDataSource)
// Lastly, create an instance of LoginViewModel with userRepository
loginViewModel = LoginViewModel(userRepository)
}
}
Görüleceği üzere küçük çaplı bir proje de bile manuel dependency injection kullanmak istersek işlerin biraz çığırından çıkacağını fark etmişsiniz. Proje büyüdükçe boilerplate kod oluşumu, yönetmenin zorlaşacağı gibi dezavantajları bulunmaktadır. Yine de manuel bir şekilde dependency injection uygulayacaksanız, Google tarafı container class üzerinden ele almaktan bahsediyor ama hala bir proje için çok maliyetli olmaya devam edecektir. Zaten bütün bu sebeplerden dolayı Hilt ve türevleri hayatımıza girmiştir.
Hilt
Hilt, projenizde manuel bir şekilde di yapmanın zahmetini azaltan Jetpack ailesinin parçası olan bir kütüphanesidir. Kendisi sıfırdan var olmamış ve yine bir çoğumuzun bildiği ve kullandığı Dagger’ın üzerine geliştirilmiştir. Bunu yaparken de Dagger’ın hantallıklarını giderip, proje bazında di yönetimini daha iyi ve esnek şekilde ele almamıza imkan vermektedir.
Hilt Entegrasyonu
Öncelikle projemde version catalog ve ksp kullandığım için gradle implementasyonları sizlere biraz farklı gelebilir. Açıkçası sizlere de yeni bir projeye başlıyorsanız bu şekilde bir gradle kullanımını öneririm. Ama hala kapt kullandığınız bir proje varsa ve buna eklemek istiyorsanız developer.google’daki örneği de burada paylaşacağım.
[versions]
hiltVersion = "2.51.1"
[libraries]
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hiltVersion" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hiltVersion" }
[plugins]
hiltAndroid = { id = "com.google.dagger.hilt.android", version.ref ="hiltVersion" }
libs.versions.toml(Version Catalog)
plugins {
alias(libs.plugins.hiltAndroid) apply false
}
build.gradle.kts(Project level)
plugins {
alias(libs.plugins.hiltAndroid)
}
dependencies {
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
}
build.gradle.kts(App level)
Aynı zamanda Hilt Java 8 özelliklerini kullanmaktadır. Daha eski bir versiyon kullanımına sahipseniz yine app level’da bulunan java versiyonlarınızı güncellemeyi unutmayın.
android {
...
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
Yukarıdaki gibi ksp ile implementasyon yapacaksanız önemli bir nokta daha bulunmaktadır.
Dagger’ın geliştiricileri bizlere, Dagger’ın ksp desteğinin hala alpha’da olduğunu belirtmekteler ve aynı zamanda versiyon kullanımları ile ilgilide aşağıdaki koşulları bize iletmektedirler.
Gereksinimler
- Dagger
2.48
(veya üstü) - Kotlin
1.9.0
(veya üstü) - KSP
1.9.0-1.0.12
(veya üstü)
Yani özetle ksp ile kullanacaksanız birazdan developer.android’de hilt’in kapt ile kullanımının örneğinde göreceğiniz üzere orada kullanılan “2.44” versiyonunu ksp ile kullanamayacak ve build error alacaksınız. Aynı zamanda projenizdeki Ksp ve Kotlin versiyonları da bu konu ile ilgili önem arz etmektedirler.
Kapt ile implemantasyon yapmak istersek
plugins {
...
id("com.google.dagger.hilt.android") version "2.44" apply false
}
build.gradle(Project level)
plugins {
id("kotlin-kapt")
id("com.google.dagger.hilt.android")
}
android {
...
}
dependencies {
implementation("com.google.dagger:hilt-android:2.44")
kapt("com.google.dagger:hilt-android-compiler:2.44")
}
// Allow references to generated code
kapt {
correctErrorTypes = true
}
build.gradle(App level)
Yine aynı şekilde app level build.gradle’da Java versiyonumuzu unutmuyoruz.
android {
...
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
Gradle işlerimizi bitirdikten sonra artık proje üzerinde Hilt’i kurmaya başlayabiliriz.
@HiltAndroidApp
Hilt kullanan veya kullanılması istenilen bütün projelerde, projenin Application sınıfına @HiltAndroidApp annotation’ını eklememiz gereklidir. Bu Hilt’in kod generate etmesini tetiklemeyi sağlar.
@HiltAndroidApp
class MyApplication: Application() {
}
Bu işlemi yaptıktan sonra build alırsak bir kısmınızın runtime’da exception alması olası olacaktır. Hilt’i geliştiren ekibinde bunu tahmin ettiğini düşündüğüm için gayet açıklayıcı bir exception mesajıyla bunu logcat üzerinden görebiliriz.
Caused by: java.lang.IllegalStateException: Hilt Activity must be attached to an @HiltAndroidApp Application. Did you forget to specify your Application’s class name in your manifest’s <application />’s android:name attribute?
Mesajda da görebileceğiniz üzere eğer sıfırdan bir proje oluşturup, sonrasında Hilt’i ayağa kaldırmak için hızlıca application sınıfını oluşturup build almaya çalıştıysanız bunu yaşayacaksınız. Bunu yaşamamak için mesajda da belirtildiği üzere manifest’e application sınıfımızı tanımlamamız gereklidir.
<application
android:name=".MyApplication"
...
>
Tanımlama yaptıktan sonra tekrardan build alabilirsiniz. Bu sefer hata almadığınızı görebilirsiniz.
@AndroidEntryPoint
Yukarıdaki gibi application sınıfımızdaki işlemi yaptıktan sonra artık injection yapabilmekteyiz. Peki bu inject etme işlemlerini Activity, Fragment gibi Android sınıflarına yapmayı planlıyorsak ne yapmalıyız. Burada devreye @AndroidEntryPoint devreye girmektedir. Bu annotation’ın desteklediği sınıflar ise;
- Activity
- Fragment
- View
- Service
- BroadcastReceiver sınıflarıdır.
Bu annotation sayesinde Android sınıflarımıza inject yapılabilmesini deklare etmiş oluyoruz. Bu annotation’ını kullanmadığımız takdirde runtime error alırız.
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var gson: Gson
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
println("gson = ${gson}")
}
}
Yukarıda MainActivity’imizin içerisine field injection yöntemi ile bir gson objesini inject ediyoruz. Build aldığımız zaman sorunsuz bir şekilde uygulamamızın çalıştığını görecekseniz. Fakat @AndroidEntryPoint annotation’nını kaldırdığımızda runtime error alacağız.
class MainActivity : ComponentActivity() {
@Inject
lateinit var gson: Gson
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
println("gson = ${gson}")
}
}
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.hiltproject/com.example.hiltproject.MainActivity}: kotlin.UninitializedPropertyAccessException: lateinit property gson has not been initialized
Yukarıda da bahsedildiği gibi MainActivity kendisine inject edilmiş olan gson’ı tanımlayamadı ve initialize edemediği içinde (kodumuzun herhangi bir yerinde gson = Gson() yazmadığımız için) runtime error aldık.
Yine başka bir noktada eğer bir Android sınıfına @AndroidEntryPoint eklediysek onu çağıran/kullanan yani ona bağlı olan Android sınıflarına da aynı annotation’ı eklemeliyiz.
Örnek olarak eğer bir fragment’a @AndroidEntryPoint eklediyseniz onu çağıran/kullanan Activity’ninde herhangi bir inject yapmasa bile bu annotation’a sahip olması gereklidir. Peki yukarıda bahsettiğimizi uygulamazsak ne olacak derseniz:
class MainFragmentsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_fragments)
}
}
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.ui.MainFragmentsActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_app" />
</androidx.constraintlayout.widget.ConstraintLayout>
@AndroidEntryPoint
class FirstFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_first, container, false)
}
}
@AndroidEntryPoint annotation’ını kullanmayan MainFragmentsActivity’nin içinde host edilen FirstFragment isimli Fragment üzerinden senaryo oluşturursak başarılı bir şekilde build olacaktır. Fakat bu uygulamanın runtime’ı sırasında exception gerçekleştiğini görülecektir.
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.hiltproject/com.example.ui.MainFragmentsActivity}: android.view.InflateException: Binary XML file line #19 in com.example.hiltproject:layout/activity_main_fragments: Binary XML file line #19 in com.example.hiltproject:layout/activity_main_fragments: Error inflating class androidx.fragment.app.FragmentContainerView
Field Injection
Yukarıda verdiğimiz MainActivity içinde ekli olan gson property’sinin inject edilme yönteminin adıdır “field injection”.
Genel olarak Android class’larına injection işlemleri yapmak için kullandığımız yöntemdir. Bu injection yöntemini lateinit keyword’u ile kullanırız ve üstüne de @Inject annotation’ı ile Hilt’e inject etmek istediğimiz class’ı deklare etmiş oluruz. Aşağıda örnek olarak Gson kütüphanesinin ve kendi oluşturduğumuz manager class’ının field injection ile inject edilmiş halini görebilirsiniz.
@AndroidEntryPoint
class MainFragmentsActivity : AppCompatActivity() {
@Inject
lateinit var sessionCounterManager: SessionCounterManager
@Inject
lateinit var gson: Gson
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_fragments)
sessionCounterManager.increment()
println("session = ${sessionCounterManager.counter}")
}
}
class FirstFragment : Fragment() {
@Inject
lateinit var sessionCounterManager: SessionCounterManager
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_first, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sessionCounterManager.increment()
println("session = ${sessionCounterManager.counter}")
}
}
Bu örneklerimizde bahsettiğimiz kurallara uyarak SessionCounterManager class’ını inject edip bu sınıfı sanki kendimiz manuel bir şekilde(SessionCounterManager()) oluşturmuş gibi erişebilmekteyiz. Burada aklınıza SessionCounterManager class’ını nasıl inject edilebilir hale getirdik diye düşünebilirsiniz. Bunun cevabını da bir sonraki başlık olan Constructor Injection’da göreceksiniz.
Eğer @Inject annotation’ını kullanmazsak projemiz runtime’da exception fırlatacaktır. Sebebi lateinit var ile tanımlanmış property’nin initialize olmamasından kaynaklıdır.
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.hiltproject/com.example.ui.MainFragmentsActivity}: kotlin.UninitializedPropertyAccessException: lateinit property aclass has not been initialized
Aynı zamanda field injection yönteminde fark ettiyseniz herhangi bir visibility modifier eklemedik. Yani default olarak public kullanmış olduk. Sebebi Hilt’in field injection’da private desteklememesinden kaynaklıdır. Bu sebeple private eklediğimizde aşağıya eklediğim compile error mesajını göreceksiniz.
Dagger does not support injection into private fields
Constructor Injection
Bu injection yöntemi isminden de anlaşılacağı gibi constructor aracılığıyla gerçekleşir. Class’ımızın constructor’ının önüne eklediğimiz @Inject annotation’ı bize hem bu class’ın inject edilebileceğini hem de bu class’ın constructor’ının içine başka objelerin, değerlerin vesaire inject edebileceğimizi deklare etmemize olanak sağlar. Örnek üzerinden bakarsak:
@Singleton
class SessionCounterManager @Inject constructor() {
var counter = 0
private set
fun increment() {
counter++
}
}
class SessionCounterUseCase @Inject constructor(
private val sessionCounterManager: SessionCounterManager
) {
fun incrementCounter() {
sessionCounterManager.increment()
}
fun getCounter() = sessionCounterManager.counter
}
Örnekte işlevi gayet basit bir SessionCounterManager isimli bir class’ımız bulunmakta. Class’ımızın üstünde @Singleton annotation’ı kullandık. Bu konuya scope başlığı altında ileride değineceğiz ama şimdilik bu scope sayesinde bu class’ı inject eden diğer class’ların inject edilenin aynı instance’ına erişeceğini bilsek yeterli. Oluşturduğumuz constructor’ın önüne de @Inject annotation’ı ile bu class’ın inject edilebileceğini belirttik. Diğer class’ımız olan SessionCounterUseCase’de de constructor önünde @Inject annotation’ı kullanımı sayesinde, manager class’ı bu constructor içinde inject edebilmemize olanak sağladı. İstersek sonrasında da bu usecase’i başka sınıflarda da inject edebiliriz.
Özetle çok basit bir şekilde yukarıdaki örneğimize istinaden class’lar arasında injection işlemleri ele alabilmekteyiz. Inject ettiğimiz örnekler bizim yarattığımız class’lar üzerinden vermiş olsakta herhangi bir String değer, context vesaire de inject edebiliriz.
Constructor inject yönteminin bizlere kazandırdığı bir diğer güzel yanı ise viewmodel’lerde artık factory kullanımına ihtiyaç duymamamızdır. Eskiden bir viewmodel’in constructor’ında parametre/argument bulundurmak istediğimizde bunun bir için factory oluştururduk. Ama projenizde Hilt kullanıyorsanız factory kullanımına gerek olmayacaktır. Hilt özelinde, compile sürecinde Hilt, @HiltViewModel annotation’ı ile sizlerin viewmodel factory’lerinizi zaten generate edecektir. Bu sebeple kendimizin factory oluşturmasına gerek olmayacaktır.
class FactoryExampleViewModel(
private val sessionCounterManager: SessionCounterManager
) : ViewModel() {
fun callSessionCounter() {
sessionCounterManager.increment()
println("session = ${sessionCounterManager.counter}")
}
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
modelClass: Class<T>,
extras: CreationExtras
): T {
return FactoryExampleViewModel(
SessionCounterManager()
) as T
}
}
}
}
Factory kullanan örnek bir ViewModel
Aynı viewmodel’i Hilt ile oluşturmak istersek:
@HiltViewModel
class MyViewModel @Inject constructor(
private val sessionCounterManager: SessionCounterManager
) : ViewModel() {
fun callSessionCounter() {
sessionCounterManager.increment()
println("session = ${sessionCounterManager.counter}")
}
}
Not: MyViewModel örneğindeki SessionCounterManager class’ı inject edilebilir halde olarak kullanılmıştır.
Hilt Module Kullanımı
Bazı yapıları constructor inject yöntemi ile inject edilebilir hale getiremeyebiliriz. Bu durumlarda Hilt bize inject edebilme kabiliyetini module ile sağlar. Buna örnek olarak projemize eklediğimiz kütüphaneler veya projemizde kullandığımız interface’ler diyebiliriz.
@Module
@InstallIn(SingletonComponent::class)
class AppModule {
}
@InstallIn
Module’un hangi component ile ilişkilendireceğini belirttiğimiz annotation’dır. Burada SingletonComponent kullanmaktayım. Lakin duruma göre ViewModelComponent, ActivityComponent gibi başka component’ler de kullanabiliriz. Component’leri detaylı olarak ilerde ele alacağız.
Bu annotation’ı kullanmadığımızda ise compile error alacağız.
@Module
class AppModule {
}
AppModule is missing an @InstallIn annotation. If this was intentional, see https://dagger.dev/hilt/flags#disable-install-in-check for how to disable this check.
@Provides
Yazımızın başında kod örneklerinde kullandığımız Google’ın bir kütüphanesi olan Gson, yukarıda bahsettiğimiz kütüphane durumuna bir örnektir. Yukarıdaki örneklerde kullandığımız SessionCounterManager class’ını @Inject annotation’ı ile inject edilebilir hale getirmiştik. Fakat Gson bir kütüphane olduğu için bunu yapabilme imkanımız bulunmamaktadır. Bu gibi durumlarda oluşturacağımız module ve bize instance sağlayacak @Provides annotation’ı ile bu yapıları da inject edilebilir hale getirebilmemize olanak sağlar. @Provide’ın altında eklediğimiz @Singleton ise bir scope’tur. Kullanımı zorunlu değildir. Inject edilen instance’ın yönetimi ile ilgilidir diyebiliriz. Scope konusuna da ileride değineceğimiz için şimdilik bu kısa bilgilendirme bizim için yeterlidir.
@Module
@InstallIn(SingletonComponent::class)
class AppModule {
@Provides
@Singleton
fun provideGson() = Gson()
}
Yazı örneklerinde kullandığımız Gson’ı inject edebilmek için oluşturduğum module.
Ayriyeten provide edilen fonksiyon parametre de alabilmektedir. Bizler module içindeki bu fonksiyonları direkt olarak çağırmadığımızdan ve Hilt’in gerekli kodları generate etmesi ile oluşturulduğu için, bu parametrelerde Hilt tarafından ele alınır. Aşağıda parametre olarak kullandığımız bir context’i görebilirsiniz.
@Module
@InstallIn(SingletonComponent::class)
class AppModule {
@Provides
@Singleton
fun provideSimpleDateFormat(): SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US)
@Provides
@Singleton
fun provideCircularProgressDrawable(@ApplicationContext context: Context): CircularProgressDrawable {
return CircularProgressDrawable(context).apply {
strokeWidth = 5f
centerRadius = 30f
start()
}
}
}
@Binds
Kütüphanelerden bahsettiğimize göre şu ana kadar hiç bahsetmediğimiz interface’lerin inject edilebilmesine de örnek üzerinden bir anlatımla değinebiliriz.
Örneğimizde bir repositoryImpl class’ımız ve bu class’ın implemente ettiği bir repository interface’imiz bulunmakta ve ViewModel içinde de bu Interface’i Hilt ile injection yaparak instance’ını almak istiyoruz.
class ExampleRepositoryImpl @Inject constructor(): ExampleRepository {
override fun getDummyData(): String = "Hello World"
}
interface ExampleRepository {
fun getDummyData(): String
}
@HiltViewModel
class FirstScreenViewModel @Inject constructor(private val exampleRepository: ExampleRepository) :
ViewModel() {
init {
println(exampleRepository.getDummyData())
}
}
Bunu yaptığımız zaman compile sürecinde aşağıdaki hatayı almış olacağız.
[Dagger/MissingBinding] com.example.domain.repository.ExampleRepository cannot be provided without an @Provides-annotated method.
Çünkü Hilt buradaki abstraction’ı kendi tarafında basit bir tabirle eşleyememiştir. Yani biz ExampleRepositoryImpl’yi inject edilebilir halde yazdığımız halde bu class’ın abstraction’ı Hilt tarafında karşılığı olmamaktadır. Bu gibi bir durumları ele alabilmek için de @Binds annotation’ı karşımıza çıkmaktadır. O zaman bu annotation’ı nasıl kullanacağımıza aşağıdaki örnek üzerinden yorumlayalım.
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindExampleRepository(impl: ExampleRepositoryImpl): ExampleRepository
}
Yukarıda örnek olarak verdiğimiz ExampleRepositoryImpl, ExampleRepository ve FirstScreenViewModel‘de hiç bir değişiklik yapmıyoruz. Sadece yeni bir module oluşturduk. Bu sefer module için oluşturduğumuz class ve function abstract’tır. Function’ın içine de implementation class’ımızı yani ExampleRepositoryImpl parametre olarak verip, return type’ını da bu function’ın abstraction’ı olan ExampleRepository veriyoruz. Böylece Hilt, parametre ile hangi implementation’ını baz aldığını , return type ile de interface’i inject etme olanağını sağlamış oluyor.
Olurda yanlış bir interface’i module’ümüz içinde olmaması gereken bir şekilde return type olarak verirsek
interface WrongMatchingRepository {
fun getDummyData(): String
}
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindExampleRepository(impl: ExampleRepositoryImpl): WrongMatchingRepository
}
Compile sürecinde aşağıdaki hatayı alacağız.
@Binds methods’ parameter type must be assignable to the return type
Hilt Qualifier Kullanımı
Bazı durumlarda aynı return type ile birden fazla farklı injection’lar yapmak isteyebilirsiniz. Bu implementation’lar aynı module’de olacağı gibi farklı modullerde de bulunabilir. Bu durumlara, birden fazla farklı amaca hizmet eden okhttp kullanımı, aynı interface’i kullanan farklı class’lar gibi örneklendirebiliriz. Bu gibi durumları ele alabilmek için Qualifier kullanımı karşımıza çıkmaktadır.
Örnek üzerinden durumu açıklarsak:
interface DataSource {
fun getData(): String
}
class RemoteDataSourceImpl @Inject constructor(): DataSource {
override fun getData(): String = "Remote Data"
}
class LocalDataSourceImpl @Inject constructor(): DataSource {
override fun getData(): String = "Local Data"
}
@Module
@InstallIn(SingletonComponent::class)
abstract class DataSourceModule {
@Binds
@Singleton
abstract fun bindLocalDataSource(localDataSourceImpl: LocalDataSourceImpl): DataSource
@Binds
@Singleton
abstract fun bindRemoteDataSource(remoteDataSourceImpl: RemoteDataSourceImpl): DataSource
}
class ExampleRepositoryImpl @Inject constructor(
private val localDataSource: DataSource
): ExampleRepository {
override fun getDummyData(): String = "Hello World"
override fun getLocalData(): String = localDataSource.getData()
}
@HiltViewModel
class FirstScreenViewModel @Inject constructor(
private val exampleRepository: ExampleRepository
) : ViewModel() {
init {
println("data = ${exampleRepository.getLocalData()}")
}
}
Yukarıda oluşturduğumuz örnekte datasource katmanımız bulunmakta. Buna istinaden 2 adet local ve remote olmak üzere aynı interface’i implemente etmiş sınıflarımız bulunmakta. Bunları module içinde @Binds annotation’ı ile inject edilebilmesini sağlıyoruz ve sonrasında localDataSource olanı kullanmak isteyen ExampleRepositoryImpl’imiz ve onuda inject eden bir ViewModel’imiz bulunmakta. Buraya kadar olan her şey tanıdık. Fakat burada önemli olan nokta, bu iki datasource class’ının da aynı interface’i kullanması ve Hilt’in bize hangi @Binds annotation’ı ile oluşturduğumuz abstract function’ı sunması gerektiğini bilememesi. Bu dediklerimiz üzerine projemizde build almaya çalışırsak:
error: [Dagger/DuplicateBindings] com.example.domain.datasource.DataSource is bound multiple times:
Yukarıda gördüğünüz hatayı almış olacağız. Qualifier’ın bu sorunun çözümü olduğundan bahsetmiştik. Qualifier ile tekrardan kodlarımızı güncellersek:
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class RemoteDataSource
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class LocalDataSource
@Module
@InstallIn(SingletonComponent::class)
abstract class DataSourceModule {
@Binds
@Singleton
@LocalDataSource
abstract fun bindLocalDataSource(localDataSourceImpl: LocalDataSourceImpl): DataSource
@Binds
@Singleton
@RemoteDataSource
abstract fun bindRemoteDataSource(remoteDataSourceImpl: RemoteDataSourceImpl): DataSource
}
class ExampleRepositoryImpl @Inject constructor(
@LocalDataSource private val localDataSource: DataSource
): ExampleRepository {
override fun getDummyData(): String = "Hello World"
override fun getLocalData(): String = localDataSource.getData()
}
Burada yaptığımız şey @Qualifier annotation’ı ile kendi custom annotation’ınımızı oluşturmak ve sonrasında bunu module içerisinde ilgili function’larımızın üstünde aynı @Provider veya @Binds gibi belirtmek sonrasında da inject ettiğimiz yerde de bu annotation’ı kullanmaktır. Yani bize inject edeceğimiz aynı type’daki yapılar arasında spesifik olarak hangisinin nerede kullanacağımızı işaretlememizi sağlıyor.
@Named
Qualifier gibi benzer işi yapmaktadır. Bizlere aynı type’lardan hangisini kullandığımızı işaret etmemize olanak sağlar.
@Module
@InstallIn(SingletonComponent::class)
class AppModule {
@Provides
@Singleton
fun provideGson() = Gson()
@Provides
@Named("AString")
fun provideAString() = "A String"
@Provides
@Named("BString")
fun provideBString() = "B String"
}
Not: Burada @Provide ile String kullanımı, kolay debug edebilmek ve basit örnek vermek amacıyla kullanılmıştır. Normal şartlarda yukarıdaki gibi bir kullanımı Hilt ile yapmaya gerek yoktur.
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var gson: Gson
@Inject
@Named("AString")
lateinit var aString: String
@Inject
@Named("BString")
lateinit var bString: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
println("aString = $aString")
println("bString = $bString")
enableEdgeToEdge()
setContent {
HiltProjectTheme {
NavigationHost()
}
}
}
}
Gördüğünüz gibi @Named annotation’ı ile provide ettiğimiz string’leri, yine @Named annotation’ı ile inject ettiğimizde sorunsuz bir şekilde çalışacaktır.
Yazı dizimizin birinci bölümü burada son bulmaktadır. Yazı dizimizin ikinci bölümüne aşağıdaki linkten ulaşabilirsiniz.
Yazımızda kullandığımız kod örneklerine tek bir projede toplanmış halde aşağıdaki GitHub reposundan erişebilir ve verilen örnekleri kendiniz deneyebilirsiniz.
Kaynakça
https://developer.android.com/training/dependency-injection
https://dagger.dev/
https://developer.android.com/codelabs/android-hilt#0