Dagger Hilt ile Dependency Injection

Emre Karataş
KoçSistem
Published in
9 min readNov 6, 2023

Merhabalar, bu yazımda sizlere Hilt ile Dependency Injection(DI) kavramının ne olduğuna, nasıl projeye dahil edildiğine, Dagger varken Hilt’e neden gerek olduğu gibi gibi gibi konulara değineceğim :)

Haydi bir soruyla başlayalım. DI nedir?

DI bir design pattern’dir. Design Pattern ise sıkça karşılaşılan sorunlara karşılık olarak deneyim ve bilgi ile geliştirilmiş çözümlerdir. Design Pattern çözüm ise DI neyin çözümüdür? Neden DI’a ihtiyaç duyuyoruz? Bu soruları şimdilik bi kenara park edip sonra konuyu buraya bağlamak üzere devam edelim :)

Problem: Yazılımda en önemli sorun değişimdir. Değişimin önünde durmak mümkün değildir. Bu kullanıcı isteği, yeni geliştirilen özellik, yasal zorunluluklar veya markette rekabet için farklı özelliklerin geliştirilmesi gibi nedenlerle değişim belli bir noktadan sonra ihtiyaç veya zorunluluk olabiliyor. Konu değişim olunca istediğimiz tek şey kodumuzun en az etkilenecek şekilde değişmesi oluyor. Kodda ne kadar çok yere dokunursak o kadar çok yeri bozuyoruz yada o kadar çok testi yeniden gözden geçirmek hatta yeniden yazmak zorunda kalıyoruz.

Yazılımda bu süreçleri kolaylaştıracak bir çok temel prensip mevcuttur. Biraz bunlardan bahsedelim;

  • Separation of concern: Gereksinimlerin birbirinden ayrılması gibi düşünebiliriz. Bir objenin oluşturulmasıyla kullanılması farklı gereksinimlerdir. Bu gereksinimleri ayırmak geliştirme ve test süreçlerini kolaylaştırır.
  • Single Responsibility principle: Mümkün olduğunca bir objenin tek bir görevi/sorumluluğu olmalı. Böylece bir değişiklik olursa sadece o obje ve o objeyi kullanılan yerlerde değişim olsun.
  • Inversion of Control: Aslında bu prensip DI’ın başlangıcı olan prensiptir. Inversion of Control ile objelerin yaratılma yönü terse çevrilir. Örneğin bir Database objesine ihtiyacımız varsa bunu bizim oluşturmamız yerine bu objenin yaratılıp bize verilmesi gerekir.

Problem ve bu problemlere karşı oluşturulan bazı önemli prensiplere değindiğimize göre Dependency Injection konumuza tekrar dönebiliriz. DI aslında Inversion of Control prensibi temelinde kurulmuş bir design pattern’dir. DI ile aslında dependency’ler activityler tarafından değil farklı bir kaynak tarafından oluşturulup kullanılması gerektiğinde activity’e verilmesi sağlanıyor. Bu şekilde objelerin oluşturulması ve kullanılması sorumluluklarını ayırmış oluyoruz. Bu kodun rahat okunmasını, kodun tekrar kullanılmasını ve en önemlisi de kodun test edilmesini kolaylaştırıyor.

Peki DI neden zor?

Her şey iyi, güzel, hoş ama en basit haliyle bile proje DI ile birlikte karmaşık bir yapı barındırmaya başlıyor. Bu kullanılan projenin büyüklüğüne ve frameworke bağlı olarak karmaşıklık çok az da olabiliyor çok fazla da olabiliyor. DI ile birlikte projemize boilerplate kod eklemek zorunda kalıyoruz. Bazı projelerde bu boilerplate birkaç anotasyon eklemekle çözülebiliyorken bazı projelerde ekstra sınıflar oluşturmak gerekebiliyor. Tüm bunların yanında da aslında projeyi debug etmemizi ve hatta DI’a hakim değilsek projeyi anlamamızı zorlaştırabiliyor. Çünkü arka planda bir anda sihirli bir şekilde objeler kendi kendilerine yaratılıyor, hatta objenin çoğu zaman referansını gördüğümüz için objenin detayını bilemiye biliyoruz.

Hilt’e geçmeden önce Hilt’in atası, babası olan Dagger için de bir iki cümle kurmak isterim :)

Dagger DI uygulamak için çok başarılı, kendini kanıtlamış ve iyi performansı olan bir framework. DI ile ilgili ihtiyacımız olan her şeye cevap olabilecek bir framework. Ama biraz kompleks. Dagger’ı çok iyi bilen geliştiriciler bile dagger entegrasyonunu farklı kurgulayabiliyorlar. Bunun yanında çok fazla boilerplate kod yazmak gerekebiliyor. Dagger’ı özetleyecek olursak; Dagger biliyorsanız Dagger ile devam edebilirsiniz, fakat DI yapısını yeni öğreniyorsanız veya projenizde yer alan yeni geliştiriciler dagger bilmiyorsa o zaman sorunlar yaşamaya başlayabilirsiniz.

Ve tam da bu noktada bir sitem duyuldu. “DI bu kadar zor olmamalıııııııı!!!”

Evet gerçekten de farklı platformlarda DI uygulamak bu kadar kolayken Android özelinde bu kadar zor olmamalı diyen bu arkadaşın sitemini Google çalışanları neyseki duymuş olacak ki, yıllardır Google içinde kullandıkları Hilt’i dışarıya açmışlar.

Nedir Hilt?

Hilt aslında arka planda birçok işlemi yapmak için Dagger’ı kullanan ve Dagger’ın çalışması için gerekli kodu yaratan bir frameworktur. Hilt kodu yazdırmak yerine bize küçük bir setup yaptırıp işi kendisi yapıyor. Böylece Hilt’in basitliğiyle dagger’ın tüm performansını elde etmiş oluyoruz.

Hadi gelin Hilt’in temel mantığı olan Inversion of control prensibini uygulamak yani bir objenin activityden yaratılması yerine sihirli bir şekilde bu objenin yaratılması ve activitye verilmesi işi nasıl olur buna bir bakalım.

Öncelikle Hilt’i ayağa kaldırmak için bir application sınıfına ihtiyacımız var. Bu application sınıfında @HiltAndroidApp anotasyonu ile Application sınıfının Hilt ile ayağa kalkacağını belirtiyoruz. Böylece uygulama çalışırken Hilt devreye giriyor.

Örneğimiz şu şekilde olsun; bir tane activity var ve bu activity bir tane DataSource objesi kullanıyor olsun. Amacımız bu DataSource objesini activity’nin yaratması yerine, arkada Hilt tarafından yaratılıp activitye verilmesini sağlamaya çalışalım.

Hilt olmadan DataSource şöyle iken;

Hilt ile bu DataSource sınıfının inject edilebilir hale getirilebilmesi için şu hale getirmemiz yeterli olacak;

Sadece @Inject constructor yazmak DataSource sınıfının inject edilebilmesini sağlamak için yeterlidir. Peki gelelim bunu activityde nasıl kullanacağız.

Hilt olmadan şu şekilde çağırdığımızı varsayalım;

Activity içerisine Hilt ile inject edilecek bir şey kullanmak istediğimizde aşağıdaki gibi yapmamız yeterli olacaktır;

Actvitiy’lerin class tanımlamalarının üstünde @AndroidEntryPoint anotasyonu ile Hilt’in bu activity’nin herhangi bir referansı inject ettiğini bilmesini ve bu activitye bakması gerektiğini söylüyoruz. @Inject lateinit var source: DataSource tanımlamasıyla artık DataSoruce sınıfının referansını Hilt’ten almış oluyoruz. Inversion of control prensibine uygun hareket edip objenin oluşturulması sorumluluğunu Hilt’e vererek, activity’de sadece bu objenin kullanması sorumluluğunu sağlamış olduk!

Yukarıdaki resimlere geniş bir çerçeveden bakacak olursak, basit bir örnek üzerinden Hilt’i projemize entegre etmek ve kullanmak için koda eklenmesi gereken boilerplate kod aşağıdaki gibi,

Karmaşık ve büyük projelerde bu kadar az ve kolay elbetteki olmayacaktır ama Hilt’i anlamak için basit bir örnek üzerinden sade bir anlatımla devam edelim :)

Peki yukarıdaki örnekten yola çıkarak, DataSource sınıfı bir contexte ihtiyaç duyduğunda ne yapacağız? Örneğin resource’larla bir işlem yapacağız ve uygulamamıza çoklu dil özelliği ekleyeceğiz vs vs vs… Bu gibi durumlarda ne yapacağız? DataSource’a bir context eklememiz gerekiyor fakat burada bir sorun var, DataSource Hilt tarafından yaratılıyor ve yaratılma sürecinde bir etkimiz yok, o halde Hilt’le bu noktada nasıl anlaşacağız? Endişelenme! “Overlok makinası ayağınıza geldi” :) Kolaylıkları getiren Hilt burada çözümlerini de getiriyor :) Application Context’e ihtiyacımız olan yerlerde @ApplicationContext yada normal Context’e ihtiyacımız varsa @Context olarak tanımlama yapmamız durumunda Hilt bunu anlayabilir hale geliyor. DataSource sınıfımız şu şekilde olacaktır;

İşte bu harika! Activity’de tek satır kod yazmadık ve değişiklik sadece DataSource üzerinde yapıldı. Seperation of concern prensibiyle değişimi minimal tutma hedefimizi gerçekleştiriyoruz gibi görünüyor :)

Yukarıdaki örnekte activity üzerinden gittik ama gerçek dünyada genel olarak design pattern olarak Mvvm kullanılıyor. Peki Hilt ve ViewModel nasıl olacak? Endişelenmeyin, Hilt viewmodeli destekliyor. Kodumuz şu şekilde olacak;

Burada tek yapmamız gereken şey @ViewModelInject anotasyonunu viewmodelin constructor’ına eklemek. DataSource sınıfının referansını parametre olarak ekliyoruz. Hilt, DataSource’un nasıl yaratılacağını bildiği için ihtiyaç duyulduğunda otomatik olarak bu objeyi yaratıp buraya inject edebiliyor.

Peki bu viewmodeli activityde nasıl kullanacağız? Çünkü viewmodeli de bir şekilde inject edebiliyor olmamız gerekiyor. İşte bu noktada Hilt extensionlarla buna destek veriyor. Viewmodels ve ActivityViewModels adında 2 delegate mevcut. Son anlattıklarımızı görsel üzerinde görecek olursak aşağıdaki gibi basit, anlaşılır ve kod kalitesi yüksek bir çıktı elde ediyoruz :)

Not: Aşağıdaki görselde PlayViewModel yerine ListViewModel olması gerekiyordu, yazım yanlışı için düzeltme sağlamış olayım, orası kafa karıştırmasın :)

Şimdi geldik önemli bir başka konuya değinmeye :) Buraya kadar herşey çok güzel, activity, viewmodel gibi bahsettiğimiz çoğu şey Hilt’in bildiği ve desteklediği yapılar. Peki uygulamamıza Room Database entegre etmeye kalktığımızda ve DataSource sınıfında bu database’i eklemeye kalktığımızda ne olacak? Hatta örnek olarak şöyle görseli de görelim;

Görseldeki gibi DataSource sınıfına database inject ediyoruz ama Hilt’in bu database hakkında bir bilgisi yok. Roomun da constructor classlarına erişimimiz yok haliyle kodunu da değiştiremeyiz. Nasıl çıkacağız bu işin içinden? İşte bu noktada da yardımımıza “Provides” anotasyonu koşuyor. Provides anotasyonu aslında bizim Hilt’e bilmediği bir şeyi öğretme yöntemimiz olmuş oluyor. Aşağıdaki görselde göreceğiniz üzere fonksiyon ve tepesindeki @Provides anotasyonu ile dependencyleri oluşturup Hilt’e işin nasıl yapılacağını anlatmamızı sağlıyoruz. Teşekkürler Provides !! :)

Görseldeki gibi provideDatabase metodunda Provides kullanarak room database builderının nasıl üretileceğini Hilt’e öğretmiş, tarif etmiş olduk. Peki güzel bir soru soralım hemen. Bu provides fonksiyonunu nerede konumlandırmamız gerekiyor?

İşte burada araya biraz boilerplate kod giriyor.

Burada bir @Module anotasyonu ile bir class oluşturmamız gerekiyor. @InstallIn anotasyonu ile oluşturulan bu modülün Hilt’in hangi komponente bu dependencyi install edeceğini söylüyoruz. Komponentte ne? Kısaca komponentleri, tanımlanan dependencyleri hangi katmanda/levelde hazır tutulması gerektiğini Hilt’e söylememize yarıyor. ApplicationComponentte tanımlı bir objeye Activity Componentlerden erişim sağlanabilmektedir. Komponentlerle ilgili detaylara ve hiyerarşilerine şu linkten ulaşabilirsiniz.

Son görsele baktığımız zaman provideDatabase her çağrıldığında farklı referans gönderilecek ama biz tüm activity’lerde aynı database referansını çağırmak istiyoruz. İşte burada işin içine Scope giriyorlar. Scope’lar dependencylerin yaşam döngülerini belirtmemizi sağlarlar. Yukarıdaki örnekte @Provides anotasyonundan sonra @Singleton anotasyonu eklersek istediğimizi gerçekleştirmiş oluruz. @Singleton anotasyonu ile Hilt’in bu objeden bir tane yaratması ve tüm applicationda aynı database objesinin referansını kullanılması sağlanıyor. Farklı bir örnek verecek olursak, aynı activity içindeki tüm fragmentlarda aynı referans kullanılsın ama farklı activitylerde farklı referans kullanılsın istyiorsanız @ActivityRetained veya @ActivityScoped gibi scopelar kullanabilirsiniz. Daha detaylı bilgi için şu linke göz atabilirsiniz.

Gelelim test işlerine.

Hilt’te en önemli motivasyon konularımızdan biri kolayca test edilebilmesidir. İlk olarak test sınıfımızın en üstüne @HiltAndroidTest anotasyonu ekleyerek başlıyoruz. Böylece Hilt burada injection yapması gerektiğini anlıyor.

Koda bakacak olursak biraz boilerplate kod var fakat o kadar da çok değil :) Bir rule yaratıp daha sonra @Before setup fonksiyonu içerisinde inject ediyoruz. Boilerplate kod bu kadar :) Fakat görsele bakarsak bir sorun var!!! Buradaki DataSource gerçek database’i kullanıyor. Normalde test senaryolarında mock data veya roomun inmemory özelliğini kullanıyor olmamız gerekiyor. Fakat projemizdeki Hilt’in yapısını düşününce room database modülde 1 defa tanımlandı ve uygulama ayağa kalktığında gerçek database ile oluştu ve biz test veya uygulamanın bir yerinde bu database’i çağırmaya kalktığımızda gerçek database bize gelmiş olacak. Uygulama içerisinde gerçek database ve test akışlarında farklı database kullanmak istediğimizde de Hilt bize çözümler sağlıyor.

  • Nasıl?
  • Şöyle;

2. satırda gördüğünüz gibi @UninstallModules anotasyonu ile önceden oluşan modülü siliyoruz ve test için test classları içerisinde yeni modül oluşturuyoruz. Böylece testlerde kullanabileceğimiz test modülünü ve database’i oluşturmuş oluyoruz.

Test ile ilgili son bir boilerplate kodumuz daha mevcut.

Görseldeki gibi bir test runner yazmamız gerekiyor ve bir de aşağıdaki gibi gradle içinde bu tanımı yapmamız gerekiyor.

Bunlar bir defa yapılan ve copy/paste mantığında eklenebilecek boilerplate kodlar :)

Son olarak değinmek istediğim konu: Entry Points!

Entry Point; üzerinde çalıştığımız obje hilti desteklemiyorsa entry pointlerle bu sorunu ortadan kaldırabiliyoruz. Yapmamız gereken tek şey bir entry point tanımlamak. Bu aslında belirtilen resource’a hilt üzerinden nasıl ulaşabileceğimizin tanımı olmuş oluyor. Daha sonrasında Hilt bu entry pointi EntryPointAccessors üzerinden alarak bu dependency’e erişmemize olanak sağlıyor.

Öncelikle yazımı okuduğunuz için Teşekkürler!!! Umarım faydalı bir yazı olmuştur. 💪

Bir sonraki yazılarda görüşmek üzere 👋

--

--