Dagger & Android

Eén van de belangrijkste voordelen van Dagger 2 ten opzichte van de meeste andere dependency injection-frameworks is dat de strikt gegenereerde implementatie (geen reflectie) betekent dat het kan worden gebruikt in Android-toepassingen. Er zijn echter nog een aantal overwegingen die moeten worden gemaakt bij het gebruik van Dagger binnen Android applicaties.

Filosofie

Hoewel code die is geschreven voor Android Java source is, is het vaak heel anders qua stijl. Typisch, dergelijke verschillen bestaan om de unieke prestaties overwegingen van een mobiel platform tegemoet te komen.

Maar veel van de patronen vaak toegepast op code bedoeld voor Android zijn strijdig met die toegepast op andere Java-code. Zelfs veel van het advies in Effective Java wordt beschouwd als ongeschikt voor Android.

Om de doelstellingen van zowel idiomatische en draagbare code te bereiken, Dagger vertrouwt op ProGuard om de gecompileerde bytecode post-processen. Hierdoor kan Dagger broncode genereren die er natuurlijk uitziet en aanvoelt op zowel de server als Android, terwijl de verschillende toolchains worden gebruikt om bytecode te produceren die in beide omgevingen efficiënt wordt uitgevoerd. Bovendien heeft Dagger een expliciet doel om ervoor te zorgen dat de Java broncode die het genereert consistent compatibel is metProGuard optimalisaties.

Natuurlijk kunnen niet alle problemen op die manier worden aangepakt, maar het is het primaire mechanisme waarmee Android-specifieke compatibiliteit zal worden geboden.

tl;dr

Dagger gaat ervan uit dat gebruikers op Android R8 of ProGuard gebruiken.

Waarom Dagger op Android moeilijk is

Een van de centrale problemen bij het schrijven van een Android applicatie met Dagger is dat veel Android framework classes door het OS zelf worden geïnstantieerd, zoalsActivity en Fragment, maar Dagger werkt het beste als het alle geinjecteerde objecten kan maken. In plaats daarvan moet je de leden injectie uitvoeren in een lifecycle-methode. Dit betekent dat veel klassen er uiteindelijk uitzien als:

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 }}

Dit heeft een paar problemen:

  1. Copy-pasting code maakt het moeilijk om later te refactoren. Naarmate meer en meer ontwikkelaars dat blok copy-pasten, zullen er minder weten wat het eigenlijk doet.

  2. Meer fundamenteel, het vereist dat het type dat om injectie vraagt (FrombulationActivity) de injector kent. Zelfs als dit wordt gedaan door middel van interfaces in plaats van concrete types, breekt het een kernprincipe van afhankelijkheidsinjectie: een klasse mag niets weten over hoe het wordt geïnjecteerd.

dagger.android

De klassen in dagger.android bieden een benadering om de bovenstaande problemen te vereenvoudigen. Dit vereist het leren van een aantal extra API’s en concepten, maar geeft u minder boilerplate en injectie in uw Android klassen op de juiste plaats in de levenscyclus.

Een andere aanpak is om gewoon gebruik maken van de normale Dagger API’s en gidsen te volgen, zoals degene hier.Dit kan eenvoudiger te begrijpen zijn, maar heeft als nadeel dat je handmatig extra boilerplate moet schrijven.

De Jetpack- en Dagger-teams werken samen aan een nieuw initiatief voor Dagger op Android, dat een grote verschuiving van de huidige status quo hoopt te zijn. Hoewel het helaas nog niet klaar is, kan dit iets zijn om te overwegen bij de keuze hoe Dagger te gebruiken in uw Android projecten van vandaag.

Injecteren van Activity objecten

  1. Installeer AndroidInjectionModule in uw applicatie component om ervoor te zorgen dat alle bindingen die nodig zijn voor deze basistypen beschikbaar zijn.

  2. Begin met het schrijven van een @Subcomponent dieAndroidInjector<YourActivity> implementeert, met een@Subcomponent.Factory dieAndroidInjector.Factory<YourActivity> uitbreidt:

    @Subcomponent(modules = ...)public interface YourActivitySubcomponent extends AndroidInjector<YourActivity> { @Subcomponent.Factory public interface Factory extends AndroidInjector.Factory<YourActivity> {}}
  3. Na het definiëren van de subcomponent, voeg het toe aan uw component hiërarchie door het definiëren van een module die de subcomponent fabriek bindt en toe te voegen aan decomponent die uw Application injecteert:

    @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: Als uw subcomponent en zijn fabriek geen andere methoden of supertypes hebben dan degene die in stap #2 zijn genoemd, kunt u@ContributesAndroidInjector gebruiken om ze voor u te genereren. In plaats van stap 2 en 3, voegt u een abstract module methode toe die uw activiteit teruggeeft, annoteer deze met @ContributesAndroidInjector, en specificeer de modules die u in de subcomponent wilt installeren. Als de subcomponent scopes nodig heeft, pas dan de scope annotaties ook toe op de methode.

    @ActivityScope@ContributesAndroidInjector(modules = { /* modules to install into the subcomponent */ })abstract YourActivity contributeYourAndroidInjector();
  4. Volgende, maak uw Application implementeer HasAndroidInjectoren @Inject eenDispatchingAndroidInjector<Object> die terugkeert van de androidInjector() methode:

    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. Ten slotte roept u in uw Activity.onCreate() methodeAndroidInjection.inject(this) aan voordat u super.onCreate(); aanroept:

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

Hoe ging dat in zijn werk?

AndroidInjection.inject() krijgt een DispatchingAndroidInjector<Object> van de Application en geeft uw activiteit door aan inject(Activity). DeDispatchingAndroidInjector zoekt de AndroidInjector.Factory voor de klasse van uw activiteit (die YourActivitySubcomponent.Factory is), maakt deAndroidInjector (die YourActivitySubcomponent is), en geeft uw activiteit door aan inject(YourActivity).

Fragmentobjecten injecteren

Een Fragment injecteren is net zo eenvoudig als een Activity injecteren. Definieer uw subcomponent op dezelfde manier.

In plaats van te injecteren in onCreate() zoals wordt gedaan voor Activitytypes, injecteer Fragments naar in onAttach().

In tegenstelling tot de modules gedefinieerd voor Activitys, heeft u een keuze van waar de modules voor Fragments te installeren. U kunt uw Fragment component maken als subcomponent van een andere Fragment component, een Activity component, of deApplication component – het hangt er allemaal vanaf welke andere bindingen uw Fragment nodig heeft. Nadat u de locatie van de component heeft bepaald, maakt u de bijbehorende type-implementatie HasAndroidInjector (als dat nog niet het geval is). Als uw Fragment bijvoorbeeld bindingen van YourActivitySubcomponent nodig heeft, ziet uw code er ongeveer zo uit:

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

Omdat DispatchingAndroidInjector bij runtime de juisteAndroidInjector.Factory door de klasse opzoekt, kan een basisklasse zowelHasAndroidInjector implementeren als AndroidInjection.inject() aanroepen. Het enige wat elke subklasse hoeft te doen is een overeenkomstige @Subcomponent te binden. Dagger biedt een paar basistypes die dit doen, zoals DaggerActivity en DaggerFragment, als u geen ingewikkelde klasse-hiërarchie hebt. Dagger biedt ook eenDaggerApplication voor hetzelfde doel – alles wat u hoeft te doen is het uit te breiden en de applicationInjector() methode te overschrijven om de component terug te geven die de Application moet injecteren.

De volgende types zijn ook opgenomen:

  • DaggerService en DaggerIntentService
  • DaggerBroadcastReceiver
  • DaggerContentProvider

Note: DaggerBroadcastReceiver moet alleen worden gebruikt wanneer deBroadcastReceiver in de AndroidManifest.xml is geregistreerd. Wanneer deBroadcastReceiver in uw eigen code wordt aangemaakt, geef dan de voorkeur aan constructor injectioninstead.

Ondersteuningsbibliotheken

Voor gebruikers van de Android-ondersteuningsbibliotheek bestaan er parallelle types in hetdagger.android.support-pakket.

TODO(ronshapiro): we moeten beginnen dit op te splitsen per androidx-pakket

Hoe kom ik eraan?

Voeg het volgende toe aan uw 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'}

Wanneer injecteren

Constructor-injectie heeft waar mogelijk de voorkeur omdat javac er dan voor zorgt dat er niet naar een veld wordt verwezen voordat het is ingesteld, watNullPointerExceptions helpt voorkomen. Wanneer injectie van leden nodig is (zoals hierboven besproken), injecteer dan bij voorkeur zo vroeg mogelijk. Om deze reden roept DaggerActivity AndroidInjection.inject() onmiddellijk aan in onCreate(), voordat super.onCreate() wordt aangeroepen, en DaggerFragment doet hetzelfde in onAttach(), wat ook inconsistenties voorkomt als Fragment opnieuw wordt aangehangen.

Het is van cruciaal belang om AndroidInjection.inject() aan te roepen vóór super.onCreate() in een Activity, omdat de aanroep van super Fragments vastmaakt van de vorige instantie tijdens het wijzigen van de configuratie, die op zijn beurt de Fragments injecteert. Om de Fragment injectie te laten slagen, moet de Activity reeds geïnjecteerd zijn. Voor gebruikers van ErrorProne is het een acompilerfout om AndroidInjection.inject() aan te roepen na super.onCreate().

FAQ

Scoping AndroidInjector.Factory

AndroidInjector.Factory is bedoeld als een stateless interface zodat implementeerders zich geen zorgen hoeven te maken over het beheren van state met betrekking tot het object dat zal worden geïnjecteerd. Wanneer DispatchingAndroidInjector eenAndroidInjector.Factory opvraagt, doet het dit via een Provider zodat het niet expliciet instanties van de fabriek vasthoudt. Omdat sommige implementaties een instantie kunnen bevatten van de Activity/Fragment/etc die wordt geïnjecteerd, is het een compile-time fout om een scope toe te passen op de methoden die deze verschaffen. Als u er zeker van bent dat uw AndroidInjector.Factory geen instantie van het geïnjecteerde object vasthoudt, kunt u deze fout onderdrukken door@SuppressWarnings("dagger.android.ScopedInjectorFactory") toe te passen op uw module-methode.