Na stronie z podstawowymi informacjami o Dagger wyjaśniono, jak Dagger może pomóc Ci zautomatyzować wstrzykiwanie zależności w aplikacji. Dzięki Dagger nie musisz pisać uciążliwego, stałego kodu podatnego na błędy.
Podsumowanie sprawdzonych metod
- Jeśli to możliwe, używaj wstrzykiwania przez konstruktor z elementami
@Inject
, aby dodawać typy do wykresu sztyletu. Gdy nie:- Użyj elementu
@Binds
, aby wskazać Daggerowi implementację interfejsu. - Użyj
@Provides
, aby poinformować Dagger, jak udostępnić zajęcia, które nie należą do Twojego projektu.
- Użyj elementu
- Moduły należy zadeklarować w komponencie tylko raz.
- Nadaj adnotacjom zakresu nazwy zależnie od okresu, w którym są one używane. Przykłady to
@ApplicationScope
,@LoggedUserScope
i@ActivityScope
.
Dodawanie zależności
Aby używać Dagger w projekcie, dodaj te zależności do aplikacji w pliku build.gradle
. Najnowszą wersję Daggera znajdziesz w tym projekcie na GitHubie.
Kotlin
plugins { id 'kotlin-kapt' } dependencies { implementation 'com.google.dagger:dagger:2.x' kapt 'com.google.dagger:dagger-compiler:2.x' }
Java
dependencies { implementation 'com.google.dagger:dagger:2.x' annotationProcessor 'com.google.dagger:dagger-compiler:2.x' }
Sztylet w Androidzie
Przyjrzyjmy się przykładowej aplikacji na Androida przedstawiającej wykres zależności z Rys. 1.
Na Androidzie zwykle tworzysz wykres Daggera, który znajduje się w klasie aplikacji, ponieważ chcesz, aby wystąpienie wykresu było w pamięci, tak długo, jak aplikacja jest uruchomiona. Dzięki temu wykres jest dołączony do cyklu życia aplikacji. W niektórych przypadkach warto też uwzględnić na wykresie kontekst aplikacji. Aby to zrobić, wykres musi znajdować się w klasie Application
. Jedną z zalet tego podejścia jest to, że wykres jest dostępny dla innych klas platformy Androida.
Dodatkowo upraszcza to testowanie, umożliwiając użycie w nich niestandardowej klasy Application
.
Interfejs, który generuje wykres, jest oznaczony adnotacją @Component
, więc możesz go nazywać ApplicationComponent
lub ApplicationGraph
. Zwykle przechowujesz ten komponent w niestandardowej klasie Application
i wywołujesz go za każdym razem, gdy potrzebujesz wykresu aplikacji, jak pokazano w tym fragmencie kodu:
Kotlin
// Definition of the Application graph @Component interface ApplicationComponent { ... } // appComponent lives in the Application class to share its lifecycle class MyApplication: Application() { // Reference to the application graph that is used across the whole app val appComponent = DaggerApplicationComponent.create() }
Java
// Definition of the Application graph @Component public interface ApplicationComponent { } // appComponent lives in the Application class to share its lifecycle public class MyApplication extends Application { // Reference to the application graph that is used across the whole app ApplicationComponent appComponent = DaggerApplicationComponent.create(); }
Ponieważ niektóre klasy platformy Androida, takie jak działania i fragmenty, są zainicjowane przez system, Dagger nie może ich utworzyć za Ciebie. W przypadku działań każdy kod inicjowania musi zostać podany w metodzie onCreate()
.
Oznacza to, że nie możesz używać adnotacji @Inject
w konstruktorze klasy (wstrzyknięcie konstruktora), jak w poprzednich przykładach. Zamiast tego użyj wstrzykiwania pól.
Zamiast tworzyć zależności, których aktywność wymaga w metodzie onCreate()
, chcesz, by Dagger wypełniał te zależności za Ciebie. W przypadku wstrzykiwania pól musisz zamiast tego zastosować adnotację @Inject
do pól, które chcesz uzyskać z wykresu Daggera.
Kotlin
class LoginActivity: Activity() { // You want Dagger to provide an instance of LoginViewModel from the graph @Inject lateinit var loginViewModel: LoginViewModel }
Java
public class LoginActivity extends Activity { // You want Dagger to provide an instance of LoginViewModel from the graph @Inject LoginViewModel loginViewModel; }
Dla uproszczenia LoginViewModel
to nie jest obiektem ViewModel architektury Androida – to po prostu zwykła klasa, która działa jako obiekt ViewModel.
Więcej informacji o wstrzykiwaniu tych klas znajdziesz w kodzie w oficjalnej implementacji Android Blueprints Dagger w gałęzi dev-dagger.
W przypadku Daggera wstrzykiwane pola nie mogą być prywatne. Muszą one mieć co najmniej widoczność w pakiecie, tak jak w poprzednim kodzie.
Wstrzykiwanie
Dagger musi wiedzieć, że LoginActivity
ma dostęp do wykresu, aby dostarczyć potrzebne ViewModel
. Na stronie Podstawowe informacje o Dagger wykorzystano w interfejsie @Component
obiekty z wykresu, prezentując funkcje z typem zwracanym, który chcesz uzyskać z wykresu. W tym przypadku musisz poinformować Daggera o obiekcie (w tym przypadku LoginActivity
), który wymaga wstrzykiwania zależności. Służy do tego funkcja, która przyjmuje jako parametr obiekt żądający wstrzykiwania.
Kotlin
@Component interface ApplicationComponent { // This tells Dagger that LoginActivity requests injection so the graph needs to // satisfy all the dependencies of the fields that LoginActivity is requesting. fun inject(activity: LoginActivity) }
Java
@Component public interface ApplicationComponent { // This tells Dagger that LoginActivity requests injection so the graph needs to // satisfy all the dependencies of the fields that LoginActivity is injecting. void inject(LoginActivity loginActivity); }
Ta funkcja informuje Daggera, że LoginActivity
chce uzyskać dostęp do wykresu i żąda wstrzykiwania. Dagger musi spełnić wszystkie zależności, których wymaga LoginActivity
(z własnymi zależnościami LoginViewModel
).
Jeśli jest wiele klas, które żądają wstrzykiwania, musisz zadeklarować je wszystkie w komponencie dokładnie, podając ich dokładny typ. Jeśli na przykład usługa LoginActivity
i RegistrationActivity
prosi o wstrzykiwanie, uzyskasz 2 metody inject()
, a nie ogólną, która obejmuje oba przypadki. Ogólna metoda inject()
nie informuje Daggera, co należy dostarczyć. Funkcje w interfejsie mogą mieć dowolną nazwę, ale wywoływanie ich inject()
po otrzymaniu obiektu do wstrzyknięcia jako parametru jest konwencją w Daggerze.
Aby wstrzyknąć obiekt do działania, użyj metody appComponent
zdefiniowanej w klasie Application
i wywołaj metodę inject()
, przekazując wystąpienie działania, które wymaga wstrzykiwania.
Podczas korzystania z działań wstrzyknij Dagger do metody onCreate()
aktywności przed wywołaniem metody super.onCreate()
, aby uniknąć problemów z przywracaniem fragmentu. W fazie przywracania w super.onCreate()
działanie dołącza fragmenty, które mogą chcieć uzyskać dostęp do powiązań działań.
Jeśli używasz fragmentów, wstrzyknij Dagger w metodzie onAttach()
danego fragmentu. W takim przypadku można to zrobić przed wywołaniem funkcji super.onAttach()
lub po nim.
Kotlin
class LoginActivity: Activity() { // You want Dagger to provide an instance of LoginViewModel from the graph @Inject lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { // Make Dagger instantiate @Inject fields in LoginActivity (applicationContext as MyApplication).appComponent.inject(this) // Now loginViewModel is available super.onCreate(savedInstanceState) } } // @Inject tells Dagger how to create instances of LoginViewModel class LoginViewModel @Inject constructor( private val userRepository: UserRepository ) { ... }
Java
public class LoginActivity extends Activity { // You want Dagger to provide an instance of LoginViewModel from the graph @Inject LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { // Make Dagger instantiate @Inject fields in LoginActivity ((MyApplication) getApplicationContext()).appComponent.inject(this); // Now loginViewModel is available super.onCreate(savedInstanceState); } } public class LoginViewModel { private final UserRepository userRepository; // @Inject tells Dagger how to create instances of LoginViewModel @Inject public LoginViewModel(UserRepository userRepository) { this.userRepository = userRepository; } }
Pokażmy Daggerowi, jak dostarczyć pozostałe zależności, by zbudować wykres:
Kotlin
class UserRepository @Inject constructor( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) { ... } class UserLocalDataSource @Inject constructor() { ... } class UserRemoteDataSource @Inject constructor( private val loginService: LoginRetrofitService ) { ... }
Java
public class UserRepository { private final UserLocalDataSource userLocalDataSource; private final UserRemoteDataSource userRemoteDataSource; @Inject public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) { this.userLocalDataSource = userLocalDataSource; this.userRemoteDataSource = userRemoteDataSource; } } public class UserLocalDataSource { @Inject public UserLocalDataSource() {} } public class UserRemoteDataSource { private final LoginRetrofitService loginRetrofitService; @Inject public UserRemoteDataSource(LoginRetrofitService loginRetrofitService) { this.loginRetrofitService = loginRetrofitService; } }
Moduły sztyletu
W tym przykładzie używasz biblioteki sieciowej Retrofit.
UserRemoteDataSource
jest zależność od LoginRetrofitService
. Jednak sposób tworzenia instancji LoginRetrofitService
różni się od tego, co robiliśmy do tej pory. Nie jest to wystąpienie klasy, a jedynie wynik wywołania Retrofit.Builder()
i przekazywania różnych parametrów w celu skonfigurowania usługi logowania.
Oprócz adnotacji @Inject
istnieje inny sposób na poinformowanie Daggera, jak udostępnić instancję klasy – informacje zawarte w modułach Daggera. Moduł Dagger to klasa z adnotacją @Module
. Możesz tam zdefiniować zależności za pomocą adnotacji @Provides
.
Kotlin
// @Module informs Dagger that this class is a Dagger Module @Module class NetworkModule { // @Provides tell Dagger how to create instances of the type that this function // returns (i.e. LoginRetrofitService). // Function parameters are the dependencies of this type. @Provides fun provideLoginRetrofitService(): LoginRetrofitService { // Whenever Dagger needs to provide an instance of type LoginRetrofitService, // this code (the one inside the @Provides method) is run. return Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService::class.java) } }
Java
// @Module informs Dagger that this class is a Dagger Module @Module public class NetworkModule { // @Provides tell Dagger how to create instances of the type that this function // returns (i.e. LoginRetrofitService). // Function parameters are the dependencies of this type. @Provides public LoginRetrofitService provideLoginRetrofitService() { // Whenever Dagger needs to provide an instance of type LoginRetrofitService, // this code (the one inside the @Provides method) is run. return new Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService.class); } }
Moduły umożliwiają semantyczne przedstawianie informacji o udostępnianiu obiektów. Jak widać, wywołaliśmy klasę NetworkModule
, aby zgrupować logikę udostępniania obiektów związanych z siecią. Jeśli aplikacja się rozwija, możesz tu też dodać sposób podawania OkHttpClient
oraz konfigurację Gson lub Moshi.
Zależności metody @Provides
są parametrami tej metody. W przypadku poprzedniej metody obiekt LoginRetrofitService
można podać bez zależności, ponieważ nie ma ona parametrów. Jeśli jako parametr zadeklarowano OkHttpClient
, Dagger musi dostarczyć z wykresu instancję OkHttpClient
, aby spełnić zależności LoginRetrofitService
. Na przykład:
Kotlin
@Module class NetworkModule { // Hypothetical dependency on LoginRetrofitService @Provides fun provideLoginRetrofitService( okHttpClient: OkHttpClient ): LoginRetrofitService { ... } }
Java
@Module public class NetworkModule { @Provides public LoginRetrofitService provideLoginRetrofitService(OkHttpClient okHttpClient) { ... } }
Aby wykres Daggera wiedział o tym module, musisz go dodać do interfejsu @Component
w ten sposób:
Kotlin
// The "modules" attribute in the @Component annotation tells Dagger what Modules // to include when building the graph @Component(modules = [NetworkModule::class]) interface ApplicationComponent { ... }
Java
// The "modules" attribute in the @Component annotation tells Dagger what Modules // to include when building the graph @Component(modules = NetworkModule.class) public interface ApplicationComponent { ... }
Zalecanym sposobem dodawania typów do wykresu Daggera jest użycie wstrzykiwania przez konstruktor (np. za pomocą adnotacji @Inject
w konstruktorze klasy).
Czasami jest to niemożliwe i konieczne jest użycie modułów Daggera. Przykładem może być sytuacja, w której Dagger ma wykorzystać wynik obliczeń do określenia, jak utworzyć instancję obiektu. Gdy musi udostępnić instancję tego typu, Dagger uruchamia kod w metodzie @Provides
.
Oto jak obecnie wygląda wykres Sztylet w przykładzie:
Punkt wejścia na wykres to LoginActivity
. Ponieważ LoginActivity
wstrzykuje LoginViewModel
, Dagger tworzy wykres, który potrafi udostępnić wystąpienie zależności LoginViewModel
i rekursywnie. Dagger wie, jak to zrobić dzięki adnotacji @Inject
w konstruktorze klas.
W elemencie ApplicationComponent
wygenerowanym przez Daggera znajduje się metoda fabryczna, która pozwala pobrać wystąpienia wszystkich klas, które potrafi udostępnić. W tym przykładzie Dagger przekazuje połączenie do obiektu NetworkModule
zawartego w ApplicationComponent
, aby uzyskać wystąpienie LoginRetrofitService
.
Zakresy Dagger
Zakresy zostały wspomniane na stronie Podstawowe informacje o Dagger jako sposób na utworzenie unikalnej instancji danego typu w komponencie. Chodzi o określenie zakresu typu na cykl życia komponentu.
UserRepository
może się przydać w innych funkcjach aplikacji i może nie chcieć tworzyć nowego obiektu za każdym razem, gdy go potrzebujesz, dlatego możesz oznaczyć go jako unikalną instancję dla całej aplikacji. Tak samo jest w przypadku LoginRetrofitService
: utworzenie może być kosztowne, a zapewnia też ponowne wykorzystanie unikalnej instancji obiektu. Tworzenie instancji UserRemoteDataSource
nie jest zbyt kosztowne, dlatego nie trzeba ograniczać jej do cyklu życia komponentu.
@Singleton
to jedyna adnotacja dotycząca zakresu dostępna w pakiecie javax.inject
. Możesz go używać do dodawania adnotacji do ApplicationComponent
i obiektów, których chcesz używać ponownie w całej aplikacji.
Kotlin
@Singleton @Component(modules = [NetworkModule::class]) interface ApplicationComponent { fun inject(activity: LoginActivity) } @Singleton class UserRepository @Inject constructor( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) { ... } @Module class NetworkModule { // Way to scope types inside a Dagger Module @Singleton @Provides fun provideLoginRetrofitService(): LoginRetrofitService { ... } }
Java
@Singleton @Component(modules = NetworkModule.class) public interface ApplicationComponent { void inject(LoginActivity loginActivity); } @Singleton public class UserRepository { private final UserLocalDataSource userLocalDataSource; private final UserRemoteDataSource userRemoteDataSource; @Inject public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) { this.userLocalDataSource = userLocalDataSource; this.userRemoteDataSource = userRemoteDataSource; } } @Module public class NetworkModule { @Singleton @Provides public LoginRetrofitService provideLoginRetrofitService() { ... } }
Uważaj, aby nie doszło do wycieku pamięci podczas stosowania zakresów do obiektów. Dopóki komponent z ograniczonym zakresem znajduje się w pamięci, utworzony obiekt znajduje się też w pamięci. Ponieważ aplikacja ApplicationComponent
jest tworzona podczas uruchamiania aplikacji (w klasie Application
), jest niszczona po zniszczeniu aplikacji. Dlatego unikalna instancja UserRepository
zawsze pozostaje w pamięci, dopóki aplikacja nie zostanie zniszczona.
Podkomponenty sztyletu
Jeśli przepływ logowania (zarządzany przez pojedynczy LoginActivity
) składa się z wielu fragmentów, użyj tego samego wystąpienia LoginViewModel
we wszystkich fragmentach. @Singleton
nie może dodać adnotacji do LoginViewModel
, aby ponownie użyć instancji z tych powodów:
Instancja
LoginViewModel
zostanie zachowana w pamięci po zakończeniu przepływu.Chcesz mieć inną instancję
LoginViewModel
dla każdego procesu logowania. Jeśli na przykład użytkownik się wyloguje, chcesz użyć innej instancjiLoginViewModel
, a nie tej samej, do której użytkownik zalogował się po raz pierwszy.
Aby zakres LoginViewModel
obejmował cykl życia jednostki LoginActivity
, musisz utworzyć nowy komponent (nowy podgraf) dla procesu logowania oraz nowy zakres.
Utwórzmy wykres przedstawiający proces logowania.
Kotlin
@Component interface LoginComponent {}
Java
@Component public interface LoginComponent { }
Teraz LoginActivity
powinien otrzymywać wstrzykiwanie z LoginComponent
, ponieważ ma konfigurację odpowiednią do logowania. Dzięki temu nie będzie już można wstrzykiwać LoginActivity
z klasy ApplicationComponent
.
Kotlin
@Component interface LoginComponent { fun inject(activity: LoginActivity) }
Java
@Component public interface LoginComponent { void inject(LoginActivity loginActivity); }
LoginComponent
musi mieć dostęp do obiektów z ApplicationComponent
, ponieważ LoginViewModel
zależy od UserRepository
. Jeśli chcesz, by nowy komponent wykorzystywał część innego, możesz użyć podkomponentów Dagger. Nowy komponent musi być podkomponentem
zawierającym udostępnione zasoby.
Komponenty podrzędne to komponenty, które dziedziczą i rozszerzają graf obiektów obiektu nadrzędnego. W ten sposób wszystkie obiekty zawarte w komponencie nadrzędnym są dostarczane również w komponencie podrzędnym. W ten sposób obiekt z podkomponentu może zależeć od obiektu dostarczonego przez komponent nadrzędny.
Aby utworzyć wystąpienia komponentu podrzędnego, potrzebujesz wystąpienia komponentu nadrzędnego. Dlatego obiekty udostępniane przez komponent nadrzędny w komponencie podrzędnym są nadal ograniczone do komponentu nadrzędnego.
W tym przykładzie musisz zdefiniować LoginComponent
jako składnik podrzędny ApplicationComponent
. Aby to zrobić, dodaj adnotację LoginComponent
za pomocą atrybutu @Subcomponent
:
Kotlin
// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent @Subcomponent interface LoginComponent { // This tells Dagger that LoginActivity requests injection from LoginComponent // so that this subcomponent graph needs to satisfy all the dependencies of the // fields that LoginActivity is injecting fun inject(loginActivity: LoginActivity) }
Java
// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent @Subcomponent public interface LoginComponent { // This tells Dagger that LoginActivity requests injection from LoginComponent // so that this subcomponent graph needs to satisfy all the dependencies of the // fields that LoginActivity is injecting void inject(LoginActivity loginActivity); }
Musisz też zdefiniować fabrykę komponentów podrzędnych w elemencie LoginComponent
, aby usługa ApplicationComponent
wiedziała, jak tworzyć instancje komponentów LoginComponent
.
Kotlin
@Subcomponent interface LoginComponent { // Factory that is used to create instances of this subcomponent @Subcomponent.Factory interface Factory { fun create(): LoginComponent } fun inject(loginActivity: LoginActivity) }
Java
@Subcomponent public interface LoginComponent { // Factory that is used to create instances of this subcomponent @Subcomponent.Factory interface Factory { LoginComponent create(); } void inject(LoginActivity loginActivity); }
Aby poinformować Daggera, że LoginComponent
jest podkomponentem elementu ApplicationComponent
, musisz to wskazać:
Utworzenie nowego modułu Dagger (np.
SubcomponentsModule
) przekazującego klasę podkomponentu do atrybutusubcomponents
adnotacji.Kotlin
// The "subcomponents" attribute in the @Module annotation tells Dagger what // Subcomponents are children of the Component this module is included in. @Module(subcomponents = LoginComponent::class) class SubcomponentsModule {}
Java
// The "subcomponents" attribute in the @Module annotation tells Dagger what // Subcomponents are children of the Component this module is included in. @Module(subcomponents = LoginComponent.class) public class SubcomponentsModule { }
Dodawanie nowego modułu (np.
SubcomponentsModule
) do projektuApplicationComponent
:Kotlin
// Including SubcomponentsModule, tell ApplicationComponent that // LoginComponent is its subcomponent. @Singleton @Component(modules = [NetworkModule::class, SubcomponentsModule::class]) interface ApplicationComponent { }
Java
// Including SubcomponentsModule, tell ApplicationComponent that // LoginComponent is its subcomponent. @Singleton @Component(modules = {NetworkModule.class, SubcomponentsModule.class}) public interface ApplicationComponent { }
Pamiętaj, że
ApplicationComponent
nie musi już wstrzykiwać metodyLoginActivity
, ponieważ odpowiada teraz za toLoginComponent
, możesz więc usunąć metodęinject()
z metodyApplicationComponent
.Konsumenci środowiska
ApplicationComponent
muszą wiedzieć, jak utworzyć wystąpieniaLoginComponent
. Komponent nadrzędny musi dodać w swoim interfejsie metodę, która pozwala klientom tworzyć wystąpienia komponentu podrzędnego z instancji komponentu nadrzędnego:Udostępnij fabrykę, która tworzy instancje
LoginComponent
w interfejsie:Kotlin
@Singleton @Component(modules = [NetworkModule::class, SubcomponentsModule::class]) interface ApplicationComponent { // This function exposes the LoginComponent Factory out of the graph so consumers // can use it to obtain new instances of LoginComponent fun loginComponent(): LoginComponent.Factory }
Java
@Singleton @Component(modules = { NetworkModule.class, SubcomponentsModule.class} ) public interface ApplicationComponent { // This function exposes the LoginComponent Factory out of the graph so consumers // can use it to obtain new instances of LoginComponent LoginComponent.Factory loginComponent(); }
Przypisywanie zakresów do podkomponentów
Jeśli kompilujesz projekt, możesz tworzyć instancje zarówno ApplicationComponent
, jak i LoginComponent
. Zasób ApplicationComponent
jest dołączony do cyklu życia aplikacji, ponieważ chcesz używać tego samego wystąpienia wykresu, dopóki aplikacja znajduje się w pamięci.
Jaki jest cykl życia usługi LoginComponent
? Jedną z powodów, dla których potrzebujesz LoginComponent
, jest to, że musisz udostępniać to samo wystąpienie LoginViewModel
między fragmentami związanymi z logowaniem. Trzeba też jednak pamiętać, że za każdym razem,
gdy pojawia się nowy proces logowania,
LoginViewModel
ma inne instancje. LoginActivity
to właściwy czas życia obiektu LoginComponent
: dla każdej nowej aktywności potrzebujesz nowego wystąpienia LoginComponent
i fragmentów, które mogą korzystać z tego wystąpienia LoginComponent
.
Ponieważ element LoginComponent
jest powiązany z cyklem życia LoginActivity
, musisz się odnosić do komponentu w aktywności w taki sam sposób, w jaki zachowano odwołanie do elementu applicationComponent
w klasie Application
. Dzięki temu
fragmenty mogą uzyskać do niego dostęp.
Kotlin
class LoginActivity: Activity() { // Reference to the Login graph lateinit var loginComponent: LoginComponent ... }
Java
public class LoginActivity extends Activity { // Reference to the Login graph LoginComponent loginComponent; ... }
Zwróć uwagę, że do zmiennej loginComponent
nie ma adnotacji @Inject
, ponieważ Dagger nie powinien podawać tej zmiennej.
Możesz użyć ApplicationComponent
, aby uzyskać odwołanie do LoginComponent
, a następnie wstrzyknąć LoginActivity
w ten sposób:
Kotlin
class LoginActivity: Activity() { // Reference to the Login graph lateinit var loginComponent: LoginComponent // Fields that need to be injected by the login graph @Inject lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { // Creation of the login graph using the application graph loginComponent = (applicationContext as MyDaggerApplication) .appComponent.loginComponent().create() // Make Dagger instantiate @Inject fields in LoginActivity loginComponent.inject(this) // Now loginViewModel is available super.onCreate(savedInstanceState) } }
Java
public class LoginActivity extends Activity { // Reference to the Login graph LoginComponent loginComponent; // Fields that need to be injected by the login graph @Inject LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { // Creation of the login graph using the application graph loginComponent = ((MyApplication) getApplicationContext()) .appComponent.loginComponent().create(); // Make Dagger instantiate @Inject fields in LoginActivity loginComponent.inject(this); // Now loginViewModel is available super.onCreate(savedInstanceState); } }
Element LoginComponent
jest tworzony w metodzie onCreate()
aktywności i zostanie domyślnie zniszczony po zniszczeniu aktywności.
LoginComponent
musi zawsze dostarczać tę samą instancję LoginViewModel
za każdym razem, gdy jest potrzebne. Aby to zrobić, utwórz niestandardowy zakres adnotacji i dodaj do niego adnotacje LoginComponent
i LoginViewModel
. Pamiętaj, że nie możesz użyć adnotacji @Singleton
, ponieważ jest ona już używana przez komponent nadrzędny, co sprawi, że obiekt stanie się singlem (unikalnym wystąpieniem dla całej aplikacji). Musisz utworzyć inny zakres adnotacji.
W tym przypadku można nazwać ten zakres @LoginScope
, ale nie jest to dobra praktyka. Nazwa adnotacji zakresu nie powinna być jednoznacznie określona dla jej celu. Należy mu nadać nazwę w zależności od jego czasu trwania, ponieważ adnotacje mogą być wielokrotnie używane przez komponenty równorzędne, takie jak RegistrationComponent
i SettingsComponent
. Dlatego lepiej ją nazwać
@ActivityScope
, a nie @LoginScope
.
Kotlin
// Definition of a custom scope called ActivityScope @Scope @Retention(value = AnnotationRetention.RUNTIME) annotation class ActivityScope // Classes annotated with @ActivityScope are scoped to the graph and the same // instance of that type is provided every time the type is requested. @ActivityScope @Subcomponent interface LoginComponent { ... } // A unique instance of LoginViewModel is provided in Components // annotated with @ActivityScope @ActivityScope class LoginViewModel @Inject constructor( private val userRepository: UserRepository ) { ... }
Java
// Definition of a custom scope called ActivityScope @Scope @Retention(RetentionPolicy.RUNTIME) public @interface ActivityScope {} // Classes annotated with @ActivityScope are scoped to the graph and the same // instance of that type is provided every time the type is requested. @ActivityScope @Subcomponent public interface LoginComponent { ... } // A unique instance of LoginViewModel is provided in Components // annotated with @ActivityScope @ActivityScope public class LoginViewModel { private final UserRepository userRepository; @Inject public LoginViewModel(UserRepository userRepository) { this.userRepository = userRepository; } }
Jeśli masz 2 fragmenty, które wymagają parametru LoginViewModel
, oba są udostępniane w tej samej instancji. Jeśli na przykład masz LoginUsernameFragment
i LoginPasswordFragment
, muszą one być wstrzykiwane przez LoginComponent
:
Kotlin
@ActivityScope @Subcomponent interface LoginComponent { @Subcomponent.Factory interface Factory { fun create(): LoginComponent } // All LoginActivity, LoginUsernameFragment and LoginPasswordFragment // request injection from LoginComponent. The graph needs to satisfy // all the dependencies of the fields those classes are injecting fun inject(loginActivity: LoginActivity) fun inject(usernameFragment: LoginUsernameFragment) fun inject(passwordFragment: LoginPasswordFragment) }
Java
@ActivityScope @Subcomponent public interface LoginComponent { @Subcomponent.Factory interface Factory { LoginComponent create(); } // All LoginActivity, LoginUsernameFragment and LoginPasswordFragment // request injection from LoginComponent. The graph needs to satisfy // all the dependencies of the fields those classes are injecting void inject(LoginActivity loginActivity); void inject(LoginUsernameFragment loginUsernameFragment); void inject(LoginPasswordFragment loginPasswordFragment); }
Komponenty uzyskują dostęp do instancji komponentu, który działa w obiekcie LoginActivity
. Przykładowy kod dla pola LoginUserNameFragment
znajduje się w tym fragmencie kodu:
Kotlin
class LoginUsernameFragment: Fragment() { // Fields that need to be injected by the login graph @Inject lateinit var loginViewModel: LoginViewModel override fun onAttach(context: Context) { super.onAttach(context) // Obtaining the login graph from LoginActivity and instantiate // the @Inject fields with objects from the graph (activity as LoginActivity).loginComponent.inject(this) // Now you can access loginViewModel here and onCreateView too // (shared instance with the Activity and the other Fragment) } }
Java
public class LoginUsernameFragment extends Fragment { // Fields that need to be injected by the login graph @Inject LoginViewModel loginViewModel; @Override public void onAttach(Context context) { super.onAttach(context); // Obtaining the login graph from LoginActivity and instantiate // the @Inject fields with objects from the graph ((LoginActivity) getActivity()).loginComponent.inject(this); // Now you can access loginViewModel here and onCreateView too // (shared instance with the Activity and the other Fragment) } }
Ta sama zmiana w przypadku LoginPasswordFragment
:
Kotlin
class LoginPasswordFragment: Fragment() { // Fields that need to be injected by the login graph @Inject lateinit var loginViewModel: LoginViewModel override fun onAttach(context: Context) { super.onAttach(context) (activity as LoginActivity).loginComponent.inject(this) // Now you can access loginViewModel here and onCreateView too // (shared instance with the Activity and the other Fragment) } }
Java
public class LoginPasswordFragment extends Fragment { // Fields that need to be injected by the login graph @Inject LoginViewModel loginViewModel; @Override public void onAttach(Context context) { super.onAttach(context); ((LoginActivity) getActivity()).loginComponent.inject(this); // Now you can access loginViewModel here and onCreateView too // (shared instance with the Activity and the other Fragment) } }
Rysunek 3 przedstawia wygląd wykresu Daggera z nowym podkomponentem. Klasy z białą kropką (UserRepository
, LoginRetrofitService
i LoginViewModel
) to te, które mają unikalną instancję ograniczoną do odpowiednich komponentów.
Przeanalizujmy następujące elementy wykresu:
Element
NetworkModule
(i tym samymLoginRetrofitService
) jest uwzględniony w elemencieApplicationComponent
, ponieważ został on przez Ciebie podany w komponencie.UserRepository
pozostaje w tabeliApplicationComponent
, ponieważ jest ograniczony doApplicationComponent
. Jeśli projekt się rozwija, chcesz udostępniać tę samą instancję różnym funkcjom (np. rejestracji).UserRepository
jest częścią komponentuApplicationComponent
, więc jego zależności (tj.UserLocalDataSource
iUserRemoteDataSource
) muszą też znajdować się w tym komponencie, aby mogły występować wystąpienia funkcjiUserRepository
.LoginViewModel
jest zawarte wLoginComponent
, ponieważ jest wymagane tylko przez klasy wstrzykiwane przezLoginComponent
.LoginViewModel
nie został uwzględniony wApplicationComponent
, ponieważ brak zależności wApplicationComponent
wymagaLoginViewModel
.Podobnie gdyby nie zakres
UserRepository
naApplicationComponent
, Dagger automatycznie dodałby do elementuLoginComponent
elementUserRepository
i jego zależności, ponieważ obecnie jest to jedyne miejsce używane w poluUserRepository
.
Oprócz ograniczania zakresu obiektów do innego cyklu życia tworzenie podkomponentów jest dobrą praktyką, aby oddzielić poszczególne części aplikacji.
Utworzenie aplikacji w taki sposób, aby tworzyła różne podgrafy Daggera w zależności od przepływu aplikacji, pomaga stworzyć bardziej wydajną i skalowalną aplikację pod względem pamięci i czasu uruchamiania.
Sprawdzone metody tworzenia wykresu sztyletowego
Tworząc wykres Dagger dla swojej aplikacji:
Podczas tworzenia komponentu warto się zastanowić, który z nich odpowiada za jego okres eksploatacji. W tym przypadku klasa
Application
zajmuje się zajęciamiApplicationComponent
, aLoginActivity
–LoginComponent
.Zakresu używaj tylko wtedy, gdy ma to sens. Nadużywanie zakresu może mieć negatywny wpływ na wydajność aplikacji w czasie działania: obiekt znajduje się w pamięci, dopóki komponent znajduje się w pamięci, a uzyskanie obiektu o zakresie jest droższe. Gdy Dagger udostępnia obiekt, używa blokady
DoubleCheck
zamiast dostawcy typu fabrycznego.
Testowanie projektu korzystającego z Daggera
Jedną z zalet korzystania ze platform wstrzykiwania zależności, takich jak Dagger, jest łatwiejsze testowanie kodu.
Testy jednostkowe
Nie musisz używać Daggera do testów jednostkowych. Gdy testujesz klasę, która używa wstrzykiwania konstruktora, nie musisz używać Daggera do utworzenia instancji tej klasy. Możesz bezpośrednio wywoływać jego konstruktor, przekazując fałszywe lub pozorowane zależności bezpośrednio, tak jak gdyby nie były opatrzone adnotacjami.
Na przykład podczas testowania LoginViewModel
:
Kotlin
@ActivityScope class LoginViewModel @Inject constructor( private val userRepository: UserRepository ) { ... } class LoginViewModelTest { @Test fun `Happy path`() { // You don't need Dagger to create an instance of LoginViewModel // You can pass a fake or mock UserRepository val viewModel = LoginViewModel(fakeUserRepository) assertEquals(...) } }
Java
@ActivityScope public class LoginViewModel { private final UserRepository userRepository; @Inject public LoginViewModel(UserRepository userRepository) { this.userRepository = userRepository; } } public class LoginViewModelTest { @Test public void happyPath() { // You don't need Dagger to create an instance of LoginViewModel // You can pass a fake or mock UserRepository LoginViewModel viewModel = new LoginViewModel(fakeUserRepository); assertEquals(...); } }
Kompleksowe testy
W przypadku testów integracji dobrze jest utworzyć TestApplicationComponent
do testowania.
Wersja produkcyjna i testowa korzystają z innej konfiguracji komponentów.
Wymaga to bardziej wcześniejszego zaprojektowania modułów w aplikacji. Komponent testowy rozszerza komponent produkcyjny i instaluje inny zestaw modułów.
Kotlin
// TestApplicationComponent extends from ApplicationComponent to have them both // with the same interface methods. You need to include the modules of the // component here as well, and you can replace the ones you want to override. // This sample uses FakeNetworkModule instead of NetworkModule @Singleton @Component(modules = [FakeNetworkModule::class, SubcomponentsModule::class]) interface TestApplicationComponent : ApplicationComponent { }
Java
// TestApplicationComponent extends from ApplicationComponent to have them both // with the same interface methods. You need to include the modules of the // Component here as well, and you can replace the ones you want to override. // This sample uses FakeNetworkModule instead of NetworkModule @Singleton @Component(modules = {FakeNetworkModule.class, SubcomponentsModule.class}) public interface TestApplicationComponent extends ApplicationComponent { }
Element FakeNetworkModule
ma fałszywą implementację tagu NetworkModule
.
Możesz tam podać fałszywe wystąpienia lub symulacje tego, co chcesz zastąpić.
Kotlin
// In the FakeNetworkModule, pass a fake implementation of LoginRetrofitService // that you can use in your tests. @Module class FakeNetworkModule { @Provides fun provideLoginRetrofitService(): LoginRetrofitService { return FakeLoginService() } }
Java
// In the FakeNetworkModule, pass a fake implementation of LoginRetrofitService // that you can use in your tests. @Module public class FakeNetworkModule { @Provides public LoginRetrofitService provideLoginRetrofitService() { return new FakeLoginService(); } }
W ramach integracji lub testów kompleksowych należy użyć obiektu TestApplication
, który tworzy TestApplicationComponent
, a nie ApplicationComponent
.
Kotlin
// Your test application needs an instance of the test graph class MyTestApplication: MyApplication() { override val appComponent = DaggerTestApplicationComponent.create() }
Java
// Your test application needs an instance of the test graph public class MyTestApplication extends MyApplication { ApplicationComponent appComponent = DaggerTestApplicationComponent.create(); }
Następnie ta aplikacja testowa jest używana w niestandardowym obiekcie TestRunner
, którego używasz do uruchamiania testów z instrumentacją. Więcej informacji na ten temat znajdziesz w ćwiczeniach z programowania za pomocą aplikacji Dagger w aplikacjach na Androida.
Praca z modułami Dagger
Moduły sztyletu to sposób na semantyczną prezentację sposobu dostarczania obiektów. Moduły możesz dodawać do komponentów, ale także w innych modułach. Ta funkcja jest bardzo potężna, ale łatwo może być niewłaściwie wykorzystywana.
Po dodaniu modułu do komponentu lub innego modułu jest on już widoczny na wykresie Daggera. Dagger może dostarczać te obiekty do komponentu. Zanim dodasz moduł, sprawdź, czy nie jest on już częścią wykresu Dagger – sprawdź, czy jest już dodany do komponentu, lub skompiluj projekt i sprawdź, czy Dagger może znaleźć wymagane zależności.
Zgodnie z dobrą praktyką moduły należy zadeklarować w komponencie tylko raz (poza konkretnymi zaawansowanymi przypadkami użycia Daggera).
Załóżmy, że masz swój wykres skonfigurowany w ten sposób. ApplicationComponent
obejmuje Module1
i Module2
oraz Module1
– ModuleX
.
Kotlin
@Component(modules = [Module1::class, Module2::class]) interface ApplicationComponent { ... } @Module(includes = [ModuleX::class]) class Module1 { ... } @Module class Module2 { ... }
Java
@Component(modules = {Module1.class, Module2.class}) public interface ApplicationComponent { ... } @Module(includes = {ModuleX.class}) public class Module1 { ... } @Module public class Module2 { ... }
Jeśli teraz Module2
zależy od zajęć prowadzonych przez ModuleX
. Niewłaściwą praktyką jest uwzględnianie ModuleX
w elemencie Module2
, ponieważ ModuleX
występuje na wykresie dwukrotnie, tak jak w tym fragmencie kodu:
Kotlin
// Bad practice: ModuleX is declared multiple times in this Dagger graph @Component(modules = [Module1::class, Module2::class]) interface ApplicationComponent { ... } @Module(includes = [ModuleX::class]) class Module1 { ... } @Module(includes = [ModuleX::class]) class Module2 { ... }
Java
// Bad practice: ModuleX is declared multiple times in this Dagger graph. @Component(modules = {Module1.class, Module2.class}) public interface ApplicationComponent { ... } @Module(includes = ModuleX.class) public class Module1 { ... } @Module(includes = ModuleX.class) public class Module2 { ... }
Zamiast tego wykonaj jedną z tych czynności:
- refaktoryzuj moduły i wyodrębnij moduł wspólny do komponentu.
- Utwórz nowy moduł z obiektami, które są wspólne dla obu modułów, i wyodrębnij go do komponentu.
Brak refaktoryzacji w ten sposób powoduje, że wiele modułów (w tym się nawzajem) nie ma jednoznacznych pomyłek w organizacji, co utrudnia określenie, skąd się bierze każda zależność.
Dobra metoda (opcja 1): moduł X jest deklarowany na wykresie sztywnym tylko raz.
Kotlin
@Component(modules = [Module1::class, Module2::class, ModuleX::class]) interface ApplicationComponent { ... } @Module class Module1 { ... } @Module class Module2 { ... }
Java
@Component(modules = {Module1.class, Module2.class, ModuleX.class}) public interface ApplicationComponent { ... } @Module public class Module1 { ... } @Module public class Module2 { ... }
Sprawdzona metoda (opcja 2): typowe zależności od Module1
i Module2
w językach ModuleX
są wyodrębniane do nowego modułu o nazwie ModuleXCommon
, który jest zawarty w komponencie. Następnie zostaną utworzone 2 inne moduły o nazwach ModuleXWithModule1Dependencies
i ModuleXWithModule2Dependencies
z zależnościami, które występują w poszczególnych modułach. Wszystkie moduły
są deklarowane na wykresie Daggera.
Kotlin
@Component(modules = [Module1::class, Module2::class, ModuleXCommon::class]) interface ApplicationComponent { ... } @Module class ModuleXCommon { ... } @Module class ModuleXWithModule1SpecificDependencies { ... } @Module class ModuleXWithModule2SpecificDependencies { ... } @Module(includes = [ModuleXWithModule1SpecificDependencies::class]) class Module1 { ... } @Module(includes = [ModuleXWithModule2SpecificDependencies::class]) class Module2 { ... }
Java
@Component(modules = {Module1.class, Module2.class, ModuleXCommon.class}) public interface ApplicationComponent { ... } @Module public class ModuleXCommon { ... } @Module public class ModuleXWithModule1SpecificDependencies { ... } @Module public class ModuleXWithModule2SpecificDependencies { ... } @Module(includes = ModuleXWithModule1SpecificDependencies.class) public class Module1 { ... } @Module(includes = ModuleXWithModule2SpecificDependencies.class) public class Module2 { ... }
Wstrzykiwanie wspomagane
Wstrzykiwanie wspomagane to wzorzec DI wykorzystywany do konstruowania obiektu, w którym niektóre parametry mogą być dostarczane przez platformę DI, a inne muszą być przekazywane przez użytkownika w momencie tworzenia.
Na Androidzie ten wzorzec jest powszechny na ekranach szczegółów, gdzie identyfikator wyświetlanego elementu jest znany tylko w czasie działania, a nie w czasie kompilacji, gdy Dagger generuje wykres DI. Więcej informacji o wstrzykiwaniu wspomaganym metodą Dagger znajdziesz w dokumentacji Daggera.
Podsumowanie
Zapoznaj się z sekcją ze sprawdzonymi metodami. Instrukcje, jak używać Daggera w aplikacji na Androida, znajdziesz w ćwiczeniach z programowania za pomocą Daggera w aplikacji na Androida.