eski-yoneticimin-bana-en-kizdigi-an-hakliydi-dagger-hilt-ve

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:

code
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 HotelRetrofit

NetworkModule.kt:

code
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):

code
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:

code
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:

code
@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:

code
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ı Interceptor ekleyin. Örneğin ValeAuthInterceptor, 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 üretip when(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.

code
// 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. :)