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:
-
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.
-
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
-
Installeer
AndroidInjectionModule
in uw applicatie component om ervoor te zorgen dat alle bindingen die nodig zijn voor deze basistypen beschikbaar zijn. -
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> {}}
-
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 eenabstract
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();
-
Volgende, maak uw
Application
implementeerHasAndroidInjector
en@Inject
eenDispatchingAndroidInjector<Object>
die terugkeert van deandroidInjector()
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; }}
-
Ten slotte roept u in uw
Activity.onCreate()
methodeAndroidInjection.inject(this)
aan voordat usuper.onCreate();
aanroept:public class YourActivity extends Activity { public void onCreate(Bundle savedInstanceState) { AndroidInjection.inject(this); super.onCreate(savedInstanceState); }}
-
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 Activity
types, injecteer Fragment
s naar in onAttach()
.
In tegenstelling tot de modules gedefinieerd voor Activity
s, heeft u een keuze van waar de modules voor Fragment
s 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
enDaggerIntentService
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, watNullPointerException
s 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
Fragment
s vastmaakt van de vorige instantie tijdens het wijzigen van de configuratie, die op zijn beurt de Fragment
s 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.