E-book
36.75
drukowana A5
58.23
Dagger 2. Profesjonalne aplikacje dla Androida i Javy

Bezpłatny fragment - Dagger 2. Profesjonalne aplikacje dla Androida i Javy

W pytaniach i odpowiedziach


Objętość:
184 str.
ISBN:
978-83-8245-506-9
E-book
za 36.75
drukowana A5
za 58.23

Rozdział I. Informacje ogólne

1. Czym jest Dagger 2?

Dagger 2 to w pełni statyczny framework do wstrzykiwania zależności (dependency injection) w czasie kompilacji dla języków Java i Kotlin, który współpracuje z platformami JavaSE/JavaEE oraz Android. Jest to rozwinięcie wcześniejszej wersji stworzonej przez firmę Square i obecnie utrzymywanej przez Google’a. Dagger 2 nie używa żadnego mechanizmu refleksji ani generowania kodu w czasie wykonywania (run-time), ale wykonuje całą analizę w czasie kompilacji i generuje wtedy zwykły kod źródłowy Java. Jest to obecnie najpopularniejsza biblioteka przeznaczona do wstrzykiwania zależności na platformę Android.

2. Czym jest wstrzykiwanie zależności (DI)?

W inżynierii oprogramowania wstrzykiwanie zależności (dependency injection, DI) to technika, w której obiekt otrzymuje z zewnątrz inne obiekty, które są mu niezbędne do prawidłowej pracy. Te inne obiekty nazywane są zależnościami. W typowej relacji obiekt, który przyjmuje inny obiekt, nazywany jest klientem (client), a przekazany (wstrzykiwany) obiekt nazywany jest usługą (service). Technika ta często utożsamiana jest z inną techniką programowania, zwaną odwróceniem kontroli — IoC (inversion of control), choć z technicznego punktu widzenia DI stanowi jedną z jej szczególnych (obecnie najpopularniejszych) postaci. Odwrócone kontroli pozwala nam na eliminację niedogodności tworzenia kodu, takich jak:


— Sztywność przy zmianie elementów kodu.

— Wrażliwość kodu na zmiany w części programu.

— Brak mobilności przy wydzielaniu części kodu do ponownego użycia.


Wspomniana technika nazywana jest wzorcem projektowym (design pattern). Wzorce projektowe to uniwersalne, sprawdzone w praktyce rozwiązanie często pojawiających się, powtarzalnych problemów projektowych. Pokazują powiązania i zależności pomiędzy klasami oraz obiektami i ułatwiają tworzenie, modyfikację, a także utrzymanie kodu źródłowego.


Celem wstrzykiwania zależności jako wzorca projektowego jest rozdzielenie dwóch funkcji tworzonego kodu, tj. konstrukcyjnej i funkcjonalnej (constructional and functional). Kod konstrukcyjny ma za zadanie tworzenie obiektów, natomiast funkcjonalny ich wykorzystanie celem realizacji przeznaczenia programu. Struktura dobrze zorganizowanej aplikacji rozdziela kod konstrukcyjny i funkcjonalny.

3. Czym jest zależność?

Zależność to ogólna sytuacja, która zachodzi w kodzie, gdy klasa odnosi się do innej klasy. Dla przykładu zostanie utworzona pewna klasa. Nie ma znaczenia, jaki jest jej cel i co robi:

public class NetManager{

User user = new User();

Password password = new Password ();

Host host = new Host();


public void doSomething(){

user.doSomething();

password.doSomething();

host.doSomething();

}

}

Odnośnie do powyższego przykładu można powiedzieć, że klasa NetManager zależy od klas User, Password i Host lub że te klasy są zależnościami klasy NetManager. Nie da się utworzyć i korzystać z obiektu tej klasy bez utworzenia instancji klas zależnych.

4. Na czym polega wstrzykiwanie zależności?

Pora wrócić do przykładu klasy NetManager i jednej z jej metod:

NetManager netManager = new NetManager();

netManager.doSomething();

Szybko i prosto otrzymana została instancja klasy gotowa do pracy, a następnie wywołana jedna z jej metod. Pojawia się jednak kilka problemów związanych z takim kodem. W jednym miejscu nastąpiło połączenie kodu konstrukcyjnego, który tworzy obiekt, i funkcjonalnego, który wykonuje jakieś zadanie. To akceptowalna konstrukcja, jeżeli dysponujesz kilkoma obiektami. Jednak jeżeli jest ich kilkaset, kilka tysięcy lub więcej, tracisz możliwość łatwego zarządzania tworzeniem obiektów. Inne utrudnienia związane są z koniecznością dostarczenia parametrów do konstruktorów:

NetManager netManager = new NetManager(user, password, link);

Choć są tylko trzy argumenty, mogą sprawić problemy. Możemy ich nie znać lub nie mieć do nich dostępu (np. do hasła). Ponadto możemy nie mieć dokumentacji klas. Możliwe też, że zamierzamy tworzyć obiekt warunkowo — inny obiekt podczas testowania, a inny w produkcji. Obiekt może wymagać także skomplikowanej konfiguracji przed użyciem. Na przykład:

NetManager netManager = new NetManager(user, password, link);

netManager.startService();

netManager.acceptFormat(NetManager.SOME­FORMAT);

netManager.establishFreq(100);

netManager.initDevice(null);

Rozwiązaniem tych problemów jest wydzielenie tworzenia obiektu i przeniesienie tego procesu w inne miejsce:

NetManager netManager;

netManager.doSomething();

Oczywiście taki kod wywoła wyjątek. Potrzebny jest mechanizm, który stworzy instancję klasy i przypisze (wstrzyknie) ją do referencji. Część z powyższych problemów można wyeliminować przy pomocy wzorca projektowego Factory. W przedstawionym przykładzie wyglądałoby to tak:

static class NetManagerFactory{

public NetManager buildNetManager(String user, String password, String link){

NetManager netManager =

new NetManage­r(user, password, link);

netManager.startService();

netManager.acceptFormat(NetManager.SOME_FORMAT);

netManager.establishFreq(100);

netManager.initDevice(null);

return netManager;

}

}

W takim wypadku utworzenie obiektu prezentowałoby się w ten sposób:

NetManager netManager = NetManager. buildNetManager();

Pewne problemy zostały rozwiązane, inne jednak pozostały. Nadal należy znać lub utworzyć parametry dla obiektu. Nadal samodzielnie utworzono docelowy obiekt.

5. Jaka jest zasadnicza różnica między wstrzykiwaniem zależności a wzorcem Factory?

Powodem, dla którego wstrzykiwanie zależności (DI) i wzorzec Factory są podobne, jest to, że są to dwie implementacje innego wzorca projektowego zwanego odwróceniem kontroli (Inversion of Control, IoC). Mówiąc prościej, to dwa rozwiązania tego samego problemu. Główną różnicę między wzorcem Factory a DI stanowi sposób uzyskiwania obiektu. Jeżeli chodzi o wstrzykiwanie zależności, jak sama nazwa wskazuje, obiekt jest wstrzykiwany lub przekazywany do kodu. W przypadku wzorca Factory w kodzie należy zażądać obiektu ręcznie. Factory jest wykorzystywany, gdy chce się zbudować obiekt, a nie tylko go stworzyć. Zbudowanie oznacza nie tylko utworzenie obiektu, ale także zastosowanie wobec niego pewnej logiki.


Warto zauważyć, że wzorce Factory (lub wzorce Factory Abstract, które są fabrykami zwracającymi nowe fabryki) mogą być napisane w celu dynamicznego dobierania lub łączenia z typem bądź klasą obiektu żądanego w czasie wykonywania (run-time). To sprawia, że są bardzo podobne (nawet bardziej niż DI) do wzorca Service Locator, który jest kolejną implementacją IoC.

6. Jak działa wzorzec projektowy Service Locator?

Zarówno wzorzec Service Locator (SL), jak i Dependency Injection (DI) są postaciami innego wzorca — znanego jako Object Access Pattern (OAP) — i stosują zasadę odwrócenia zależności (dependency inversion principle). Oba wzorce mają taki sam cel — zwiększenie rozdzielenia kodu konstrukcyjnego od funkcjonalnego dla lepszego testowania, skalowalności i czytelności kodu. Różnica jest taka, że DI to wzorzec statycznego dostępu do obiektów, podczas gdy SL stanowi postać dynamiczną. SL jest wykorzystywany, gdy nie znamy konkretnego dostawcy usługi przed kompilacją. Tymczasem w przypadku DI musimy znać dostawcę obiektów w czasie pisania kodu. SL lepiej pasuje do modułowej konstrukcji niektórych aplikacji, docelowy obiekt bowiem pozyskuje zależności z lokatora (kontenera, rejestru), który pomaga w ich wyszukaniu i pozyskaniu. Nie zostają one wstrzyknięte, ale zlokalizowane:

public class NetManager{

final private User user;

public NetManager(){

this.user = serviceLocator.getObject(User.class);

}

}

7. Jakie są sposoby wstrzykiwania zależności?

Istotą wstrzykiwania zależności jest pozyskanie obiektów z zewnątrz za pomocą jednej z trzech technik:


— Wstrzykiwanie przez konstruktor.

— Wstrzykiwanie przez metodę.

— Wstrzykiwanie przez pole.


Wstrzykiwanie zależności przez konstruktor polega na dostarczeniu zależnego obiektu jako argumentu konstruktora. W przypadku wstrzykiwania przez metodę zależność dostarczamy przez osobną publiczną funkcję. Z kolei wstrzyknięcie przez pole ma miejsce w przypadku dostarczenia obiektu przez zewnętrzny mechanizm do publicznego pola. W poniższym przykładzie obiekt klasy User zostaje dostarczony przez parametr konstruktora, obiekt klasy Password przez parametr metody, a obiekt klasy Host może zostać dostarczony z zewnątrz do publicznego pola:

public class NetManager{

private User user;

private Password password;

public Host host;

public NetManager(User user){

this.user = user;

}

public setPassword(Password password){

this.password = password;

}

public void doSomething(){

use.doSomething();

password.doSomething();

host.doSomething();

}

}

Zalety wstrzyknięcia przez konstruktor to prostota, jasne odzwierciedlenie koniecznych zależności w parametrach, łatwość testowania oraz możliwość finalizacji pola. W przypadku wstrzykiwania przez metodę zależności również są odzwierciedlone w parametrach. Ponadto możliwe jest wywołanie metody po konstruktorze. Wadą wstrzyknięcia przez konstruktor jest brak wskazania, że akurat te obiekty stanowią zależności oraz w jakiej kolejności należy wywołać metody, co może prowadzić do kolizji z metodami wykorzystującymi dane zależności. Jeżeli chodzi o wstrzykiwanie przez pole, to ta technika ma te same wady i zalety, co wstrzykiwanie przez metodę, choć nie wskazuje tak jednoznacznie, co jest zależnością.

8. Jak wygląda wstrzykiwanie zależności za pomocą Daggera 2 w praktyce?

Jedna z metod wstrzykiwania zależności przez pola wygląda mniej więcej tak:

@Inject

NetManager netManager;

Jak łatwo można zauważyć, brakuje tutaj słowa kluczowego „new” oraz pojawiła się anotacja @Inject. Anotacje to elementy kodu, którymi mogą zostać oznaczone klasy, metody, zmienne, parametry czy też moduły. Tak jak w przypadku tagów w dokumentacji Javy — istnieje możliwość odczytania anotacji z plików źródłowych i tym samym rozszerzenia funkcjonalności klas i obiektów. Anotacje dostarczają meta informacji w czasie kompilacji, budowania lub pracy programu.

9. Czym zajmuje się anotacja @Inject?

Jak wspomniano wcześniej, przy tworzeniu kodu problemem może być tworzenie obiektu, przekazywanie mu parametrów oraz jego konfiguracja. Poniższa anotacja robi to wszystko automatycznie:

@Inject

NetManager netManager;

Pomimo braku słowa kluczowego „new” pole netManager nie jest pustą referencją i nie wywoła wyjątku NullPointerException przy próbie użycia. Wskazuje na skonfigurowany, w pełni legalny, gotowy do użycia obiekt. Oczywiste jest, że gdzieś w kodzie ta referencja musi zostać zainicjowana przez przypisanie jej obiektu danej klasy. Sama anotacja jedynie wskazuje, do jakiego pola ma to nastąpić.

10. Jakie są zalety wykorzystania Daggera 2 do wstrzykiwania zależności?

Dagger 2 jako framework do wstrzykiwania zależności posiada wiele zalet:


— Upraszcza dostęp do współdzielonych instancji obiektu.

— Zapewnia łatwą konfigurację złożonych zależności. Istnieje niejawna kolejność, w jakiej często tworzone są obiekty. Dagger 2 przechodzi przez graf zależności i generuje kod, który jest łatwy do zrozumienia i śledzenia, a jednocześnie chroni przed pisaniem dużej ilości standardowego kodu, który normalnie należałoby pisać ręcznie, aby uzyskać referencje i przekazać je do innych obiektów jako zależności.

— Pomaga również uprościć zmiany kodu (refaktoryzację), ponieważ pozwala skupić się na tym, jakie moduły zbudować, zamiast na kolejności, w jakiej należy je tworzyć.

— Ułatwia testy jednostkowe i integracyjne, ponieważ tworzony jest graf zależności, w którym możesz łatwo wymieniać moduły.

— Kontroluje cykl życia obiektu. Nie tylko umożliwia łatwe zarządzanie instancjami, które mogą trwać cały cykl życia aplikacji, ale również pozwala na definiowanie instancji o krótszych okresach istnienia (np. związanych z sesją użytkownika, cyklem życia działania itp.).

— Umożliwia wykorzystanie mechanizmu lokalnych singletonów.

— Ma niewielki rozmiar.

— Opiera się o generator kodu, co ułatwia debugowanie.

11. Jakie są wady Daggera 2?

Dagger 2 nie uniknął oczywiście pewnych wad. Zaliczyć do nich można przede wszystkim wysoki poziom złożoności biblioteki i stromą krzywą nauki. Do tego dochodzi skromna dokumentacja, brak dobrych praktyk i zbyt dużo zaimplementowanych funkcji. Jeżeli chodzi o konkretne wady, to w dużym skrócie można powiedzieć, że po pierwsze Dagger 2 nie wstrzykuje obiektów do pól automatycznie, po drugie nie wstrzykuje obiektów w prywatne pola, a po trzecie — jeżeli Dagger 2 ma wstrzyknąć obiekt w pole, należy najpierw zdefiniować odpowiednią metodę w interfejsie oznaczonym anotacją @Component, która zwraca odpowiedni obiekt.

12. Jakie są konkurencyjne frameworki zajmujące się wstrzykiwaniem zależności?

Głównymi konkurentami dla frameworka Dagger 2 są Spring, Guice, a w mniejszym stopniu Koin. Spring to stosunkowo ciężki framework z wieloma integracjami, językiem konfiguracji XML i wstrzykiwaniem dokonywanym run-time / refleksyjnie. Aplikacje korzystające już ze Springa mogą używać struktury wstrzykiwania zależności Spring przy niewielkim nakładzie pracy. Guice to stosunkowo lekki framework z mniejszą liczbą integracji, konfiguracją instancji i wstrzykiwaniem dokonywanym run-time / refleksyjnie. Korzystając z powiązań Javy, uzyskuje się kontrolę typu podczas kompilacji i autouzupełnianie przy integracji w IDE. Z kolei Koin to prosty, potężny framework do wstrzykiwania zależności dla języka Kotlin. Napisany w czystym Kotlinie bez proxy, bez generowania kodu, bez refleksji.


Na tle wymienionych Dagger 2 to bardzo lekki framework z bardzo małą liczbą integracji, konfiguracją za pomocą interfejsu/anotacji Java oraz powiązaniami generowanymi w czasie kompilacji. Aspekt generowania kodu sprawia, że Dagger jest ogólnie bardzo wydajny, szczególnie w środowiskach mobilnych i o ograniczonych zasobach.

13. Jaka jest relacja Daggera 2 do Daggera 1?

Dagger 2 nie stanowi prostego rozwinięcia swojego poprzednika. W ramach Daggera 2 dokonano wielu zmian, które pod wieloma względami uczyniły tę bibliotekę niekompatybilną z wcześniejszą wersją i nader odmienną, jeżeli chodzi o założenia konstrukcyjne. Chociaż projekty Dagger 1 i Dagger 2 są do siebie podobne pod wieloma względami, jeden nie zastępuje drugiego.


Wszystkie metody wstrzykiwania obsługiwane przez framework Dagger 1 (przez pole i konstruktor) nadal są obsługiwane przez Daggera 2, ale Dagger 2 obsługuje również wstrzykiwanie przez metodę. Dagger 2 nie obsługuje wstrzykiwania statycznego. Jednak podstawową różnicę między platformami programistycznymi Dagger 1 i Dagger 2 stanowi mechanizm, za pomocą którego budowany jest pełny graf zależności (o tym, czym jest graf, napisano w innym rozdziale). W Daggerze 1 graf zostaje utworzony przez mechanizm refleksji w ObjectGraph, a w Daggerze 2 jest to zrobione przez anotację @Component, czyli typ zdefiniowany przez użytkownika, którego implementacja jest generowana w czasie kompilacji.


Dagger 2 znany jest również jako Google Dagger z racji przejęcia prowadzenia projektu przez firmę Google. Wraz z nową wersją wprowadzono konkretne zmiany:


— Wszystkie analizy kodu mają miejsce podczas kompilacji.

— Wprowadzono anotacje zakresowe.

— Usunięto refleksję.

— Wprowadzono wstrzykiwanie przez metodę.

— Usunięto tworzenie grafu run-time.

— Istnieje mniej konfiguracji przy tworzeniu modułów.

— Kod jest szybszy i łatwiejszy w użyciu.


Dagger 1 oficjalnie uznano za przestarzały (15 września 2016 r.) i nie jest już aktywnie rozwijany. Zaleca się migrację do Daggera 2.

14. Na jakiej licencji działa Dagger 2?

Dagger 2 działa na licencji Apache License w wersji 2.0. W dużym skrócie pozwala ona na używanie, modyfikowanie i redystrybucję programu w postaci źródłowej lub binarnej bez obowiązku udostępnienia kodu źródłowego. Oznacza to, że kod na tej licencji można włączyć do zamkniętych programów — pod warunkiem zachowania zgodności z warunkami tej licencji.

15. Czy można liczyć na utrzymanie Daggera 2 i jego dalszy rozwój?

Dagger 2 jest aktywnie utrzymywany przez zespół Google’a, ten sam, który pracuje nad biblioteką Guava. Dlatego też można spodziewać się długiego życia tej biblioteki. Podczas konferencji Android Dev Summit w 2019 r. przedstawiciele Google’a zaprezentowali opinię na temat budowania aplikacji na platformę Android, zgodnie z którą wykorzystanie Daggera 2 dla średnich i dużych projektów stanowi rekomendowany sposób tworzenia kodu na Androida. Dla małych projektów rekomenduje się platformę Dagger 2 lub wzorzec Service Locator. Intensywnie rozwijana jest także część bibliotek przeznaczonych wyłącznie dla Androida, zwłaszcza wraz z inauguracją pakietu Hilt.

16. Gdzie można znaleźć kod źródłowy Daggera 2?

Dagger 2 posiada ogólnie dostępny kod na GitHubie pod tym adresem:


https://github.com/google/dagger

17. Gdzie można znaleźć dokumentację Daggera 2?

Tutaj dostępny jest mały tutorial i FAQ po angielsku:


https://dagger.dev/users-guide


Pełna dokumentacja dla wersji 2.32 znajduje się tutaj:


https://dagger.dev/api/2.32/overview-summary.html

Rozdział II. Instalacja

1. Jak zainstalować Daggera 2 w Javie dla Androida?

W celu instalacji wykorzystaj środowisko Android Studio. Na początku należy utworzyć nowy projekt o nazwie Dagger Android:


File -> New -> New Project


Następnie wynależy wybrać templatkę o nazwie Empty Activity. Przyda się startowa klasa Activity, ale nie musi mieć żadnych szczególnych elementów graficznych. Po wygenerowaniu projektu powinno się otrzymać klasę MainActivity, a w niej metodę onCreate. W projekcie w sekcji Gradle Scripts znajduje się plik build.gradle. Wybierzemy ten z modułu app i edytuj go. W sekcji:

dependencies{…}

Należy dodać następujące wpisy:

implementation „com. google. dagger: dagger-android:2.32”

implementation „com. google. dagger: dagger-android-support:2.32”

anotationProcessor „com. google. dagger: dagger-android-processor:2.32”

anotationProcessor „com. google. dagger: dagger-compiler:2.32”

Następnie należy wymusić pobranie bibliotek:


File -> Sync Project with Gradle Files


W oknie (konsoli) Build powinna pojawić się następująca informacja:

BUILD SUCCESSFUL in Xm XXs

2. Jak zainstalować Daggera 2 w Kotlinie dla Androida?

Konfiguracja bibliotek dla języka Kotlin różni się trochę od tej dla Javy. Przede wszystkim w pliku build.gradle należy dodać plugin dla Kotlina:

apply plugin: „kotlin-kapt”

Plugin ten musi zostać dodany w odpowiednim miejscu:

apply plugin: 'com.android. application”

apply plugin: „kotlin-android”

apply plugin: „kotlin-kapt”

apply plugin: „kotlin-android-extensions”

W sekcji dependencies trzeba dodać następujące wpisy:

implementation „com. google. dagger: dagger-android:2.32”

implementation „com. google. dagger: dagger-android-support: 2.32”

kapt „com. google. dagger: dagger-android-processor: 2.32”

kapt „com. google. dagger: dagger-compiler: 2.32”

compileOnly „com. google. dagger: dagger:2.31.2”

compileOnly 'javax.anotation: jsr250-api:1.0”

implementation 'javax.inject:javax.inject:1”

compileOnly 'javax.anotation:javax.anotation-api:1.3.2”

3. Jak sprawdzić w środowisku Android Studio, czy instalacja Daggera 2 jest poprawna?

Aby przetestować, czy Dagger 2 został poprawnie zainstalowany, należy stworzyć mały projekt. W tym momencie nie jest istotne, co znaczą jego poszczególne elementy. Zacząć powinniśmy od utworzenia klasy, którą będziemy wstrzykiwać:

public class NetManager{

public void sayHello() {

System.out.println(„NetComponent says Hello”);

}

}

Następnie należy utworzyć zwykły interfejs o dowolnej nazwie, na przykład NetComponent:

import dagger.Component;


@Component(modules={NetModule.class})

public interface NetComponent{

NetManager provideNetManager();

}

Jak widać, nad nazwą interfejsu dodana została anotacja @Component wraz z parametrem wskazującym na klasę NetModule, którą zaraz utworzymy. Nie będzie możliwe jej użycie bez zaimportowania pakietu „dagger.Component”.


Stworzymy teraz klasę o nazwie NetModule i oznaczymy ją anotacją @Module. Następnie umieścimy w niej metodę oznaczoną anotacją @Provides, która zwraca utworzony obiekt NetManager:

import dagger.Module;

import dagger.Provides;


@Module

public class NetModule{

@Provides

NetManager providesNetManager(){

return new NetManager();

}

}

W klasie NetComponent w metodzie inject() zmienimy parametr na startową klasę MainActivity:

public void inject(MainActivity mainActivity);

Na koniec w MainActivity w metodzie onCreate pod:

super. onCreate(savedInstanceState);

należy dodać następujący kod:

NetComponent netComponent DaggerNetComponent.create();

netComponent.inject(this);

netManager.sayHello();

Natomiast nad metodą onCreate() należy dodać pole netManager:

@Inject

NetManager netManager;

Jak łatwo można zauważyć, pojawiła się klasa DaggerNetComponent. Jej znaczenie zostanie wyjaśnione później. W tym momencie należy tylko wiedzieć, że ta klasa jest generowana automatycznie w czasie komplikacji. Niestety, w czasie pisania kodu Android Studio nie rozpoznaje klasy, dlatego na początku edytor będzie sygnalizował błąd nieznanej klasy. Żeby ją wygenerować, należy skompilować kod. W tym celu z górnego menu wybieramy:


Build -> Clean


i następnie:


Build -> Rebuild


Po tym kod powinien się poprawnie skompilować. Rezultatem kompilacji będzie utworzenie pliku DaggerNetComponent. Zostanie on umieszczony w katalogu projektu:

app\build\generated\sources\debug\out\com\dagger

Pamiętać należy jednak, że wygenerowanie klasy nie dodaje automatycznie ścieżki import dla klasy w miejscu jej użycia.

4. Jak zainstalować Daggera 2 poza środowiskiem Android?

W celu utworzenia projektu Dagger 2 posłużymy się narzędziami IntelliJ i Gradle. Należy założyć, że są one poprawnie zainstalowane i skonfigurowane. W IntelliJ utworzymy nowy projekt Java o nazwie DaggerSE.


File -> New -> Project -> Java


W utworzonym katalogu o nazwie DaggerSE znajduje się plik build.gradle. Edytuj go. W sekcji:

dependencies{…}

należy dodać następujące wpisy:

implementation „com. google. dagger: dagger:2.32”

anotationProcessor „com. google. dagger: dagger-compiler:2.32”

W chwili pisania najnowszą wersją była ta oznaczona numerem 2.32. Aby sprawdzić, jaka jest najnowsza wersja w momencie czytania tej książki, można wejść na stronę:


https://github.com/google/dagger


i poszukać ikony „maven central”. Przy niej będzie numer najnowszej wersji.


https://mvnrepository.com/artifact/com.google.dagger/dagger


Niestety, IntelliJ nie oferuje bezproblemowego wsparcia dla Daggera 2. Wynika to głównie z budowy tego frameworka, który wykonuje wiele czynności w czasie kompilacji, tworząc też własne pliki klas. Dlatego też potrzebna jest dodatkowa konfiguracja środowiska programistycznego. Najpierw musimy włączyć przetwarzanie anotacji. W tym celu wybieramy:

File -> Settings


Następnie wybieramy:


Build, Execution Deployment -> Compilers -> Anotations Processors

i zaznaczamy:


Enable Anotations Processing


Na końcu należy zrestartować IntelliJ:


File -> Invalidate Caches/Restart


Należy pamiętać o synchronizacji narzędzia Gradle, które powinno pobrać wszystkie konieczne pliki. W ten sposób dokonane zostało przygotowanie do pracy.

5. Jak przetestować w środowisku IntelliJ, czy instalacja jest poprawna?

Żeby przyspieszyć test, wykorzystamy klasy utworzone wcześniej, tj. klasy NetManager, NetModule, NetComponent. Pliki z tymi klasami należy skopiować do niniejszego projektu. W klasie NetComponent należy zmienić tylko parametr na startową klasę Dagger:

public void inject(Dagger dagger);

Stworzymy też klasę, w której umieścimy referencję do NetManagera. Należy opatrzyć ją anotacją @Inject. Następnie wygenerujemy obiekt DaggerNetComponent, z którego wywołamy metodę sayHello():

import javax.inject.Inject;


public class Dagger{

@Inject

NetManager netManager;

public Dagger (){

NetComponent netComponent=

DaggerNetComponent.create();

netComponent.inject(this);

netManager.sayHello();

}

}

Teraz pora na klasę początkową:

public class Main{

public static void main(String[] args){

Dagger dagger=new Dagger();

}

}

W klasie Dagger ponownie pojawiła się klasa DaggerNetComponent. W czasie pisania kodu IntelliJ nie rozpoznaje klasy, dlatego na początku edytor będzie pokazywał błąd. Żeby ją wygenerować, należy skompilować kod. W tym celu z górnego menu wybierzemy:


Build -> Rebuild


Po tym działaniu kod powinien się poprawnie skompilować. Rezultatem kompilacji będzie utworzenie pliku DaggerNetComponent. Zostanie on umieszczony w katalogu projektu:

Project\build\generated\sources\anotationProcessor\java\main\dagger

Pamiętać należy jednak, że wygenerowanie klasy nie dodaje automatycznie ścieżki import dla klasy w miejscu jej użycia.

6. Jakie błędy najczęściej można napotkać podczas instalacji Daggera 2?

Większość błędów, które można napotkać, wynika z błędów przy napisaniu samego kodu. Niektóre wynikają z braku pobrania potrzebnych bibliotek wskutek nieprzeprowadzenia synchronizacji przez Gradle’a. Poniżej znajdują się najpopularniejsze komunikaty błędów wraz możliwymi przyczynami:


— Component cannot be resolved to a type.

Błąd pojawia się w związku z interfejsem oznaczonym anotacją Component. Wynika z braku ścieżki import lub jej nierozpoznania wskutek braku synchronizacji Gradle’a.


— Module is not an anotation type.

Błąd pojawia się w związku z klasą oznaczoną anotacją @Module. Przyczyny jak wyżej.


— [Dagger/MissingBinding] com. dagger. XXX cannot be provided without an @Inject constructor or an @Provides-anotated method.

Błąd najczęściej występuje dopiero podczas pracy z Daggerem 2, gdy nie oznaczymy konstruktora anotacją @Inject lub nie zapewnimy w module metody z anotacją @Provides. Wystąpi też, gdy w interfejsie oznaczonym anotacją @Component brak parametru o nazwie module wskazującego na klasę modułu.


— Cannot resolve symbol DaggerNetComponent.

Błąd występuje zawsze po stworzeniu kodu implementującego interfejs komponentu, ale przed kompilacją. Jeżeli występuje po kompilacji, oznacza wadliwe dodanie bibliotek lub nieoczyszczenie projektu ze starej kompilacji.


— [ComponentProcessor: MiscError] dagger.internal.codegen. ComponentProcessor was unable to process this class because not all of its dependencies could be resolved. Check for compilation errors or a circular dependency with generated code.

Pochodny błąd wadliwej instalacji Daggera 2. Występuje najczęściej wtedy, gdy Dagger nie generuje wszystkich potrzebnych klas.

Rozdział III. Implementacja

1. Jakie podstawowe zagadnienia należy znać przed rozpoczęciem pracy z Daggerem 2?

Żeby w pełni pojąć działanie Daggera 2 oraz wstrzykiwanie zależności w ogóle, należy poprawnie zrozumieć dwie rzeczy: automatyzm wstrzykiwania zależności i pojęcie grafu zależności. Są to dwa zagadnienia, które determinują działanie i implementację biblioteki.

2. Na czym polega automatyzm wstrzykiwania zależności?

Wyjaśniając automatyzm wstrzykiwania zależności, warto użyć przykładu zaprezentowanego wcześniej. Jest zatem pole oznaczone anotacją @Inject. Wstrzykniemy do niego zależność:

@Inject

NetManager netManager;

Czyli w istocie chcemy, aby mechanizm wstrzykiwania w którymś miejscu zrobił za nas to:

NetManager netManager = new NetManager();

W tym wypadku nie powinno to stanowić żadnego problemu. Mechanizm wstrzykiwania zależności może utworzyć obiekt za pomocą zeroargumentowego konstruktora. Problem zaczyna się wtedy, gdy obiekt powinien wyglądać tak:

NetManager netManager = new NetManager(user, password, link);

Argumenty to:

User user=new User(name);

Password password=new Password();

Link link=new Link(type);

W tym wypadku mechanizm wstrzykiwania zależności musi wiedzieć:

— Jak utworzyć żądany obiekt.

— Jakie przyjmuje argumenty.

— Jak utworzyć obiekty argumentów.

— Jakie argumenty przyjmują obiekty argumentów.

— I tak dalej…


Jak widać, wstrzykiwanie zależności wymaga dość głębokiej wiedzy o sposobie tworzenia obiektów, która to wiedza musi się rozciągać czasami na kilkanaście, a nawet kilkadziesiąt i więcej klas. Dlatego w tym momencie rozsądne jest postawić pytanie: czy nie można przekazać wymaganych argumentów przy oznaczaniu pola anotacją @Inject, żeby ułatwić pracę mechanizmowi wstrzykiwania zależności? Odpowiedź jest prosta — nawet gdyby można było to zrobić, byłoby to równoznaczne ze zniszczeniem istoty wstrzykiwania zależności, która polega na rozdzieleniu kodu konstrukcyjnego od funkcjonalnego. W tym celu Dagger 2, aby działać automatycznie, musi znać wszystkie elementy potrzebne do utworzenia żądanego obiektu samodzielnie. Przekazanie mu jakichkolwiek argumentów ręcznie tworzy twardą zależność (hard dependency). Twarda zależność to stan, w którym kluczowe elementy kodu zostają w nim osadzone na sztywno. Twarde zależności mają 3 niesprzyjające tworzeniu dobrego kodu cechy:


— Zmniejszają możliwość ponownego użycia kodu.

W programowaniu obiektowym jednym z podstawowych przykazań jest możliwość ponownego użycia klas. Dlatego powinno się zmniejszyć zależność, aby zwiększyć możliwość ponownego wykorzystania, używając wstrzykiwania zależności.


— Utrudniają testowanie.

Klasa powinna pobierać swoje zależności z zewnątrz. Żadna klasa nie powinna tworzyć instancji innej klasy, lecz pobierać instancje z klasy konfiguracyjnej. Jeśli klasa tworzy instancję innej klasy za pośrednictwem operatora new, nie może być używana i testowana niezależnie od tej klasy.


— Utrudniają utrzymanie kodu podczas skalowania projektu.

Dlatego też, aby zwiększyć możliwości ponownego użycia klas, ich testowalność i łatwość konserwacji kodu, warto używać mechanizmu wstrzykiwania zależności.

3. Czym jest graf zależności?

Graf zależności to struktura opisująca pewną grupę obiektów i relacji między nimi, która stanowi podstawę wstrzykiwania zależności. Obejmuje klasy przeznaczone do wstrzyknięcia oraz wymagane przez nie argumenty. Graf zależności musi umieć samodzielnie utworzyć każdy obiekt, który posiada w sobie. Ponadto raz dodana klasa powoduje, że nie trzeba jej dodawać ponownie. Graf jest strukturą, do której nie ma bezpośredniego dostępu. Jest tworzony i zarządzany przez mechanizm Daggera 2.


Umieszczenie klasy w grafie zależności stanowi istotę wstrzykiwania zależności i jego automatyzmu. Klasy mogą znaleźć się w grafie na kilka sposobów. Podstawowy to użycie anotacji @Inject. Ta anotacja może zostać użyta na polu, konstruktorze lub metodzie. Ponadto do grafu można trafić poprzez użycie anotacji @Provides lub @Binds na metodzie w klasie oznaczonej anotacją @Module. Każde pole oznaczone anotacją @Inject wymaga istnienia odpowiadającej mu klasy w grafie.


Ważne, aby zrozumieć, że przy wykorzystaniu Daggera 2 zazwyczaj powstaje więcej niż jeden graf zależności. Każdy graf związany jest z komponentem, a dokładnie mówiąc, jego instancją. Zatem graf jest tworzony wokół komponentu. Czym jest komponent, napisano dalej. Kilka niezależnych komponentów tworzy osobne grafy, dwa obiekty tej samej klasy komponentu również tworzą osobne grafy. Klasy umieszczone w grafie mogą być wykorzystane nie tylko do tworzenia instancji klas do wstrzyknięcia, ale też jako parametry konstruktorów klas w grafie. Ponadto grafu nie należy rozumieć jako rodzaju kontenera (rejestru) obiektów. Graf nie przechowuje gotowych instancji obiektów, lecz wiedzę o tym, jak je skonstruować i jak są powiązane. Konstrukcja grafu opiera się o tworzenie powiązań (bindings) między typami klas. Na klasach właśnie graf opiera swoje działanie. Posługuje się nimi dla rozpoznania, co ma być wstrzyknięte. Dlatego też dla Daggera 2 nie ma znaczenia, jakie nazwy mają metody z komponentów i modułów, ale to, jakie typy zwracają i jakie typy przyjmują jako parametry.

4. Jakie są podstawowe anotacje w Daggerze 2?

Podstawowe anotacje potrzebne do podstawowej pracy z Daggerem 2 to:


— @Inject — umieszczana przede wszystkim na polu lub konstruktorze. Rzadziej na metodzie. Wskazuje na klasę, która ma być wstrzyknięta lub umieszczona w grafie zależności.

— @Component — stanowi most między polem do wstrzyknięcia a modułem. Pewnego rodzaju kontroler dostarczający pomocnych metod.

— @Module — silnik zapewniający konkretne obiekty do wstrzyknięcia przez komponent.

— @Provides — podstawowy sposób wskazania obiektów do wstrzyknięcia. Ta anotacja jest umieszczana na metodach w klasie z anotacją @Module.

5. Jakie są podstawowe kroki, aby zacząć pracę z Daggerem 2?

Aby poprawnie zaimplementować framework Dagger 2, należy wykonać następujące kroki:


— Zidentyfikować pole do wstrzyknięcia i oznaczyć je anotacją @Inject.

— Zidentyfikować klasę, w której jest umieszczone (zadeklarowane) pole do wstrzyknięcia z punktu a.

— Utworzyć interfejs z anotacją @Component. W nim należy utworzyć metodę o dowolnej nazwie, zwracającą typ odpowiadający klasie, która ma być wstrzyknięta (punkt a), oraz metodę przyjmującą jako parametr klasę zawierającą obiekt do wstrzyknięcia (punkt b). Ponadto należy wskazać w nim jako parametr anotacji moduł tworzący obiekty do wstrzyknięcia (punkt d).

— Utworzyć klasę oznaczoną anotacją @Module, która będzie zawierała metodę oznaczoną anotacją @Provides, zwracającą instancje klasy odpowiadającej typowi do wstrzyknięcia (punkt a).

— Utworzyć obiekt stanowiący implementację interfejsu oznaczonego anotacją @Component (punkt c). Należy przekazać mu obiekt zawierający pola do wstrzyknięcia (punkt b).

— Skompilować kod.

6. Jak przebiega podstawowy tok pracy z Daggerem 2?

Zaczniemy od utworzenia mobilnego projektu w środowisku Android Studio z jedną klasą MainActivity. Klasa startowa MainActivity będzie bardzo prosta:

import androidx. appcompat. app. AppCompatActivity;

import android. os. Bundle;


public class MainActivity extends AppCompatActivity{

@Override

protected void onCreate(Bundle savedInstanceState){

super. onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

}

}

Potrzebny będzie także interfejs łączący wszystkie klasy:

public interface Member{

void sayPosition();

}

Na koniec utwórzymy trzy klasy do wstrzyknięcia (biorąc pod uwagę zasady dziedziczenia, nie ma potrzeby oznaczenia wszystkich klas interfejsem, ale uczyniono to, żeby lepiej pokazać związek między nimi):

public class Mother implements Member{

public void sayHello(){

System.out.println(„I am mother”);

};

public void sayPosition(){

System.out.println(„Mother”);

}

}


public class Son extends Mother implements Member{

public void sayHello(){

System.out.println(„I am son”);

};

public void sayPosition(){

System.out.println(„Son ”);

}

}


public class Daughter extends Mother implements Member{

public void sayHello(){

System.out.println(„I am daughter”);

};

public void sayPosition(){

System.out.println(„Daughter ”);

}

}

Teraz zdecydujemy, która klasa będzie wstrzyknięta. Tu użyta zostanie klasa Mother, a ostatecznie wstrzyknięta klasa Son. W tym celu umieścimy pole w MainActivity:

@Inject

Mother mother;

Następnie utwórzymy interfejs o nazwie FamilyComponent. Należy oznaczyć go anotacją @Component. Przy anotacji użyjemy parametru, wskazując na moduł, który zaraz zostanie utworzony. W interfejsie wygenerujemy metodę o nazwie provideMember() — bez parametrów, zwracającą klasę Mother. Na końcu dodamy metodę inject() — przyjmującą parametr MainActitivity:

import dagger.Component;


@Component(modules={FamilyModule.class})

public interface FamilyComponent{

Mother provideMember();

void inject(MainActivity mainActivity);

}

Teraz utworzymy klasę oznaczoną anotacją @Module, która będzie zawierać metodę oznaczoną anotacją @Provides, zwracającą obiekt Son:

import dagger.Module;

import dagger.Provides;


@Module

public class FamilyModule{

@Provides

Mother providesFamilyMember(){

return new Son();

}

}

Na końcu w MainActivity i w metodzie onCreate stworzymy obiekt DaggerFamilyComponent za pomocą metody create(). Wywołamy także metodę inject na obiekcie mainActivity. Kod kończysz wywołaniem metody sayHello():

FamilyComponent familyComponent=DaggerFamilyComponent.create();

familyComponent.inject(this);

mother.sayHello();

Jak już wcześniej wspomniano, DaggerFamilyComponent stanowi klasę implementującą interfejs FamilyComponent. Jest ona generowana automatycznie przez Daggera 2 i nie mamy bezpośredniego wpływu na jej nazwę. Dagger 2 dodaje po prostu prefiks „Dagger” do nazwy interfejsu.


Czas na kompilację. W konsoli otrzymamy tekst: „I am son”. Jest to konsekwencja podstawienia do wstrzyknięcia w module obiektu Son. Równie dobrze można podstawić obiekt klasy Mother lub Daughter:

import dagger.Module;

import dagger.Provides;


@Module

public class FamilyModule{

@Provides

Mother providesFamilyMember(){

//return new Son();

//return new Daughter();

return new Mother();

}

}

Warto zauważyć, że w powyższym przykładzie w grafie zależności utworzono powiązanie (binding) między klasą Mother, którą zamierzasz wstrzyknąć, a Son, która zostanie dostarczona przez moduł.

7. Czy musi istnieć metoda zwracająca klasę do wstrzyknięcia zarówno w komponencie, jak i module?

Co do zasady — tak. Takie pytania pojawiają się w związku z podobieństwem między komponentem i modułem. W obu mogą wystąpić metody o identycznej nazwie i zwracające tę samą klasę. Jednak należy zrozumieć przede wszystkim konstrukcję mechanizmu Daggera 2. Komponent jest mostem między modułem (dostawcą) a klasą zawierającą referencję do wstrzyknięcia (konsumentem). Jest on przede wszystkim interfejsem, który nie daje możliwości ręcznej implementacji zadeklarowanych metod. Dostarcza on tylko API dla programisty, który sam nie ma dostępu do modułu. Moduł natomiast zapewnia konkretne obiekty, które poprzez interfejs, jaki stanowi komponent, są wstrzykiwane. Dlatego też, choć może się wydawać, że kod z komponentu jest powielany w module, wraz z poznaniem działania Daggera 2 to rozróżnienie staje się oczywiste. Pokreślić należy, że nazwy metod nie mają znaczenia. Mogą się nazywać tak samo w module i komponencie, ale nie muszą. Zazwyczaj zaczynają się od słowa „provide”.


Można jednak zrezygnować z deklaracji metod dostarczających klasy w komponencie, a nawet metod dostarczających obiekty z modułu w prostych konstrukcjach (implicit injection), gdy Dagger 2 wie, jak samodzielnie utworzyć obiekt, który ma anotację @Inject na konstruktorze lub jest dostarczany przez metodę z anotacją @Provides, oraz nie ma wątpliwości, który obiekt może być wstrzyknięty do żądanej referencji.

8. W jaki sposób komponent jest powiązany z modułem?

Komponent zapewnia API dla programisty, przedstawiając do dyspozycji możliwość wstrzyknięcia określonych klas. O tym, jak zostaną utworzone, decyduje moduł. Formalne powiązanie następuje przez wskazanie klasy modułu w parametrze komponentu. Zwane jest to czasami instalacją modułu:

import dagger.Component;


@Component(modules={FamilyModule.class})

public interface FamilyComponent{…}

Komponent może mieć zainstalowany więcej niż jeden moduł. W komponencie umieszcza się metody zwracające typy, które mają być wstrzyknięte. Moduły mają zapewnić implementację tych typów. Jednak sama deklaracja metody, która zwraca określoną klasę w komponencie nie wymusza konieczności zapewnienia obiektu przez moduł. Dopiero jeżeli chcemy wstrzyknąć obiekt, musi temu towarzyszyć odpowiednia metoda w komponencie i module. Więcej na ten temat zostanie powiedziane w następnym rozdziale.

9. Czy można umieścić dwa pola do wstrzyknięcia z różnych komponentów w tej samej klasie?

Dotyczy to następującej sytuacji, w której w jednej klasie są pola z anotacjami @Inject:

@Inject

NetManager netManager;

@Inject

Mother mother;

Mają one być obsłużone przez różne komponenty:

NetComponent netComponent=DaggerNetComponent.create();

netComponent.inject(this);

FamilyComponent familyComponent=DaggerFamilyComponent.create();

familyComponent.inject(this);

Odpowiedź na pytanie brzmi: nie. Niemożliwość wstrzykiwania pól znajdujących się w jednej klasie przez różne komponenty wynika z czysto praktycznego powodu. Każdy z komponentów skanuje kolejno wskazaną w metodzie inject klasę w całości i osobno. Pierwszy komponent znajdzie wszystkie anotacje @Inject i będzie się starał dopasować do nich odpowiednie deklaracje z interfejsu według typów klas. Części nie znajdzie, bo są obsługiwane przez inny komponent, i dlatego kompilacja zakończy się błędem. Zatem jeden komponent może obsługiwać jedną lub wiele klas zawierających pola do wstrzyknięcia, ale jedna klasa nie może być obsługiwana przez kilka komponentów.

10. Czym jest anotacja @Inject i gdzie może być użyta?

Anotacja @Inject służy różnym celom, przede wszystkim pozwala nam określić punkt wstrzyknięcia zależności. Mówi ona mechanizmowi Daggera 2, gdzie wstawić odpowiednią zależność. Do tej pory widzieliśmy tę anotację jedynie na polu w takiej postaci:

@Inject

Mother mother;

Jednak Dagger 2 pozwala umieścić taką anotację także w dwóch innych miejscach, tj. na konstruktorze i metodzie. Niestety, znaczenie takich anotacji jest dość odmienne od tego, co przedstawiono do tej pory. Warto zacząć od anotacji na konstruktorze. Wygląda ona następująco:

import javax.inject.Inject;


public class Mother{

@Inject

public Mother(){}

public void sayHello(){

System.out.println(„I am mother”);

};

}

Rzadziej anotacja może być na metodzie:

public class Son extends Mother{

Daughter daughter;

@Inject

void prepareFamily(Daughter daughter){

this. daughter=daughter;

}

public void sayHello(){

System.out.println(„I am son”);

}

}

11. Jakie znaczenie ma anotacja @Inject na polu?

Użycie anotacji @Inject na polu jest głównym sposobem wstrzyknięcia zależności do referencji i implikuje kilka rzeczy:


— Gdzieś w kodzie zainicjowano implementację interfejsu komponentu.

— Gdzieś w kodzie wywołano metodę z komponentu, która przyjmuje za parametr klasę, w której znajduje się pole z anotacją @Inject celem przeskanowania kodu i przypisania obiektu.

— Komponent zawiera deklarację metody, która zwraca typ zbieżny z klasą oznaczoną anotacją @Inject.

— Gdzieś w kodzie zapewniony jest obiekt do wstrzyknięcia albo ręcznie w kodzie modułu utworzony i zwrócony zostaje obiekt zbieżny z klasą oznaczoną anotacją @Inject lub też konstrukor tej klasy zostaje oznaczony anotacją @Inject.

— Komponent jest powiązany z modułem przez argument o nazwie module.

12. Jakie są wymagania wobec pola oznaczonego anotacją @Inject?

Pole oznaczone anotacją @Inject może przede wszystkim być klasą lub interfejsem. W przypadku klasy wskazanie obiektu do wstrzyknięcia może się odbyć poprzez modułową metodę oznaczoną anotacją @Provides (ewentualnie anotacją @Binds) albo przez umieszczenie anotacji @Inject na konstruktorze tej klasy. Pole interfejsowe ma mniej opcji, co wynika z zasad czysto logicznych. Dostarczenie obiektu możliwe jest poprzez metodę modułową, w ramach której tworzy się konkretną implementację danego interfejsu. Dla przykładu do takiego pola interfejsowego:

@Inject

Member member;

nie można wstrzyknąć zależności automatycznie poprzez proste umieszczenie anotacji @Inject na konstruktorze klasy implementującej interfejs:

public class Mother implements Member{

@Inject

public Mother(){}

public void sayPosition(){

System.out.println(„Mother”);

}

public void sayHello(){

System.out.println(„I am mother”);

}

}

W takim przypadku konieczna jest modułowa metoda tworząca i zwracająca instancję klasy implementującej:

@Provides

Member providesMember(){

return new Mother();

}

W istocie istnieje sposób na dostarczenie implementacji do wstrzyknięcia do interfejsu bez metody z anotacją @Provides, lecz za pomocą metody z anotacją @Binds, ale o tym przy innej okazji. Ponadto ważnym warunkiem i zarazem dużym ograniczeniem jest wymóg, aby pole oznaczone anotacją @Inject nie było prywatne.

13. W jaki sposób pole do wstrzyknięcia jest powiązane z komponentem?

Do pola oznaczonego anotacją @Inject obiekt nie zostaje wstrzyknięty automatycznie. Sama anotacja nie wystarcza. Dagger 2 posługuje się techniką ręcznego wstrzykiwania. Oznacza to, że Dagger 2 nie skanuje całości kodu źródłowego aplikacji w poszukiwaniu anotacji @Inject. Należy ręcznie wskazać klasę zawierającą pole do obsłużenia za pomocą metody inject().


Zatem aby prawidłowo obsłużyć anotację @Inject na polu, konieczne jest stworzenie obiektu oznaczonego anotacją @Component, zawierającego metodę z parametrem klasy, w której znajduje się pole do wstrzyknięcia. Konieczne jest także wywołanie metody z tego obiektu, które stanowi o inicjalizacji klasy. Bez tego komponent nie wie w istocie, gdzie znajdują się pola do wstrzyknięcia. Oczywiście oprócz tego należy pamiętać, że musi istnieć moduł zapewniający obiekt do wstrzyknięcia.

14. Jakie znaczenie ma anotacja @Inject na konstruktorze?

Pomimo takiego samego słowa kluczowego anotacja @Inject na polu jest trochę inna niż na konstruktorze. W przypadku pola oczekuje się przypisania konkretnego obiektu przez moduł do referencji. W przypadku anotacji na konstruktorze jedyne, co można osiągnąć, to umieszczenie danej klasy w grafie zależności. W żaden sposób nie implikuje to konieczności utworzenia konkretnej instancji klasy. Taka anotacja oznacza tylko „gotowość” do wstrzyknięcia gdzieś w kodzie za pomocą mechanizmu Daggera 2. Zatem dlaczego używa się takich samych anotacji dla podobnych, ale w istocie różniących się mechanizmów? Odpowiedź jest prosta — ponieważ anotacja @Inject nie jest własną anotacją Daggera 2, ale została odziedziczona po standardzie wstrzykiwania zależności JSR-330.


Gdyby istaniała możliwość zmiany nazwy tej anotacji, żeby lepiej odpowiadała rzeczywistemu celowi, to anotacja na polu mogłaby mieć nazwę @InjectIntoThis, a ta na konstruktorze @Injectable. Anotacji @InjectIntoThis musiałaby towarzyszyć metoda modułowa zwracająca klasę odpowiadającą polu lub musiałby istnieć konstruktor żądanej klasy oznaczony anotacją @Inject. Natomiast w przypadku anotacji @Injectable mogliyśmy zrezygnować z metod w module, które są oznaczone @Provides i zwracają daną klasę.

15. Jaki cel ma anotacja @Inject na konstruktorze?

Jak wspomniano wcześniej, jej główny cel to uczynienie danej klasy „wstrzykiwalną”. Użycie tej anotacji implikuje w praktyce kilka rzeczy:


— Klasa staje się zdolna do wstrzyknięcia gdzieś w kodzie za pomocą @Inject na polu.

— Klasa zostaje umieszczona w grafie zależności.

— Mechanizm Daggera 2 będzie wiedział, jak automatycznie utworzyć instancję danej klasy.

— Wszystkie argumenty konstruktora potrzebne do utworzenia klasy muszą również znajdować się w grafie zależności (nie należy zapominać o zasadzie automatyzmu).


Jaki jest zatem cel tej anotacji? Jest ona w istocie substytutem @Provides. Zamiast pisać metodę i ręcznie deklarować klasę, wystarczy umieścić jedna anotację na konstruktorze. Stanowi on też element mechanizmu określanego jako niejawne wstrzyknięcie (implicit injection), czyli umieszczania zależności w grafie bez dodatkowego kodu, który tworzy instancję danej klasy.

16. Jaka jest różnica między anotacją @Inject na konstruktorze a metodą modułową z anotacją @Provides?

Przyjrzyjmy się najpierw kodowi. Na początku konstruktor klasy Mother z anotacją:

@Inject

public Mother(){}

A teraz alternatywny sposób z klasą oznaczoną anotacją @Module:

@Provides

Mother providesFamilyMember(){

return new Mother();

}

Ostateczny cel tych dwóch sposobów jest taki sam czyli dostarczyć klasę Mother do grafu zależności. Jak widać, dużo łatwiej jest jednak dodać anotację, niż napisać całą metodę. Ponadto raz oznaczona w ten sposób klasa nie musi być powtarzana w kilku modułach. I to w sumie tyle korzyści z anotacji na konstruktorze. Ma ona także kilka wad:


— Wymaga dostępu do kodu źródłowego, aby umieścić anotację.

— Wymaga umieszczenia w grafie wszystkich parametrów, tak aby móc samodzielnie i automatycznie utworzyć obiekt.

— Uniemożliwia konfigurację obiektu po jego utworzeniu.

— Nie pozwala jasno określić, jakie klasy zostają umieszczone w grafie.


Z drugiej strony metoda z anotacją @Provides ma wiele zalet:

— Nie wymaga dostępu do kodu źródłowego i tym samym może pracować z zewnętrznymi bibliotekami.

— Umożliwia wprowadzenie parametrów, które nie znajdują się w grafie zależności.

— Umożliwia zwracanie istniejącej instancji zamiast zwracania za każdym razem nowej.

— Umożliwia konfigurację obiektu w czasie działania programu (run-time).


Przyjrzyjmy się fikcyjnemu przykładowi takiej metody oznaczonej anotacją @Provides z kilkoma jej zaletami:

MyParentClass myParentClass;


@Provides

MyParentClass providesParent(){

if(myParentClass == null)

myParentClass = new MyKidClass (“SonsName”);

myParentClass.configAndStuff();

return myParentClass;

}

Jak widać, anotacja @Provides umożliwia też utworzenie jednego obieku, który będzie później powtórnie wykorzystywany. Ponadto możliwe jest utworzenie instancji z użyciem obiektów, które nie znajdują się w grafie zależności, oraz skonfigurowanie obiektu przed przekazaniem go do użycia.

17. Jakie są wymagania wobec konstruktora z anotacją @Inject?

Jak wskazano wyżej, anotacją @Inject można przede wszystkim oznaczyć konstruktor bezargumentowy:

@Inject

public Mother(){}

Możliwe jest także oznaczenie konstruktora z parametrami:

@Inject

public Mother(Sister sister){}

Warunkiem jest natomiast, aby każdy parametr konstruktora znajdował się w grafie zależności. Można to osiągnąć przez metodę w module — oznaczoną anotacją @Provides:

@Provides

Sister provideSister(){

return new Sister();

}

albo też przez oznaczenie konstruktora tego argumentu anotacją @Inject:

public class Sister{

@Inject

public Sister (){}

}

W tym drugim przypadku także jego argumenty muszą być w grafie. Tym sposobem można doprowadzić do efektu łańcuchowego i konieczności umieszczenia w grafie kilkunastu, a nawet kilkudziesięciu klas. Ważnym ograniczeniem jest to, aby tylko jeden konstruktor w danej klasie był oznaczony anotacją @Inject. Inaczej Dagger 2 nie wie, z pomocą którego konstruktora utworzyć obiekt.

18. W jaki sposób wskazać klasę do wstrzyknięcia przy anotacji @Inject na konstruktorze kilku pasujących klas?

Problem dotyczy sytuacji, gdy istnieje kilka klas, które dziedziczą po jednej z nich, lub też zamierzamy wstrzyknąć jedną z implementacji do interfejsu. Każda z klas ma anotacje @Inject na konstruktorze, co zwalnia z używania modułowych metod oznaczonych anotacją @Provides. W którymś miejscu kodu natomiast chcemy wstrzyknąć obiekt do następującego pola:

@Inject

Mother mother;

Jak wskazano wcześniej, w przypadku użycia anotacji @Inject na konstruktorze nie jest konieczne posługiwanie się metodami modułowymi z anotacją @Provides. Gdyby użyć takiej metody, to problem zostałby łatwo rozwiązany, gdyż w takich metodach obiekty są tworzone ręcznie — i to programista decyduje, co wstrzyknąć:

@Provides

Mother providesMember(){

return new Son();

}

W przypadku anotacji @Inject obiekt jest tworzony automatycznie przez Daggera 2. Pojawia się zatem pytanie: który obiekt zostanie wstrzyknięty? Każdy z powyższych konstruktorów jest w stanie spełnić wymaganie utworzenia instancji do pola typu nadrzędnej klasy Mother. W tej sytuacji wstrzyknięty zostanie jednak obiekt Mother jako pasujący do referencji z anotacją @Inject, czyli Mother.


Jeżeli spróbujemy wstrzyknąć w pole, które jest interfejsem:

@Inject

Member member;

posługując się następującą metodą z komponentu:

Member provideMember();

i każda z powyższych klas implementowałaby ten interfejs, to w przypadku zażądania wstrzyknięcia do pola interfejsu Dagger 2 wygeneruje błąd podczas kompilacji, informując, że interfejs Member nie został zaimplementowany za pomocą metody z anotacją @Provides. Oznacza to, że posługując się interfejsami, musisz wykorzystywać metodę modułową z anotacją @Provides.

19. Który mechanizm ma pierwszeństwo w przypadku konfliktu — z anotacją @Inject czy @Provides?

Problem dotyczy przypadku, gdy konieczne jest dostarczenie obiektu konkretnej klasy do wstrzyknięcia, a w kodzie występuje konstruktor oznaczony anotacją @Inject:

public class Mother implements Member{

@Inject

public Mother(){}

public void sayPosition(){

System.out.println(„Mother”);

}

public void sayHello(){System.out.println(„I am mother”);};

}

oraz modułowa metoda oznaczona anotacją @Provides, która zwraca ten obiekt:

@Provides

Mother providesFamilyMember(){

return new Mother();

}

W takim przypadku nie zachodzi żaden konflikt, ponieważ oba mechanizmy wykorzystują ten sam konstruktor. Użyty zostanie on tylko raz. Pierwszeństwo ma metoda modułowa. Problem pojawia się przy dwóch różnych konstruktorach, gdy występuje konstruktor z anotacją @Inject i jednocześnie metoda modułowa wykorzystuje inny konstruktor:

@Inject

public Mother(){}

public Mother(String name){}

W takim przypadku również pierwszeństwo ma metoda modułowa z anotacją @Provides:

@Provides

Mother providesFamilyMember(){

return new Mother(„Anna”);

}

20. Jaki cel ma anotacja @Inject na metodzie?

Taka metoda wygląda następująco:

public class Son extends Mother{

String secondName;

@Inject

void prepareFamily(Daughter daughter){

this.secondName = daughter.getSecondName();

}

public void sayHello(){

System.out.println(„I am son”);

}

}

Dodanie anotacji do metody za pomocą @Inject przekazuje Daggerowi 2 polecenie wykonania tej metody automatycznie zaraz po utworzeniu obiektu oraz zaraz po wywołaniu konstruktora. Jest to przydatne, gdy potrzebujemy do czegoś w pełni skonstruowanego obiektu. To po prostu kolejny sposób umieszczenia przez Daggera zależności w grafie podczas konstruowania lub wstrzykiwania gotowego obiektu.


Metoda z anotacjami @Inject jest wywoływana przez Daggera 2 raz podczas konstrukcji. Takiej metody nie należy wykonywać samodzielnie/ręcznie. Jest ona automatycznie wykonywana przez mechanizm Daggera 2. Stanowi pewnego rodzaju dodatkowy konstruktor. Ideą stojącą za tym mechanizmem jest umożliwienie dodatkowej konfiguracji obiektu.


Należy pamiętać, że wszystkie argumenty takiej metody muszą znajdować się w grafie zależności. Ponadto sama klasa zawierająca taką metodę może, ale nie musi być w grafie. Czyli może być wstrzykiwana lub nie, może mieć konstruktor z anotacją @Inject lub być dostarczona przez metodę z anotacją @Provides, albo i nie. Metoda z anotacją @Inject nie wymaga „wstrzykiwalności” klasy, do której należy.

21. W którym momencie jest tworzony obiekt do wstrzyknięcia?

Jeżeli przyjrzymy się dotychczasowemu kodowi, zauważymy że teoretycznie momentem tworzenia obiektu mogą być trzy zdarzenia: inicjalizacja obiektu komponentu, wywołanie metody inject(), która skanuje kod w poszukiwaniu anotacji @Inject, lub też pierwsze użycie obiektu:

FamilyComponent familyComponent=DaggerFamilyComponent.create();

familyComponent.inject(activity);

mother.sayHello();

W Daggerze 2 obiekt jest tworzony w chwili wywołania metody inject().

22. Jakie są sposoby wstrzykiwania zależności?

Do tej pory wstrzyknięcie odbywało się automatycznie za pomocą oznaczenia pola anotacją @Inject. Dagger 2 za pomocą swoich wewnętrznych metod wstrzykiwał obiekt do pola w momencie wywołania metody inject():

@Inject

Mother mother;

FamilyComponent familyComponent=DaggerFamilyComponent.create();

familyComponent.inject(this);

Jest jednak również inny sposób przypisania obiektu do referencji. Odbywa się to ręcznie za pomocą obiektu implementującego interfejs komponentu. W takim wypadku po usunięciu anotacji @Inject znad referencji:

Mother mother;

będzie można pobrać zależność komponentu ręcznie za pomocą metody, którą została przez nas stworzona:

FamilyComponent familyComponent=DaggerFamilyComponent.create();

familyComponent.inject(activity);

mother=familyComponent.provideFamilyMember();

mother.sayHello();

Warto pamiętać, że implementacja interfejsu oznaczonego anotacją @Component jest zwykłym obiektem, a zawarte tam metody da się wywołać ręcznie.

23. Jakie są dobre praktyki przy implementacji Daggera 2?

Większość z poniższych punktów będzie można w pełni zrozumieć dopiero po zapoznaniu się ze wszystkimi tematami, ale warto już teraz podkreślić kilka rzeczy (żeby potem do nich powrócić):


— Należy unikać zbędnych anotacji zakresowych.

— Zaleca się używanie statycznych metod dostarczających wszędzie tam, gdzie to możliwe.

— Wskazane jest dostarczanie obiektów do Daggera 2 przez builder komponentu, a nie parametry konstruktora modułu.

— Korzystniejsze jest wykorzystywanie anotacju @Binds zamiast @Provides.

— W testowaniu zależy zmieniać zależności komponentów, zamiast nadpisywać moduły.

24. Jakie błędy najczęściej pojawiają się przy implementacji?

Poniżej przedstawiono najpopularniejsze komunikaty błędów wraz z możliwymi przyczynami.


— Dagger does not support injection into private fields.

Błąd pojawia się przy próbie wstrzyknięcia zależności w prywatne pole.


— [Dagger/MissingBinding] com. dagger. XXXX cannot be provided without an @Inject constructor or an @Provides-anotated method.

Przeczytałeś bezpłatny fragment.
Kup książkę, aby przeczytać do końca.
E-book
za 36.75
drukowana A5
za 58.23