Dagger & Android

Jedną z podstawowych zalet Dagger 2 w porównaniu z większością innych frameworków wstrzykiwania zależności jest to, że jego ściśle generowana implementacja (bez refleksji) oznacza, że może być używany w aplikacjach na Androida. Jednakże, nadal istnieją pewne rozważania, które należy poczynić podczas używania Daggera w aplikacjach na Androida.

Filozofia

Kod napisany dla Androida jest kodem źródłowym Javy, ale często jest zupełnie inny pod względem stylu. Zazwyczaj takie różnice istnieją w celu dostosowania się do unikalnych względów wydajnościowych platformy mobilnej.

Ale wiele wzorców powszechnie stosowanych do kodu przeznaczonego dla Androida jest sprzecznych z tymi stosowanymi do innego kodu Java. Nawet wiele z porad w Efektywnej Javie jest uważanych za nieodpowiednie dla Androida.

Aby osiągnąć cele zarówno idiomatycznego i przenośnego kodu, Dagger polega na ProGuard do post-processingu skompilowanego bajtkodu. To pozwala Daggerto emitować źródło, które wygląda i czuje się naturalnie zarówno na serwerze jak i na Androidzie, podczas gdy używa różnych toolchains do produkcji kodu bajtowego, który wykonuje się efektywnie w obu środowiskach. Ponadto, Dagger ma wyraźny cel, aby zapewnić, że źródło Java, które generuje jest konsekwentnie kompatybilne z optymalizacjamiProGuard.

Oczywiście, nie wszystkie problemy mogą być rozwiązane w ten sposób, ale jest to podstawowy mechanizm, dzięki któremu zostanie zapewniona kompatybilność specyficzna dla Androida.

tl;dr

Dagger zakłada, że użytkownicy na Androidzie będą używać R8 lub ProGuard.

Dlaczego Dagger na Androida jest trudny

Jedną z głównych trudności pisania aplikacji na Androida przy użyciu Dagger jest to, że wiele klas frameworka Androida jest instancjonowanych przez sam system operacyjny, jakActivity i Fragment, ale Dagger działa najlepiej, jeśli może tworzyć wszystkie wstrzykiwane obiekty. Zamiast tego, musisz wykonać wstrzykiwanie członków w metodzie cyklu życia. Oznacza to, że wiele klas wygląda jak:

public class FrombulationActivity extends Activity { @Inject Frombulator frombulator; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // DO THIS FIRST. Otherwise frombulator might be null! ((SomeApplicationBaseType) getContext().getApplicationContext()) .getApplicationComponent() .newActivityComponentBuilder() .activity(this) .build() .inject(this); // ... now you can write the exciting code }}

Powoduje to kilka problemów:

  1. Kopiowanie kodu sprawia, że trudno jest go później refaktoryzować. Ponieważ coraz więcej programistów kopiuje ten blok, coraz mniej z nich będzie wiedziało, co on właściwie robi.

  2. Ponadto, wymaga, aby typ żądający wstrzyknięcia (FrombulationActivity) wiedział o swoim wstrzykiwaczu. Nawet jeśli jest to zrobione poprzez interfejsy zamiast konkretnych typów, łamie to podstawową zasadę wstrzykiwania zależności: klasa nie powinna wiedzieć nic o tym, jak jest wstrzykiwana.

dagger.android

Klasy w dagger.android oferują jedno podejście do uproszczenia powyższych problemów. Wymaga to nauczenia się kilku dodatkowych API i koncepcji, ale daje zredukowany boilerplate i wstrzykiwanie klas Androida w odpowiednim miejscu cyklu życia.

Innym podejściem jest po prostu użycie normalnych API Dagger i podążanie za przewodnikami, takimi jak ten tutaj.Może to być prostsze do zrozumienia, ale wiąże się z minusem w postaci konieczności ręcznego pisania dodatkowych szablonów.

Zespoły Jetpack i Dagger pracują razem nad nową inicjatywą dla Dagger na Androida, która ma nadzieję być dużą zmianą w stosunku do obecnego statusquo. Chociaż niestety nie jest to jeszcze gotowe, może to być coś do rozważeniaerwhen wybierając jak używać Dagger w swoich projektach Android dzisiaj.

Injecting Activity objects

  1. Zainstaluj AndroidInjectionModule w swoim komponencie aplikacji, aby zapewnićthat wszystkie wiązania niezbędne dla tych typów bazowych są dostępne.

  2. Zacznij od napisania @Subcomponent, który implementujeAndroidInjector<YourActivity>, z@Subcomponent.Factory, który rozszerzaAndroidInjector.Factory<YourActivity>:

    @Subcomponent(modules = ...)public interface YourActivitySubcomponent extends AndroidInjector<YourActivity> { @Subcomponent.Factory public interface Factory extends AndroidInjector.Factory<YourActivity> {}}
  3. Po zdefiniowaniu podkomponentu, dodaj go do swojej hierarchii komponentów poprzez zdefiniowanie modułu, który wiąże fabrykę podkomponentu i dodanie go do komponentu, który wstrzykuje twój Application:

    @Module(subcomponents = YourActivitySubcomponent.class)abstract class YourActivityModule { @Binds @IntoMap @ClassKey(YourActivity.class) abstract AndroidInjector.Factory<?> bindYourAndroidInjectorFactory(YourActivitySubcomponent.Factory factory);}@Component(modules = {..., YourActivityModule.class})interface YourApplicationComponent { void inject(YourApplication application);}

    Pro-tip: Jeśli Twój subcomponent i jego fabryka nie mają innych metod lubsupertypów poza tymi wymienionymi w kroku #2, możesz użyć@ContributesAndroidInjector, aby wygenerować je za Ciebie. Zamiast kroków 2 i 3, dodaj metodę modułu abstract, która zwraca twoją aktywność, opatrz ją adnotacją @ContributesAndroidInjector i określ moduły, które chcesz zainstalować w podkomponencie. Jeśli podkomponent potrzebuje zakresu, zastosuj adnotacje zakresu również do metody.

    @ActivityScope@ContributesAndroidInjector(modules = { /* modules to install into the subcomponent */ })abstract YourActivity contributeYourAndroidInjector();
  4. Następnie spraw, aby Twój Application zaimplementował HasAndroidInjector i @Inject aDispatchingAndroidInjector<Object> toreturn z metody androidInjector():

    public class YourApplication extends Application implements HasAndroidInjector { @Inject DispatchingAndroidInjector<Object> dispatchingAndroidInjector; @Override public void onCreate() { super.onCreate(); DaggerYourApplicationComponent.create() .inject(this); } @Override public AndroidInjector<Object> androidInjector() { return dispatchingAndroidInjector; }}
  5. Na koniec, w swojej Activity.onCreate() metodzie, wywołajAndroidInjection.inject(this)przed wywołaniem super.onCreate();:

    public class YourActivity extends Activity { public void onCreate(Bundle savedInstanceState) { AndroidInjection.inject(this); super.onCreate(savedInstanceState); }}
  6. Gratulacje!

Jak to zadziałało?

AndroidInjection.inject() otrzymuje DispatchingAndroidInjector<Object> od Application i przekazuje twoją aktywność do inject(Activity). TheDispatchingAndroidInjector wyszukuje AndroidInjector.Factory dla klasy twojej aktywności (którą jest YourActivitySubcomponent.Factory), tworzy AndroidInjector (którą jest YourActivitySubcomponent) i przekazuje twoją aktywność do inject(YourActivity).

Wstrzykiwanie obiektów Fragment

Wstrzyknięcie Fragment jest tak samo proste jak wstrzyknięcie Activity. Zdefiniuj swój subkomponent w ten sam sposób.

Zamiast wstrzykiwać w onCreate(), jak to się robi dla Activitytypów, wstrzykuj Fragments do w onAttach().

W przeciwieństwie do modułów zdefiniowanych dla Activitys, masz wybór, gdzie zainstalować moduły dla Fragments. Możesz uczynić swój komponent Fragment podkomponentem innego komponentu Fragment, komponentu Activity lub komponentuApplication – wszystko zależy od tego, jakich innych wiązań wymaga Twój Fragment. Po podjęciu decyzji o lokalizacji komponentu, spraw, aby odpowiadający mu typ implementował HasAndroidInjector (jeśli jeszcze tego nie robi). Na przykład, jeśli twój Fragment wymaga wiązań z YourActivitySubcomponent, twój kod będzie wyglądał mniej więcej tak:

public class YourActivity extends Activity implements HasAndroidInjector { @Inject DispatchingAndroidInjector<Object> androidInjector; @Override public void onCreate(Bundle savedInstanceState) { AndroidInjection.inject(this); super.onCreate(savedInstanceState); // ... } @Override public AndroidInjector<Object> androidInjector() { return androidInjector; }}public class YourFragment extends Fragment { @Inject SomeDependency someDep; @Override public void onAttach(Activity activity) { AndroidInjection.inject(this); super.onAttach(activity); // ... }}@Subcomponent(modules = ...)public interface YourFragmentSubcomponent extends AndroidInjector<YourFragment> { @Subcomponent.Factory public interface Factory extends AndroidInjector.Factory<YourFragment> {}}@Module(subcomponents = YourFragmentSubcomponent.class)abstract class YourFragmentModule { @Binds @IntoMap @ClassKey(YourFragment.class) abstract AndroidInjector.Factory<?> bindYourFragmentInjectorFactory(YourFragmentSubcomponent.Factory factory);}@Subcomponent(modules = { YourFragmentModule.class, ... }public interface YourActivityOrYourApplicationComponent { ... }

Base Framework Types

Ponieważ DispatchingAndroidInjector wyszukuje odpowiednieAndroidInjector.Factory przez klasę w czasie wykonywania, klasa bazowa może zaimplementowaćHasAndroidInjector, jak również wywołać AndroidInjection.inject(). Wszystko, co musi zrobić każda podklasa, to powiązać odpowiednią @Subcomponent. Dagger dostarcza kilka typów bazowych, które to robią, takich jak DaggerActivity i DaggerFragment, jeśli nie masz skomplikowanej hierarchii klas. Dagger dostarcza również DaggerApplication do tego samego celu – wszystko, co musisz zrobić, to rozszerzyć go i nadpisać metodę applicationInjector(), aby zwrócić komponent, który powinien wstrzyknąć Application.

Zawarte są również następujące typy:

  • DaggerService i DaggerIntentService
  • DaggerBroadcastReceiver
  • DaggerContentProvider

Uwaga: DaggerBroadcastReceiver powinien być używany tylko wtedy, gdyBroadcastReceiver jest zarejestrowany w AndroidManifest.xml. GdyBroadcastReceiver jest tworzony we własnym kodzie, preferuj konstruktor injectioninstead.

Biblioteki pomocnicze

Dla użytkowników biblioteki pomocniczej Androida, typy równoległe istnieją w pakieciedagger.android.support.

TODO(ronshapiro): powinniśmy zacząć rozdzielać to według pakietów androidx

Jak to zdobyć?

Dodaj poniższe do swojego build.gradle:

dependencies { implementation 'com.google.dagger:dagger-android:2.x' implementation 'com.google.dagger:dagger-android-support:2.x' // if you use the support libraries annotationProcessor 'com.google.dagger:dagger-android-processor:2.x' annotationProcessor 'com.google.dagger:dagger-compiler:2.x'}

Kiedy wstrzykiwać

Wstrzykiwanie konstruktora jest preferowane, gdy tylko jest to możliwe, ponieważ javac zapewni, że żadne pole nie jest przywoływane przed jego ustawieniem, co pomaga uniknąćNullPointerExceptions. Kiedy wstrzykiwanie członków jest wymagane (jak omówiono powyżej), preferuj wstrzykiwanie tak wcześnie, jak to możliwe. Z tego powodu, DaggerActivity wywołuje AndroidInjection.inject() natychmiast w onCreate(), przed wywołaniemsuper.onCreate(), a DaggerFragment robi to samo w onAttach(), co również zapobiega niespójnościom, jeśli Fragment zostanie ponownie dołączone.

Kluczowe jest wywołanie AndroidInjection.inject() przed super.onCreate() w Activity, ponieważ wywołanie super dołącza Fragments z poprzedniej instancji aktywności podczas zmiany konfiguracji, która z kolei wstrzykuje Fragments. Aby wstrzyknięcie Fragment się powiodło, Activitymusi być już wstrzyknięte. Dla użytkowników ErrorProne, jest to błąd kompilatora, aby wywołać AndroidInjection.inject() po super.onCreate().

FAQ

Scoping AndroidInjector.Factory

AndroidInjector.Factory ma być bezstanowym interfejsem, więc implementatorzy nie muszą się martwić o zarządzanie stanem związanym z obiektem, który zostanie wstrzyknięty. Kiedy DispatchingAndroidInjector żądaAndroidInjector.Factory, robi to poprzez Provider, więc nie zachowuje jawnie żadnych instancji fabryki. Ponieważ niektóre implementacje mogą zawierać instancję Activity/Fragment/etc, które są wstrzykiwane, błędem czasu kompilacji jest stosowanie zakresu do metod, które je udostępniają. Jeśli jesteś pewien, że twój AndroidInjector.Factory nie zachowuje instancji wstrzykiwanego obiektu, możesz wyeliminować ten błąd przez zastosowanie@SuppressWarnings("dagger.android.ScopedInjectorFactory") do twojej metody modułowej.