Hilt to biblioteka wstrzykiwania zależności na Androidzie, która zmniejsza ilość kodu szablonowego potrzebnego do ręcznego wstrzykiwania zależności w projekcie. Ręczne wstrzykiwanie zależności wymaga ręcznego tworzenia każdej klasy i jej zależności oraz używania kontenerów do ponownego wykorzystywania zależności i zarządzania nimi.
Hilt zapewnia standardowy sposób korzystania z DI w aplikacji, udostępniając kontenery dla każdej klasy Androida w projekcie i automatycznie zarządzając ich cyklami życia. Hilt jest oparty na popularnej bibliotece DI Dagger, dzięki czemu korzysta z poprawności w czasie kompilacji, wydajności w czasie działania, skalowalności i obsługi Androida Studio zapewnianych przez Daggera. Więcej informacji znajdziesz w sekcji Hilt i Dagger.
Z tego przewodnika dowiesz się, czym jest Hilt i jak działają generowane przez niego kontenery. Pokazujemy też, jak uruchomić istniejącą aplikację, aby korzystała z Hilt.
Dodawanie zależności
Najpierw dodaj wtyczkę hilt-android-gradle-plugin do pliku głównego build.gradle projektu:
Kotlin
plugins { ... id("com.google.dagger.hilt.android") version "2.57.1" apply false }
Dynamiczny
plugins { ... id 'com.google.dagger.hilt.android' version '2.57.1' apply false }
Następnie zastosuj wtyczkę Gradle i dodaj te zależności w pliku app/build.gradle:
Kotlin
plugins { id("com.google.devtools.ksp") id("com.google.dagger.hilt.android") } android { ... } dependencies { implementation("com.google.dagger:hilt-android:2.57.1") ksp("com.google.dagger:hilt-android-compiler:2.57.1") }
Dynamiczny
... plugins { id 'com.google.devtools.ksp' id 'com.google.dagger.hilt.android' } android { ... } dependencies { implementation "com.google.dagger:hilt-android:2.57.1" ksp "com.google.dagger:hilt-compiler:2.57.1" }
Aby mieć pewność, że projekt jest skonfigurowany pod kątem Javy 17, która jest wymagana przez wersje Jetpack Compose i Hilt, dodaj do pliku app/build.gradle
te informacje:
Kotlin
android { ... compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } }
Dynamiczny
android { ... compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } }
Klasa aplikacji Hilt
Wszystkie aplikacje, które korzystają z Hilt, muszą zawierać klasę Application oznaczoną adnotacją @HiltAndroidApp.
@HiltAndroidApp wywołuje generowanie kodu Hilt, w tym klasy bazowej aplikacji, która służy jako kontener zależności na poziomie aplikacji.
@HiltAndroidApp class ExampleApplication : Application() { ... }
Ten wygenerowany komponent Hilt jest powiązany z obiektem Application i zapewnia mu zależności. Jest to też komponent nadrzędny aplikacji, co oznacza, że inne komponenty mogą uzyskiwać dostęp do zależności, które on udostępnia.
Wstrzykiwanie zależności do klas Androida
Gdy Hilt jest skonfigurowany w klasie Application i dostępny jest komponent na poziomie aplikacji, Hilt może dostarczać zależności do innych klas Androida, które mają adnotację @AndroidEntryPoint:
@AndroidEntryPoint class ExampleActivity : ComponentActivity() { ... }
Hilt obsługuje obecnie te klasy Androida:
Application(przy użyciu@HiltAndroidApp)ViewModel(przy użyciu@HiltViewModel)ActivityServiceBroadcastReceiver
W Compose nie musisz dodawać adnotacji do poszczególnych funkcji kompozycyjnych. Zamiast tego dodaj do elementu głównego ComponentActivity adnotację @AndroidEntryPoint. Jest to pojedynczy punkt wejścia DI dla całej hierarchii interfejsu, dzięki czemu możesz uzyskiwać dostęp do wstrzykiwanych przez Hilt obiektów ViewModel bezpośrednio w funkcjach kompozycyjnych.
@AndroidEntryPoint generuje osobny komponent Hilt dla każdej klasy Androida w projekcie. Komponenty te mogą otrzymywać zależności z odpowiednich klas nadrzędnych zgodnie z opisem w sekcji Hierarchia komponentów.
Aby uzyskać zależności z komponentu, użyj adnotacji @Inject do wstrzykiwania przez pola:
@AndroidEntryPoint class ExampleActivity : ComponentActivity() { @Inject lateinit var analytics: AnalyticsAdapter ... }
Klasy wstrzykiwane przez Hilt mogą mieć inne klasy bazowe, które również korzystają z wstrzykiwania.
Jeśli klasy są abstrakcyjne, nie wymagają adnotacji @AndroidEntryPoint.
Więcej informacji o tym, w którym wywołaniu zwrotnym cyklu życia klasy Androida następuje wstrzykiwanie, znajdziesz w sekcji Okresy istnienia komponentów.
Definiowanie powiązań Hilt
Aby przeprowadzić wstrzykiwanie przez pola, Hilt musi wiedzieć, jak dostarczać instancje niezbędnych zależności z odpowiedniego komponentu. Powiązanie zawiera informacje niezbędne do udostępniania instancji typu jako zależności.
Jednym ze sposobów przekazywania informacji o powiązaniach do Hilta jest wstrzykiwanie przez konstruktor. Użyj adnotacji @Inject w konstruktorze klasy, aby poinformować Hilta, jak ma dostarczać instancje tej klasy:
class AnalyticsAdapter @Inject constructor( private val service: AnalyticsService ) { ... }
Parametry konstruktora klasy z adnotacjami są zależnościami tej klasy. W tym przykładzie AnalyticsAdapter ma AnalyticsService jako zależność. Dlatego Hilt musi też wiedzieć, jak udostępniać instancje interfejsu AnalyticsService.
Moduły Hilt
Czasami nie można wstrzyknąć typu za pomocą konstruktora. Może to wynikać z wielu powodów. Nie możesz na przykład wstrzykiwać interfejsu za pomocą konstruktora. Nie możesz też wstrzykiwać przez konstruktor typu, którego nie jesteś właścicielem, np. klasy z biblioteki zewnętrznej. W takich przypadkach możesz przekazać Hiltowi informacje o powiązaniach za pomocą modułów Hilta.
Moduł Hilt to klasa oznaczona adnotacją @Module. Zawiera instrukcje dla Hilta dotyczące tworzenia instancji typów, których nie można udostępnić za pomocą wstrzykiwania przez konstruktor, takich jak interfejsy lub klasy zewnętrzne. Musisz też oznaczyć każdy moduł adnotacją @InstallIn, aby poinformować Hilt, w której klasie Androida będzie używany lub instalowany dany moduł.
Zależności, które udostępniasz w modułach Hilt, są dostępne we wszystkich wygenerowanych komponentach powiązanych z klasą Androida, w której instalujesz moduł Hilt.
Wstrzykiwanie instancji interfejsów za pomocą adnotacji @Binds
Zapoznaj się z przykładem AnalyticsService. Jeśli AnalyticsService jest interfejsem, nie możesz go wstrzyknąć za pomocą konstruktora. Zamiast tego przekaż Hiltowi informacje o powiązaniu, tworząc w module Hilt funkcję abstrakcyjną z adnotacją @Binds.
Adnotacja @Binds informuje Hilt, której implementacji ma użyć, gdy musi dostarczyć instancję interfejsu.
Funkcja z adnotacjami przekazuje do Hilt te informacje:
- Zwracany typ funkcji informuje Hilt, jakiego interfejsu dostarcza instancje.
- Parametr funkcji informuje Hilt, którą implementację ma udostępnić.
interface AnalyticsService { fun analyticsMethods() } // Constructor-injected, because Hilt needs to know how to // provide instances of AnalyticsServiceImpl, too. class AnalyticsServiceImpl @Inject constructor( ... ) : AnalyticsService { ... } @Module @InstallIn(ActivityComponent::class) abstract class AnalyticsModule { @Binds abstract fun bindAnalyticsService( analyticsServiceImpl: AnalyticsServiceImpl ): AnalyticsService }
Moduł Hilt AnalyticsModule jest oznaczony adnotacją @InstallIn(ActivityComponent.class), ponieważ chcesz, aby Hilt wstrzykiwał tę zależność do ExampleActivity. Ta adnotacja oznacza, że wszystkie zależności w AnalyticsModule są dostępne we wszystkich aktywnościach aplikacji.
Wstrzykiwanie instancji za pomocą adnotacji @Provides
Interfejsy to nie jedyny przypadek, w którym nie można wstrzyknąć typu za pomocą konstruktora.
Wstrzykiwanie przez konstruktor nie jest też możliwe, jeśli nie jesteś właścicielem klasy, ponieważ pochodzi ona z biblioteki zewnętrznej (np. klasy Retrofit, OkHttpClient lub bazy danych Room) albo jeśli instancje muszą być tworzone za pomocą wzorca konstruktora.
Weźmy pod uwagę poprzedni przykład. Jeśli nie jesteś bezpośrednim właścicielem AnalyticsServiceklasy, możesz poinformować Hilta, jak dostarczać instancje tego typu, tworząc funkcję w module Hilta i dodając do niej adnotację @Provides.
Funkcja z adnotacjami dostarcza Hiltowi tych informacji:
- Zwracany typ funkcji informuje Hilt, jakiego typu instancje udostępnia funkcja.
- Parametry funkcji informują Hilt o zależnościach odpowiedniego typu.
- Ciało funkcji informuje Hilta, jak dostarczyć instancję odpowiedniego typu. Hilt wykonuje treść funkcji za każdym razem, gdy musi dostarczyć instancję tego typu.
@Module @InstallIn(ActivityComponent::class) object AnalyticsModule { @Provides fun provideAnalyticsService( // Potential dependencies of this type ): AnalyticsService { return Retrofit.Builder() .baseUrl("https://example.com") .build() .create(AnalyticsService::class.java) } }
Podawanie wielu powiązań tego samego typu
Jeśli chcesz, aby Hilt udostępniał różne implementacje tego samego typu jako zależności, musisz podać Hiltowi wiele powiązań. Możesz zdefiniować wiele powiązań tego samego typu za pomocą kwalifikatorów.
Kwalifikator to adnotacja, której używasz do identyfikowania konkretnego powiązania z typem, gdy typ ma zdefiniowanych wiele powiązań.
Przyjrzyjmy się przykładowi. Jeśli chcesz przechwytywać wywołania funkcji AnalyticsService, możesz użyć obiektu OkHttpClient z interceptor. W przypadku innych usług może być konieczne przechwytywanie połączeń w inny sposób. W takim przypadku musisz poinformować Hilta, jak udostępniać 2 różne implementacje interfejsu OkHttpClient.
Najpierw określ kwalifikatory, których będziesz używać do adnotacji metod @Binds lub @Provides:
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class AuthInterceptorOkHttpClient @Qualifier @Retention(AnnotationRetention.BINARY) annotation class OtherInterceptorOkHttpClient
Następnie Hilt musi wiedzieć, jak udostępnić instancję typu, która odpowiada każdemu kwalifikatorowi. W takim przypadku możesz użyć modułu Hilt z adnotacją @Provides.
Obie metody mają ten sam zwracany typ, ale kwalifikatory oznaczają je jako 2 różne powiązania:
@Module @InstallIn(SingletonComponent::class) object NetworkModule { @AuthInterceptorOkHttpClient @Provides fun provideAuthInterceptorOkHttpClient( authInterceptor: AuthInterceptor ): OkHttpClient { return OkHttpClient.Builder() .addInterceptor(authInterceptor) .build() } @OtherInterceptorOkHttpClient @Provides fun provideOtherInterceptorOkHttpClient( otherInterceptor: OtherInterceptor ): OkHttpClient { return OkHttpClient.Builder() .addInterceptor(otherInterceptor) .build() } }
Możesz wstrzyknąć konkretny typ, dodając do pola lub parametru odpowiedni kwalifikator:
// As a dependency of another class. @Module @InstallIn(ActivityComponent::class) object AnalyticsModule { @Provides fun provideAnalyticsService( @AuthInterceptorOkHttpClient okHttpClient: OkHttpClient ): AnalyticsService { return Retrofit.Builder() .baseUrl("https://example.com") .client(okHttpClient) .build() .create(AnalyticsService::class.java) } } // As a dependency of a constructor-injected class. class ExampleServiceImpl @Inject constructor( @AuthInterceptorOkHttpClient private val okHttpClient: OkHttpClient ) : ... // At field injection. @AndroidEntryPoint class ExampleActivity: ComponentActivity() { @AuthInterceptorOkHttpClient @Inject lateinit var okHttpClient: OkHttpClient }
Zgodnie ze sprawdzoną metodą, jeśli dodasz kwalifikator do typu, dodaj kwalifikatory do wszystkich możliwych sposobów dostarczania tej zależności. Pozostawienie podstawowej lub wspólnej implementacji bez kwalifikatora jest podatne na błędy i może spowodować wstrzyknięcie przez Hilt nieprawidłowej zależności.
Wstępnie zdefiniowane kwalifikatory w Hilt
Hilt udostępnia kilka wstępnie zdefiniowanych kwalifikatorów. Na przykład, ponieważ możesz potrzebować klasy
Context zarówno w aplikacji, jak i w aktywności, Hilt udostępnia kwalifikatory
@ApplicationContext i @ActivityContext.
Załóżmy, że klasa AnalyticsAdapter z przykładu potrzebuje kontekstu aktywności. Poniższy kod pokazuje, jak przekazać kontekst aktywności do funkcji AnalyticsAdapter:
class AnalyticsAdapter @Inject constructor( @ActivityContext private val context: Context, private val service: AnalyticsService ) { ... }
Inne predefiniowane powiązania dostępne w Hilt znajdziesz w sekcji Domyślne powiązania komponentów.
Wygenerowane komponenty klas Androida
Każda klasa Androida, w której możesz przeprowadzić wstrzykiwanie przez pola, ma powiązany komponent Hilt, do którego możesz się odwołać w adnotacji @InstallIn.
Każdy komponent Hilt jest odpowiedzialny za wstrzykiwanie powiązań do odpowiedniej klasy Androida.
W poprzednich przykładach pokazaliśmy użycie ActivityComponent w modułach Hilt.
Hilt udostępnia te komponenty:
| Komponent Hilt | Wtryskiwacz do |
|---|---|
SingletonComponent |
Application |
ActivityRetainedComponent |
Nie dotyczy |
ViewModelComponent |
ViewModel |
ActivityComponent |
Activity |
ServiceComponent |
Service |
Okresy eksploatacji komponentów
Hilt automatycznie tworzy i usuwa instancje wygenerowanych klas komponentów, śledząc cykl życia odpowiednich klas Androida.
| Wygenerowany komponent | Utworzono o | Zniszczono o |
|---|---|---|
SingletonComponent |
Application#onCreate() |
Application zniszczony |
ActivityRetainedComponent |
Activity#onCreate() |
Activity#onDestroy() |
ViewModelComponent |
Utworzono: ViewModel |
ViewModel zniszczony |
ActivityComponent |
Activity#onCreate() |
Activity#onDestroy() |
ServiceComponent |
Service#onCreate() |
Service#onDestroy() |
Zakresy komponentów
Domyślnie wszystkie powiązania w Hilt są nieobjęte zakresem. Oznacza to, że za każdym razem, gdy aplikacja zażąda powiązania, Hilt utworzy nową instancję wymaganego typu.
W tym przykładzie za każdym razem, gdy Hilt udostępnia AnalyticsAdapter jako zależność innego typu lub za pomocą wstrzykiwania przez pola (jak w przypadku ExampleActivity), Hilt udostępnia nową instancję AnalyticsAdapter.
Hilt umożliwia jednak też ograniczenie zakresu powiązania do określonego komponentu. Hilt tworzy powiązanie w zakresie tylko raz na instancję komponentu, do którego zakresu należy powiązanie, a wszystkie żądania tego powiązania korzystają z tej samej instancji.
W tabeli poniżej znajdziesz adnotacje dotyczące zakresu poszczególnych wygenerowanych komponentów:
| Zajęcia w Classroomie | Wygenerowany komponent | Zakres |
|---|---|---|
Application |
SingletonComponent |
@Singleton |
Activity |
ActivityRetainedComponent |
@ActivityRetainedScoped |
ViewModel |
ViewModelComponent |
@ViewModelScoped |
Activity |
ActivityComponent |
@ActivityScoped |
Service |
ServiceComponent |
@ServiceScoped |
W tym przykładzie, jeśli ograniczysz zakres AnalyticsAdapter do ActivityComponent za pomocą @ActivityScoped, Hilt będzie udostępniać tę samą instancję AnalyticsAdapter przez cały okres istnienia odpowiedniej aktywności:
@ActivityScoped class AnalyticsAdapter @Inject constructor( private val service: AnalyticsService ) { ... }
Załóżmy, że AnalyticsService ma stan wewnętrzny, który wymaga używania tej samej instancji za każdym razem – nie tylko w ExampleActivity, ale w dowolnym miejscu w aplikacji. W takim przypadku odpowiednie jest ograniczenie zakresu AnalyticsService do SingletonComponent. Dzięki temu za każdym razem, gdy komponent musi udostępnić instancję AnalyticsService, udostępnia tę samą instancję.
Poniższy przykład pokazuje, jak ograniczyć zakres powiązania do komponentu w module Hilt. Zakres powiązania musi być zgodny z zakresem komponentu, w którym jest ono zainstalowane. W tym przykładzie musisz zainstalować AnalyticsService w SingletonComponent zamiast w ActivityComponent:
// If AnalyticsService is an interface. @Module @InstallIn(SingletonComponent::class) abstract class AnalyticsModule { @Singleton @Binds abstract fun bindAnalyticsService( analyticsServiceImpl: AnalyticsServiceImpl ): AnalyticsService } // If you don't own AnalyticsService. @Module @InstallIn(SingletonComponent::class) object AnalyticsModule { @Singleton @Provides fun provideAnalyticsService(): AnalyticsService { return Retrofit.Builder() .baseUrl("https://example.com") .build() .create(AnalyticsService::class.java) } }
Więcej informacji o zakresach komponentów Hilt znajdziesz w artykule Zakresy w Androidzie i Hilt.
Hierarchia komponentów
Zainstalowanie modułu w komponencie umożliwia dostęp do jego powiązań jako zależności innych powiązań w tym komponencie lub w dowolnym komponencie podrzędnym w hierarchii komponentów.
Domyślne powiązania komponentów
Każdy komponent Hilt zawiera zestaw domyślnych powiązań, które Hilt może wstrzykiwać jako zależności do Twoich niestandardowych powiązań. Pamiętaj, że te powiązania odpowiadają ogólnemu typowi aktywności, a nie żadnej konkretnej podklasie. Wynika to z faktu, że Hilt używa jednej definicji komponentu aktywności do wstrzykiwania wszystkich aktywności. Każda aktywność ma inną instancję tego komponentu.
| komponent Androida, | Domyślne powiązania |
|---|---|
SingletonComponent |
Application |
ActivityRetainedComponent |
Application |
ViewModelComponent |
SavedStateHandle |
ActivityComponent |
Application, Activity |
ServiceComponent |
Application, Service |
Powiązanie kontekstu aplikacji jest też dostępne za pomocą @ApplicationContext.
Przykład:
class AnalyticsServiceImpl @Inject constructor( @ApplicationContext context: Context ) : AnalyticsService { ... } // The Application binding is available without qualifiers. class AnalyticsServiceImpl @Inject constructor( application: Application ) : AnalyticsService { ... }
Powiązanie kontekstu aktywności jest też dostępne za pomocą @ActivityContext. Przykład:
class AnalyticsAdapter @Inject constructor( @ActivityContext context: Context ) { ... } // The Activity binding is available without qualifiers. class AnalyticsAdapter @Inject constructor( activity: ComponentActivity ) { ... }
Wstrzykiwanie zależności w klasach nieobsługiwanych przez Hilt
W Compose standardowym wzorcem jest wstrzykiwanie zależności do @HiltViewModel za pomocą wstrzykiwania przez konstruktor, a następnie używanie hiltViewModel() w funkcji kompozycyjnej w celu uzyskania dostępu do ViewModel. Hilt obsługuje większość najpopularniejszych klas Androida, ale możesz napotkać nieobsługiwane klasy, w których musisz przeprowadzić wstrzykiwanie przez pola.
W takich przypadkach możesz utworzyć punkt wejścia za pomocą adnotacji @EntryPoint. Punkt wejścia to granica między kodem zarządzanym przez Hilt a kodem, który nie jest przez niego zarządzany. Jest to punkt, w którym kod po raz pierwszy wchodzi do grafu obiektów zarządzanych przez Hilt. Punkty wejścia umożliwiają Hiltowi używanie kodu, którym Hilt nie zarządza, do udostępniania zależności w grafie zależności.
Na przykład Hilt nie obsługuje bezpośrednio dostawców treści. Jeśli chcesz, aby dostawca treści używał Hilta do pobierania niektórych zależności, musisz zdefiniować interfejs z adnotacją @EntryPoint dla każdego typu powiązania, którego chcesz użyć, i uwzględnić kwalifikatory. Następnie dodaj @InstallIn, aby określić komponent, w którym ma zostać zainstalowany punkt wejścia:
class ExampleContentProvider : ContentProvider() { @EntryPoint @InstallIn(SingletonComponent::class) interface ExampleContentProviderEntryPoint { fun analyticsService(): AnalyticsService } ... }
Aby uzyskać dostęp do punktu wejścia, użyj odpowiedniej metody statycznej z klasy EntryPointAccessors. Parametr powinien być instancją komponentu lub obiektem @AndroidEntryPoint, który pełni rolę kontenera komponentu. Upewnij się, że komponent przekazywany jako parametr i statyczna metoda EntryPointAccessors są zgodne z klasą Androida w adnotacji @InstallIn w interfejsie @EntryPoint:
class ExampleContentProvider: ContentProvider() { ... override fun query(...): Cursor { val appContext = context?.applicationContext ?: throw IllegalStateException() val hiltEntryPoint = EntryPointAccessors.fromApplication(appContext, ExampleContentProviderEntryPoint::class.java) val analyticsService = hiltEntryPoint.analyticsService() ... } }
W tym przykładzie musisz użyć ApplicationContext, aby pobrać punkt wejścia, ponieważ jest on zainstalowany w SingletonComponent. Jeśli powiązanie, które chcesz pobrać, znajduje się w ActivityComponent, użyj ActivityContext.
Hilt and Dagger
Hilt to oficjalnie rekomendowana biblioteka do wstrzykiwania zależności na Androidzie. Zapewnia on standardowy, oparty na opiniach i wydajny sposób implementowania wstrzykiwania zależności w aplikacji, który jest zoptymalizowany pod kątem Jetpack Compose i architektur z jednym działaniem.
Cele Hilt:
- Aby utworzyć standardowy zestaw komponentów i zakresów, który ułatwi konfigurację, czytelność i udostępnianie kodu między aplikacjami.
- Aby zapewnić łatwy sposób udostępniania różnych powiązań różnym typom kompilacji, takim jak testowa, debugowania czy produkcyjna.
System operacyjny Android tworzy instancje wielu własnych klas frameworka, więc używanie Daggera w aplikacji na Androida wymaga napisania znacznej ilości kodu szablonowego. Hilt ogranicza liczbę powtarzalnych fragmentów kodu związanych z używaniem Daggera w aplikacji na Androida. Hilt automatycznie generuje i udostępnia te elementy:
- Komponenty do integrowania klas platformy Android z Daggerem, które w inny sposób musiałbyś utworzyć ręcznie.
- Adnotacje dotyczące zakresu do użycia z komponentami, które Hilt generuje automatycznie.
- Wstępnie zdefiniowane powiązania reprezentujące klasy Androida, takie jak
ApplicationlubActivity. - Wstępnie zdefiniowane kwalifikatory reprezentujące
@ApplicationContexti@ActivityContext.
Kod Daggera i Hilta może współistnieć w tej samej bazie kodu. W większości przypadków najlepiej jest jednak używać Hilta do zarządzania wszystkimi przypadkami użycia Daggera na Androidzie. Jeśli chcesz przenieść projekt, który korzysta z Daggera, do Hilt, zapoznaj się z przewodnikiem po migracji.
Dodatkowe materiały
Więcej informacji o Hilt znajdziesz w tych materiałach.
Przykłady
Blogi
- Wstrzykiwanie zależności w Androidzie za pomocą Hilta
- Określanie zakresu w Androidzie i Hilt
- Dodawanie komponentów do hierarchii Hilt
- Przenoszenie aplikacji Google I/O do Hilt