Dagger & Android

En av de främsta fördelarna med Dagger 2 jämfört med de flesta andra ramverk för injektion av beroenden är att dess strikt genererade implementering (ingen reflektion) innebär att det kan användas i Android-program. Det finns dock fortfarande några överväganden att göra när Dagger används i Android-applikationer.

Filosofi

Samtidigt som kod som skrivs för Android är Java-källkod, är den ofta ganska annorlunda när det gäller stilen. Typiskt sett finns sådana skillnader för att tillgodose de unika prestandaövervägandena för en mobil plattform.

Men många av de mönster som vanligen tillämpas på kod som är avsedd för Android skiljer sig från dem som tillämpas på annan Javakod. Till och med många av råden iEffective Java anses vara olämpliga för Android.

För att uppnå målen med både idiomatisk och portabel kod förlitar sig Dagger på ProGuard för att efterbehandla den kompilerade bytekoden. Detta gör det möjligt för Dagger att skapa källkod som ser ut och känns naturlig på både server och Android, samtidigt som de olika verktygskedjorna används för att producera bytekod som körs effektivt i båda miljöerna. Dessutom har Dagger ett uttryckligt mål att se till att den Java-källa som genereras är konsekvent kompatibel med ProGuard-optimeringar.

Naturligtvis kan inte alla problem lösas på detta sätt, men det är den primära mekanismen genom vilken Android-specifik kompatibilitet kommer att tillhandahållas.

tl;dr

Dagger förutsätter att användare på Android kommer att använda R8 eller ProGuard.

Varför Dagger på Android är svårt

En av de centrala svårigheterna när det gäller att skriva en Android-applikation med Dagger är att många av Android-ramverkets klasser instansieras av operativsystemet självt, somActivity och Fragment, men Dagger fungerar bäst om den kan skapa alla de injicerade objekten. Istället måste du utföra members injection i en livscykelmetod. Detta innebär att många klasser slutar med att se ut som:

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

Detta har några problem:

  1. Kopiering av kod gör det svårt att refaktorisera senare. När fler och fler utvecklare kopierar och klistrar in det blocket kommer färre att veta vad det faktiskt gör.

  2. Mer grundläggande är att det kräver att den typ som begär injektion (FrombulationActivity) känner till sin injektor. Även om detta görs genom gränssnitt istället för konkreta typer bryter det mot en kärnprincip för injektion av beroenden: en klass ska inte veta något om hur den injiceras.

dagger.android

Klasserna i dagger.android erbjuder ett tillvägagångssätt för att förenkla ovanstående problem. Detta kräver att man lär sig några extra API:er och begrepp, men ger en minskad boilerplate och injektion i dina Android-klasser på rätt ställe i livscykeln.

En annan metod är att bara använda de normala Dagger-API:erna och följa guider som den som finns här.Detta kan vara enklare att förstå men har nackdelen att man måste skriva extra boilerplate manuellt.

Jetpack- och Dagger-teamen arbetar tillsammans på ett nytt initiativ för Dagger på Android som förhoppningsvis kommer att innebära ett stort skifte från den nuvarande status quo. Även om det tyvärr inte är färdigt ännu kan detta vara något att tänka på när du väljer hur du ska använda Dagger i dina Android-projekt idag.

Injicera Activity-objekt

  1. Installera AndroidInjectionModule i din applikationskomponent för att se till att alla bindningar som behövs för dessa bastyper är tillgängliga.

  2. Börja med att skriva en @Subcomponent som implementerarAndroidInjector<YourActivity>, med en@Subcomponent.Factory som utökarAndroidInjector.Factory<YourActivity>:

    @Subcomponent(modules = ...)public interface YourActivitySubcomponent extends AndroidInjector<YourActivity> { @Subcomponent.Factory public interface Factory extends AndroidInjector.Factory<YourActivity> {}}
  3. När du har definierat underkomponenten lägger du till den i din komponenthierarki genom att definiera en modul som binder underkomponentfabriken och lägger till den i den komponent som injicerar din 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-tips: Om din underkomponent och dess fabrik inte har några andra metoder eller supertyper än de som nämns i steg 2 kan du använda @ContributesAndroidInjector för att generera dem åt dig. Istället för steg 2 och 3 lägger du till en abstract modulmetod som returnerar din aktivitet, kommenterar den med @ContributesAndroidInjector och anger de moduler som du vill installera i underkomponenten. Om underkomponenten behöver scopes tillämpar du även dessascope-annotationer på metoden.

    @ActivityScope@ContributesAndroidInjector(modules = { /* modules to install into the subcomponent */ })abstract YourActivity contributeYourAndroidInjector();
  4. Nästan gör din Application implementera HasAndroidInjectoroch @Inject enDispatchingAndroidInjector<Object> toreturn från androidInjector() metoden:

    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. Slutligt, i din Activity.onCreate() metod, anropar duAndroidInjection.inject(this) innan du anropar super.onCreate();:

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

Hur fungerade det?

AndroidInjection.inject() får en DispatchingAndroidInjector<Object> från Application och skickar din aktivitet till inject(Activity). DispatchingAndroidInjector letar upp AndroidInjector.Factory för din aktivitets klass (som är YourActivitySubcomponent.Factory), skapar AndroidInjector (som är YourActivitySubcomponent) och skickar din aktivitet till inject(YourActivity).

Injicera Fragmentobjekt

Injicera en Fragment är lika enkelt som att injicera en Activity. Definiera din underkomponent på samma sätt.

Istället för att injicera i onCreate() som görs för Activitytyper, injicera Fragments till i onAttach().

I motsats till de moduler som definierats för Activitys har du ett val av var du vill installera moduler för Fragments. Du kan göra din Fragment-komponent till en underkomponent till en annan Fragment-komponent, en Activity-komponent eller Application-komponenten – allt beror på vilka andra bindningar din Fragment kräver. När du har bestämt dig för komponentens placering gör du motsvarande typeimplement HasAndroidInjector (om den inte redan gör det). Om din Fragment till exempel behöver bindningar från YourActivitySubcomponent kommer din kod att se ut ungefär så här:

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

Om DispatchingAndroidInjector letar upp lämpligAndroidInjector.Factory av klassen vid körning, kan en basklass implementeraHasAndroidInjector såväl som anropa AndroidInjection.inject(). Allt varje underklass behöver göra är att binda en motsvarande @Subcomponent. Dagger tillhandahåller några bastyper som gör detta, till exempel DaggerActivity och DaggerFragment,om du inte har en komplicerad klasshierarki. Dagger tillhandahåller också en DaggerApplication för samma ändamål – allt du behöver göra är att utöka den och överskriva applicationInjector()-metoden för att returnera den komponent som ska injicera Application.

Följande typer ingår också:

  • DaggerService och DaggerIntentService
  • DaggerBroadcastReceiver
  • DaggerContentProvider

Notera: DaggerBroadcastReceiver bör endast användas närBroadcastReceiver är registrerad i AndroidManifest.xml. NärBroadcastReceiver skapas i din egen kod, föredrar du konstruktörsinjektion i stället.

Stödbibliotek

För användare av Android-stödbiblioteket finns parallella typer i paketetdagger.android.support.

TODO(ronshapiro): Vi bör börja dela upp det här efter androidx-paket

Hur får jag tag på det?

Lägg till följande i din 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'}

När man ska injicera

Injicering av konstruktör är att föredra när det är möjligt, eftersom javac kommer att säkerställa att inget fält refereras innan det har satts, vilket hjälper till att undvikaNullPointerExceptions. När injektion av medlemmar krävs (som diskuteras ovan), föredrar du att injicera så tidigt som möjligt. Av denna anledning kallar DaggerActivity AndroidInjection.inject() omedelbart i onCreate(), innan super.onCreate() anropas, och DaggerFragment gör detsamma i onAttach(), vilket också förhindrar inkonsekvenser om Fragment återkopplas.

Det är viktigt att anropa AndroidInjection.inject() före super.onCreate() i en Activity, eftersom anropet till super bifogar Fragments från den föregående aktivitetsinstansen under konfigurationsändringen, som i sin tur injicerar Fragments. För att Fragment-injektionen ska lyckas måste Activity redan vara injicerad. För användare av ErrorProne är det ett kompilerfel att anropa AndroidInjection.inject() efter super.onCreate().

FAQ

Scoping AndroidInjector.Factory

AndroidInjector.Factory är tänkt att vara ett tillståndslöst gränssnitt så att implementerare inte behöver oroa sig för att hantera tillstånd relaterat till objektet som ska injiceras. När DispatchingAndroidInjector begär en AndroidInjector.Factory gör den det genom en Provider så att den inte explicit behåller några instanser av fabriken. Eftersom vissa implementationer kan behålla en instans av Activity/Fragment/etc som injiceras, är det ett kompileringsfel att tillämpa ett scope på de metoder som tillhandahåller dem. Om du är säker på att din AndroidInjector.Factory inte behåller en instans av det injicerade objektet, kan du undanröja detta fel genom att tillämpa@SuppressWarnings("dagger.android.ScopedInjectorFactory") på din modulmetod.