Eski Yöneticimin Bana En Kızdığı An (Haklıydı) : Dagger Hilt ve DI Dersi

Photo by Sadeq Mousavi on Unsplash
Yıl 2018’di. Otel ve restoran sektöründe faaliyet gösteren, Türkiye’nin en büyük şirketlerinden birinde işe girmiştim. İşe giriş hikâyem başlı başına film gibi, onu başka bir yazıya saklıyorum. Kısacası, o dönemde bu gözler çok şey gördü… (Olumlu anlamda.)
İki ana projemiz vardı:
Birincisi, restoranlar için sipariş-mutfak-kasa entegrasyonu sağlayan, devasa ölçekli bir projeydi. İkincisi ise takım liderimin başlatıp sonrasında bana devrettiği müşteri karşılama uygulamasıydı.
Lüks restoranlarda sizi karşılayan görevli sorar:
“Rezervasyonunuz var mıydı?”
“Nasıl bir masada oturmak istersiniz?”
“Daha önce gelmiş miydiniz?”
“Yemek yiyecek misiniz yoksa sadece nargile mi?”
...
İşte bu soruları soran personelin elinde rezervasyon listesi, masa listesi ve masaların doluluk durumunu gösteren bir tablet uygulaması olsun istiyorduk. Projeye Java ile başladık. Android henüz Kotlin desteğini duyurmamıştı; duyurduğunda bile “Hadi tüm projeleri Kotlin’le baştan yazalım” gibi sert bir geçiş sektörde genelde yapılmadı. Biz, Java + RxJava tabanlı, Ruby on Rails API servisleriyle konuşan bir tablet uygulaması geliştiriyorduk.
Ben, çalıştığı projelerle duygusal bağ kuran biriyim. Ara ara meraktan baktım; tablet uygulamasını bırakıp bir Android telefon uygulamasına geçmişler. Tersine mühendislik yapmadım ama Kotlin’le baştan yazdıklarına adım gibi eminim. “Masa sistemi zaten zor sığıyordu, o yüzden bilerek tablete yapmıştık. Google Play Store ekran görüntülerinden anladığım kadarıyla, masa planı sayfamızı tamamen bırakmışlar. Takım liderimin yardımıyla intersection algoritmalı çok güzel sistem yazmıştık,” diye düşünüyorum. Sebebini merak etmiyor değilim ama tercih onların. Belki tablete de uygun yapmışlardır da tablete indirince UI farklılaşıyordur. Zekice. Umarım böyle yapmışlardır.
Gelelim asıl hikâyeye…
Bir önceki iş yerimde tek Android geliştiriciydim; kendimi ne kadar geliştirebildiğimden emin değildim. Yeni iş yerimde ise takım liderim Erdem abi, hem teknik bilgisi hem de karakteriyle çok değer verdiğim biriydi. Şimdi çok sık görüşemesek de hâlâ kendisini saygıyla anarım. Erdem abi, belki okursun; her şey için tekrar teşekkürler. :)
Bir gün Erdem abi beni çağırdı, uzun uzun dependency injection anlattı: “Tekrar tekrar aynı nesneyi oluşturmak yerine tek bir yerde oluştururuz, her yerde onu kullanırız; kod temiz ve anlaşılır kalır…” Sonra Dagger’dan bahsetti. (O zamanlar Hilt yoktu; projeye Dagger eklemek daha zahmetliydi.) “Büyük projede Dagger kullandık ve gayet iyi oldu,” dedi. “Senin projeye de ekleyelim mi?” diye sordu. Ben ise hem biraz cahillikten hem çekingenlikten hem ürktüğümden hem de “küçük proje, ne gerek var” düşüncesinden dolayı yanaşmadım. Bana hafifçe kızdığını hatırlıyorum ama ne dediğini net hatırlayamıyorum; 6–7 sene oldu.
Projenin büyüyebileceğini o an hiç düşünmemiştim. Açıkçası onun da aklına gelmemişti. Ta ki… Projeyi vale hizmeti olan bir restorana sattığımızda işler değişene kadar.
Restoran, “Hem vale hem de sizin uygulama isim, soy isim, telefon sormasın; vale uygulamasının verdiği karttaki QR kodu nu okutun, bilgileri oradan alın,” dedi. Hoppalaaa! Hiç tanımadığımız bir şirketin QR sistemi ve servisleriyle entegre olmak zorunda kaldık.
Ardından projeyi bir otel restoranına sattık. Bu kez, “Zaten otel müşterisi geliyor; oda numarasını versin, isim-soyisim bilgilerini otelin veritabanından alın,” dendi. Hoppalaaa! Bu sefer de bize tamamen yabancı bir Oracle veritabanına entegre olmamız gerekti. Hatırladığım kadarıyla bu durum backend ekibimizi epey zorlamıştı; doğrudan veri alamayınca sadece sunucuda çalışacak küçük bir Golang ara katman yazmak zorunda kalmışlardı.
Her yeni bağlantıyla birlikte, kullanıcı ekranları çok değişmese de arka taraftaki bağlantı katmanı kabardıkça kabarıyordu. Üstelik bu sadece backend’de değildi; iOS ve Android tarafında da farklı servislere bağlanmak için yeni uç noktalar, yeni konfigürasyonlar derken uygulama gittikçe karmaşıklaşmaya başlamıştı.
İşte tam bu noktada “Keşke o gün Dagger’ı ekleseydik,” dedim. Neyse ki bugün elimizde Hilt var. Birden fazla base URL ve servis varken @Qualifier anotasyonlarıyla hangi istemcinin nerede kullanılacağını netleştirip bağımlılıkları projeye tertemiz enjekte edebiliyoruz…
Qualifiers.kt:
import javax.inject.Qualifier
@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class BaseApiUrl
@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class ValeApiUrl
@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class HotelApiUrl
@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class BaseClient
@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class ValeClient
@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class HotelClient
@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class BaseRetrofit
@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class ValeRetrofit
@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class HotelRetrofitNetworkModule.kt:
import com.example.network.BaseApi
import com.example.network.ValeApi
import com.example.network.HotelApi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
// Base URLs
@Provides @Singleton @BaseApiUrl
fun provideBaseApiUrl(): String = "https://base.example.com/api/"
@Provides @Singleton @ValeApiUrl
fun provideValeApiUrl(): String = "https://vale.example.com/api/"
@Provides @Singleton @HotelApiUrl
fun provideHotelApiUrl(): String = "https://hotel.example.com/api/"
// Common logging
@Provides @Singleton
fun provideLogging(): Interceptor =
HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }
// OkHttp Clients (gerekirse farklı header/interceptor eklenir)
@Provides @Singleton @BaseClient
fun provideBaseClient(logging: Interceptor): OkHttpClient =
OkHttpClient.Builder()
.addInterceptor(logging)
// .addInterceptor { chain -> /* base headers */ chain.proceed(chain.request()) }
.build()
@Provides @Singleton @ValeClient
fun provideValeClient(logging: Interceptor): OkHttpClient =
OkHttpClient.Builder()
.addInterceptor(logging)
.build()
@Provides @Singleton @HotelClient
fun provideHotelClient(logging: Interceptor): OkHttpClient =
OkHttpClient.Builder()
.addInterceptor(logging)
.build()
// Retrofit Instances
@Provides @Singleton @BaseRetrofit
fun provideBaseRetrofit(
@BaseApiUrl baseUrl: String,
@BaseClient client: OkHttpClient
): Retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.client(client)
.addConverterFactory(MoshiConverterFactory.create())
.build()
@Provides @Singleton @ValeRetrofit
fun provideValeRetrofit(
@ValeApiUrl baseUrl: String,
@ValeClient client: OkHttpClient
): Retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.client(client)
.addConverterFactory(MoshiConverterFactory.create())
.build()
@Provides @Singleton @HotelRetrofit
fun provideHotelRetrofit(
@HotelApiUrl baseUrl: String,
@HotelClient client: OkHttpClient
): Retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.client(client)
.addConverterFactory(MoshiConverterFactory.create())
.build()
// ---- API arayüzleri ----
@Provides @Singleton
fun provideBaseApi(@BaseRetrofit retrofit: Retrofit): BaseApi =
retrofit.create(BaseApi::class.java)
@Provides @Singleton
fun provideValeApi(@ValeRetrofit retrofit: Retrofit): ValeApi =
retrofit.create(ValeApi::class.java)
@Provides @Singleton
fun provideHotelApi(@HotelRetrofit retrofit: Retrofit): HotelApi =
retrofit.create(HotelApi::class.java)
}Api arayüz örnekleri (Farklı klasörlerde bulunabilir):
import retrofit2.http.GET
import retrofit2.http.Query
interface BaseApi {
@GET("tables") suspend fun getTables(): List<TableDto>
}
interface ValeApi {
@GET("tickets/by-qr") suspend fun getByQr(@Query("qr") qr: String): ValeTicketDto
}
interface HotelApi {
@GET("guest/by-room") suspend fun getGuest(@Query("roomNumber") room: String): GuestDto
}Repository katmanında hangi bağlantıyı seçeceğimizi enum ile seçiyoruz:
import com.example.network.BaseApi
import com.example.network.HotelApi
import com.example.network.ValeApi
import javax.inject.Inject
import javax.inject.Singleton
enum class Backend { BASE, VALE, HOTEL }
@Singleton
class CheckInRepository @Inject constructor(
private val baseApi: BaseApi,
private val valeApi: ValeApi,
private val hotelApi: HotelApi
) {
suspend fun fetchByQr(qr: String) = valeApi.getByQr(qr)
suspend fun fetchGuest(room: String) = hotelApi.getGuest(room)
suspend fun fetchTables() = baseApi.getTables()
// tek entry-point isteyen ekranlar için:
suspend fun fetch(mode: Backend = Backend.BASE, arg: String): Any = when (mode) {
Backend.BASE -> baseApi.getTables()
Backend.VALE -> valeApi.getByQr(arg)
Backend.HOTEL -> hotelApi.getGuest(arg)
}
}Daha da kısa bir yolla da halledebiliriz tabii ki:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModuleNamed {
@Provides @Singleton @Named("url_base") fun urlBase(): String = "https://base.example.com/api/"
@Provides @Singleton @Named("url_vale") fun urlVale(): String = "https://vale.example.com/api/"
@Provides @Singleton @Named("url_hotel") fun urlHotel(): String = "https://hotel.example.com/api/"
@Provides @Singleton fun logging(): Interceptor =
HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }
@Provides @Singleton @Named("client_base")
fun clientBase(logging: Interceptor) = OkHttpClient.Builder().addInterceptor(logging).build()
@Provides @Singleton @Named("client_vale")
fun clientVale(logging: Interceptor) = OkHttpClient.Builder().addInterceptor(logging).build()
@Provides @Singleton @Named("client_hotel")
fun clientHotel(logging: Interceptor) = OkHttpClient.Builder().addInterceptor(logging).build()
@Provides @Singleton @Named("retrofit_base")
fun retrofitBase(@Named("url_base") url: String, @Named("client_base") c: OkHttpClient): Retrofit =
Retrofit.Builder().baseUrl(url).client(c).addConverterFactory(MoshiConverterFactory.create()).build()
@Provides @Singleton @Named("retrofit_vale")
fun retrofitVale(@Named("url_vale") url: String, @Named("client_vale") c: OkHttpClient): Retrofit =
Retrofit.Builder().baseUrl(url).client(c).addConverterFactory(MoshiConverterFactory.create()).build()
@Provides @Singleton @Named("retrofit_hotel")
fun retrofitHotel(@Named("url_hotel") url: String, @Named("client_hotel") c: OkHttpClient): Retrofit = Retrofit.Builder().baseUrl(url).client(c).addConverterFactory(MoshiConverterFactory.create()).build()
@Provides @Singleton @Named("api_base")
fun apiBase(@Named("retrofit_base") r: Retrofit): BaseApi = r.create(BaseApi::class.java)
@Provides @Singleton @Named("api_vale")
fun apiVale(@Named("retrofit_vale") r: Retrofit): ValeApi = r.create(ValeApi::class.java)
@Provides @Singleton @Named("api_hotel")
fun apiHotel(@Named("retrofit_hotel") r: Retrofit): HotelApi = r.create(HotelApi::class.java)
}Kullanım:
class CheckInRepository @Inject constructor(
@Named("api_base") private val baseApi: BaseApi,
@Named("api_vale") private val valeApi: ValeApi,
@Named("api_hotel") private val hotelApi: HotelApi
) { /* ... */ }Küçük pratik notlar
Auth/headers ayrışıyorsa her client’a ayrı
Interceptorekleyin. ÖrneğinValeAuthInterceptor,HotelAuthInterceptor).Timeout/cache/pinning gibi politikalar 3 client’ta birbirinden farklı olabilir.
Aynı interface’i üç backend’de birden kullanacaksanız -endpoint sözleşmesi birebir aynıysa - 3 ayrı
Retrofit.create()ile 3 instance üretipwhen(Backend)ile seçmek en temiz yol.
Dependency Injection (DI) Sadece Bu Değildir
DI deyince çoğumuzun aklına ilk olarak “Retrofit’i Hilt’le enjekte etmek” geliyor. Ama DI’nin özü, bir sınıfın ihtiyaç duyduğu nesneleri kendi içinde yaratmaması, onların dışarıdan verilmesi, yani inject edilmesi. Böylece:
Bağımlılıklar görünür olur: Sınıfın neye ihtiyaç duyduğu constructor’da açıkça görülür.
Test edilebilirlik artar: Gerçek servis yerine fake/mock verip unit test yazarsın.
Konfigürasyon değişimi kolaylaşır: URL, timeout, cache, logger… Hepsi tek yerden değişir.
Katmanlar temiz ayrılır: UI → Use Case/Repository → Data (API/DB) zinciri gevşek bağlı hâle gelir.
Bu projede BASE/VALE/HOTEL ayrımı için DI bize şunları sağladı:
Aynı “müşteri karşılama” akışını farklı veri kaynaklarına -otel DB, vale servisi, ana API- bağlayabildik.
Backend’de endpoint değişince, tek bir modülde etkisini izole ettik.
Yeni entegrasyon geldiğinde “mevcut kodu yıkmadan” yeni implementasyon ekledik.
DI’ın Kalbi: “Inversion of Control” ve Composition Root
DI, kontrolün ters çevrilmesi dir, yani IoC. Nesneler bağımlılıklarını kendileri oluşturmaz, dışarıdan alır. Bu bağımlılıkların nasıl oluşturulduğu kararı tek bir yerde toplanır: composition root.
Composition root: Uygulamanın “inşa edildiği” yerdir. Android’de genelde Application/DI modülleri. Tüm bağlama, yani wiring burada yapılır.
Sınıflar saf iş mantığı na odaklanır; “hangi URL, hangi client, hangi logger?” gibi sorular composition root’un işidir.
Anti-Pattern: Service Locator vs. Saf DI
Service Locator: “Global bir ServiceLocator.get(Api.class) çağır, nesneyi al” yaklaşımı. Kolaydır ama gizli bağımlılık yaratır, testte zayıftır.
Saf DI (Constructor Injection): “Neyi istiyorsan constructor’da iste.” Bağımlılıkları görünür kılar, testleri kolaylaştırır.
// Kötü (Service Locator):
class CheckInController {
private val api = ServiceLocator.get<BaseApi>()
fun load() { /* api'yi kullanır */ }
}
// İyi (Constructor Injection):
class CheckInController(private val api: BaseApi) {
fun load() { /* api'yi kullanır */ }
}Sonuç
O gün “küçük proje, ne gerek var” diyerek ertelediğim DI kararı, proje büyüyüp üç ayrı backend’e dönünce gerçek yüzünü gösterdi. DI sadece Retrofit enjekte etmek değil; bağımlılıkları görünür kılmak, test etmeyi kolaylaştırmak ve değişimi ucuzlatmak demek.
Bugün aynı senaryoda, Hilt + @Qualifier ile üç client’ı temizce ayırıyor, repository’de basit bir seçimle akışı yönetiyor ve yeni entegrasyonları “sök-tak” rahatlığıyla ekleyebiliyoruz.
Keşke o gün yapsaydım; iyi ki bugün var. Dersimi aldım Erdem abi. Teşekkür ederim. Ve özür dilerim. :)
Sıkça Sorulan Sorular
▸Dagger Hilt neden küçük projelere de baştan eklenmelidir?
Proje başlangıçta küçük görünse de ilerleyen süreçte farklı servislerle entegrasyon gerekebilir. Hilt olmadan birden fazla backend'e bağlanmak kodu hızla karmaşıklaştırır. Baştan kurulan bir DI altyapısı, sonradan eklenen her yeni servisi temiz bir şekilde yönetmeyi sağlar.
▸Birden fazla base URL olduğunda Hilt nasıl kullanılır?
Hilt'te @Qualifier anotasyonları tanımlanarak her servis için ayrı OkHttpClient ve Retrofit nesneleri oluşturulur. Örneğin @BaseClient, @ValeClient ve @HotelClient gibi qualifier'lar, hangi istemcinin nerede kullanılacağını açıkça belirtir. Bu sayede bağımlılıklar projeye temiz ve karışıklık olmadan enjekte edilebilir.
▸Dependency Injection olmadan çok servisli projeyi yönetmek neden zorlaşır?
Her yeni entegrasyonda yeni uç noktalar ve konfigürasyonlar elle yönetilmek zorunda kalınır; bu da hem Android hem iOS tarafında kodun şişmesine yol açar. Ayrıca test yazmak güçleşir ve aynı nesne birden fazla yerde tekrar tekrar oluşturulur. DI bu nesneleri tek bir yerden yönetip her yere dağıtarak karmaşıklığı azaltır.



