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:
-
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.
-
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
-
Installera
AndroidInjectionModule
i din applikationskomponent för att se till att alla bindningar som behövs för dessa bastyper är tillgängliga. -
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> {}}
-
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 enabstract
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();
-
Nästan gör din
Application
implementeraHasAndroidInjector
och@Inject
enDispatchingAndroidInjector<Object>
toreturn frånandroidInjector()
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; }}
-
Slutligt, i din
Activity.onCreate()
metod, anropar duAndroidInjection.inject(this)
innan du anroparsuper.onCreate();
:public class YourActivity extends Activity { public void onCreate(Bundle savedInstanceState) { AndroidInjection.inject(this); super.onCreate(savedInstanceState); }}
-
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 Activity
typer, injicera Fragment
s till i onAttach()
.
I motsats till de moduler som definierats för Activity
s har du ett val av var du vill installera moduler för Fragment
s. 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
ochDaggerIntentService
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 undvikaNullPointerException
s. 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 Fragment
s från den föregående aktivitetsinstansen under konfigurationsändringen, som i sin tur injicerar Fragment
s. 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.