Dagger & Android
Einer der Hauptvorteile von Dagger 2 gegenüber den meisten anderen Dependency-Injection-Frameworks besteht darin, dass seine streng generierte Implementierung (keine Reflexion) bedeutet, dass es in Android-Anwendungen verwendet werden kann. Es gibt jedoch immer noch einige Überlegungen, die bei der Verwendung von Dagger in Android-Anwendungen angestellt werden müssen.
Philosophie
Während Code, der für Android geschrieben wurde, Java-Source ist, unterscheidet er sich oft in Bezug auf den Stil. Typischerweise bestehen solche Unterschiede, um den einzigartigen Leistungsüberlegungen einer mobilen Plattform Rechnung zu tragen.
Aber viele der Muster, die üblicherweise auf Code für Android angewendet werden, stehen im Gegensatz zu denen, die auf anderen Java-Code angewendet werden. Sogar viele der Ratschläge inEffective Java werden als unpassend für Android angesehen.
Um die Ziele eines idiomatischen und portablen Codes zu erreichen, verlässt sich Dagger auf ProGuard, um den kompilierten Bytecode nachzubearbeiten. Dies ermöglicht es Dagger, Quellcode zu erzeugen, der sowohl auf dem Server als auch auf Android natürlich aussieht und sich natürlich anfühlt, während die unterschiedlichen Toolchains genutzt werden, um Bytecode zu erzeugen, der in beiden Umgebungen effizient ausgeführt werden kann. Darüber hinaus hat Dagger das explizite Ziel, sicherzustellen, dass der von ihm generierte Java-Quellcode konsistent mit den Optimierungen von ProGuard kompatibel ist.
Natürlich können nicht alle Probleme auf diese Weise angegangen werden, aber es ist der primäre Mechanismus, mit dem Android-spezifische Kompatibilität bereitgestellt wird.
tl;dr
Dagger geht davon aus, dass Benutzer auf Android R8 oder ProGuard verwenden werden.
Warum Dagger auf Android schwierig ist
Eine der zentralen Schwierigkeiten beim Schreiben einer Android-Anwendung mit Dagger ist, dass viele Android-Framework-Klassen vom Betriebssystem selbst instanziiert werden, wieActivity
und Fragment
, aber Dagger funktioniert am besten, wenn es alle injizierten Objekte erstellen kann. Stattdessen müssen Sie die Injektion von Mitgliedern in einer Lifecycle-Methode durchführen. Das bedeutet, dass viele Klassen am Ende so aussehen:
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 }}
Das hat einige Probleme:
-
Das Kopieren von Code erschwert späteres Refactoring. Je mehr Entwickler diesen Block kopieren, desto weniger werden wissen, was er eigentlich tut.
-
Grundsätzlicher noch ist, dass der Typ, der die Injektion anfordert (
FrombulationActivity
), seinen Injektor kennen muss. Selbst wenn dies über Schnittstellen anstelle von konkreten Typen geschieht, bricht es mit einem Kernprinzip der Dependency Injection: eine Klasse sollte nichts darüber wissen, wie sie injiziert wird.
dagger.android
Die Klassen in dagger.android
bieten einen Ansatz, um die oben genannten Probleme zu vereinfachen. Dies erfordert das Erlernen einiger zusätzlicher APIs und Konzepte, aber Sie erhalten reduzierte Boilerplate und Injektion in Ihre Android-Klassen an der richtigen Stelle im Lebenszyklus.
Ein anderer Ansatz besteht darin, nur die normalen Dagger-APIs zu verwenden und Anleitungen wie die hier zu folgen.Dies mag einfacher zu verstehen sein, hat aber den Nachteil, dass man zusätzliche Boilerplate manuell schreiben muss.
Die Jetpack- und Dagger-Teams arbeiten gemeinsam an einer neuen Initiative für Dagger auf Android, die hoffentlich eine große Veränderung gegenüber dem derzeitigen Status quo darstellt. Während es leider noch nicht fertig ist, kann dies etwas sein, das Sie in Betracht ziehen sollten, wenn Sie entscheiden, wie Sie Dagger in Ihren Android-Projekten verwenden möchten.
Injecting Activity objects
-
Installieren Sie
AndroidInjectionModule
in Ihrer Anwendungskomponente, um sicherzustellen, dass alle Bindungen, die für diese Basistypen erforderlich sind, verfügbar sind. -
Schreiben Sie zunächst ein
@Subcomponent
, dasAndroidInjector<YourActivity>
implementiert, mit einem@Subcomponent.Factory
, dasAndroidInjector.Factory<YourActivity>
erweitert:@Subcomponent(modules = ...)public interface YourActivitySubcomponent extends AndroidInjector<YourActivity> { @Subcomponent.Factory public interface Factory extends AndroidInjector.Factory<YourActivity> {}}
-
Nachdem Sie die Unterkomponente definiert haben, fügen Sie sie zu Ihrer Komponentenhierarchie hinzu, indem Sie ein Modul definieren, das die Fabrik der Unterkomponente bindet, und es zu der Komponente hinzufügen, die Ihre
Application
injiziert:@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);}
Tipp: Wenn Ihre Unterkomponente und ihre Fabrik keine anderen Methoden oder Supertypen als die in Schritt 2 erwähnten haben, können Sie
@ContributesAndroidInjector
verwenden, um sie für Sie zu erzeugen. Anstelle der Schritte 2 und 3 fügen Sie eineabstract
Modulmethode hinzu, die Ihre Aktivität zurückgibt, kommentieren Sie sie mit@ContributesAndroidInjector
und geben Sie die Module an, die Sie in der Unterkomponente installieren möchten. Wenn die Unterkomponente Geltungsbereiche benötigt, wenden Sie die Geltungsbereichsannotationen auch auf die Methode an.@ActivityScope@ContributesAndroidInjector(modules = { /* modules to install into the subcomponent */ })abstract YourActivity contributeYourAndroidInjector();
-
Als Nächstes implementieren Sie
Application
HasAndroidInjector
und@Inject
einenDispatchingAndroidInjector<Object>
Rückgabewert derandroidInjector()
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; }}
-
Schließlich rufen Sie in Ihrer
Activity.onCreate()
-MethodeAndroidInjection.inject(this)
auf, bevor Siesuper.onCreate();
aufrufen:public class YourActivity extends Activity { public void onCreate(Bundle savedInstanceState) { AndroidInjection.inject(this); super.onCreate(savedInstanceState); }}
-
Glückwunsch!
Wie hat das funktioniert?
AndroidInjection.inject()
bekommt ein DispatchingAndroidInjector<Object>
vom Application
und gibt deine Aktivität an inject(Activity)
weiter. Das DispatchingAndroidInjector
sucht nach dem AndroidInjector.Factory
für die Klasse deiner Aktivität (die YourActivitySubcomponent.Factory
ist), erstellt das AndroidInjector
(das YourActivitySubcomponent
ist) und übergibt deine Aktivität an inject(YourActivity)
.
Injizieren von Fragmentobjekten
Das Injizieren eines Fragment
ist genauso einfach wie das Injizieren eines Activity
. Definieren Sie Ihre Unterkomponente auf die gleiche Weise.
Anstatt in onCreate()
zu injizieren, wie es für Activity
Typen gemacht wird, injizieren Sie Fragment
s in onAttach()
.
Im Gegensatz zu den Modulen, die für Activity
s definiert sind, haben Sie die Wahl, wo Sie Module für Fragment
s installieren. Sie können Ihre Fragment
-Komponente als Unterkomponente einer anderen Fragment
-Komponente, einer Activity
-Komponente oder der Application
-Komponente einrichten – es hängt alles davon ab, welche anderen Bindungen Ihre Fragment
-Komponente benötigt. Nachdem Sie sich für den Ort der Komponente entschieden haben, machen Sie das entsprechende Typeimplement HasAndroidInjector
(wenn es das nicht schon tut). Wenn Ihr Fragment
zum Beispiel Bindungen von YourActivitySubcomponent
benötigt, wird Ihr Code etwa so aussehen:
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
Da DispatchingAndroidInjector
zur Laufzeit das entsprechendeAndroidInjector.Factory
der Klasse nachschlägt, kann eine Basisklasse sowohl HasAndroidInjector
implementieren als auch AndroidInjection.inject()
aufrufen. Alles, was jede Unterklasse tun muss, ist, ein entsprechendes @Subcomponent
zu binden. Dagger bietet einige Basistypen, die dies tun, wie DaggerActivity
und DaggerFragment
, wenn Sie keine komplizierte Klassenhierarchie haben. Dagger bietet auch eine DaggerApplication
für den gleichen Zweck – alles, was Sie tun müssen, ist sie zu erweitern und die applicationInjector()
Methode zu überschreiben, um die Komponente zurückzugeben, die die Application
injizieren soll.
Die folgenden Typen sind ebenfalls enthalten:
-
DaggerService
undDaggerIntentService
DaggerBroadcastReceiver
DaggerContentProvider
Hinweis: DaggerBroadcastReceiver
sollte nur verwendet werden, wenn dasBroadcastReceiver
im AndroidManifest.xml
registriert ist. Wenn dasBroadcastReceiver
im eigenen Code erstellt wird, sollte man stattdessen die Konstruktorinjektion bevorzugen.
Unterstützungsbibliotheken
Für Benutzer der Android-Unterstützungsbibliothek gibt es parallele Typen im Paketdagger.android.support
.
TODO(ronshapiro): wir sollten anfangen, dies nach androidx-Paketen aufzuteilen
Wie bekomme ich es?
Fügen Sie folgendes zu Ihrer build.gradle hinzu:
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'}
Wann injizieren
Konstruktorinjektion wird, wann immer möglich, bevorzugt, weil javac
sichergestellt wird, dass kein Feld referenziert wird, bevor es gesetzt wurde, was hilft,NullPointerException
s zu vermeiden. Wenn die Injektion von Mitgliedern erforderlich ist (wie oben besprochen), ist es vorzuziehen, so früh wie möglich zu injizieren. Aus diesem Grund ruft DaggerActivity
AndroidInjection.inject()
sofort in onCreate()
auf, bevor es super.onCreate()
aufruft, und DaggerFragment
tut dasselbe in onAttach()
, was auch Inkonsistenzen verhindert, wenn Fragment
wieder angehängt wird.
Es ist wichtig, AndroidInjection.inject()
vor super.onCreate()
in einem Activity
aufzurufen, da der Aufruf von super
bei einer Konfigurationsänderung Fragment
s von der vorherigen Aktivitätsinstanz anhängt, die wiederum dieFragment
s injiziert. Damit die Fragment
-Injektion erfolgreich ist, muss die Activity
bereits injiziert sein. Für Benutzer von ErrorProne ist es ein Compiler-Fehler, AndroidInjection.inject()
nach super.onCreate()
aufzurufen.
FAQ
Scoping AndroidInjector.Factory
AndroidInjector.Factory
soll eine zustandslose Schnittstelle sein, so dass Implementierer sich nicht um die Verwaltung des Zustands des Objekts kümmern müssen, das injiziert wird. Wenn DispatchingAndroidInjector
einAndroidInjector.Factory
anfordert, tut es dies durch ein Provider
, so dass es nicht explizit irgendwelche Instanzen der Fabrik behält. Da einige Implementierungen eine Instanz der Activity
/Fragment
/etc, die injiziert wird, enthalten können, ist es ein Kompilierfehler, einen Bereich auf die Methoden anzuwenden, die sie bereitstellen. Wenn Sie sicher sind, dass Ihr AndroidInjector.Factory
keine Instanz des injizierten Objekts enthält, können Sie diesen Fehler unterdrücken, indem Sie@SuppressWarnings("dagger.android.ScopedInjectorFactory")
auf Ihre Modulmethode anwenden.