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:

  1. Das Kopieren von Code erschwert späteres Refactoring. Je mehr Entwickler diesen Block kopieren, desto weniger werden wissen, was er eigentlich tut.

  2. 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

  1. Installieren Sie AndroidInjectionModule in Ihrer Anwendungskomponente, um sicherzustellen, dass alle Bindungen, die für diese Basistypen erforderlich sind, verfügbar sind.

  2. Schreiben Sie zunächst ein @Subcomponent, das AndroidInjector<YourActivity> implementiert, mit einem @Subcomponent.Factory, das AndroidInjector.Factory<YourActivity> erweitert:

    @Subcomponent(modules = ...)public interface YourActivitySubcomponent extends AndroidInjector<YourActivity> { @Subcomponent.Factory public interface Factory extends AndroidInjector.Factory<YourActivity> {}}
  3. 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 eine abstract 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();
  4. Als Nächstes implementieren Sie Application HasAndroidInjectorund @Inject einenDispatchingAndroidInjector<Object>Rückgabewert der 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. Schließlich rufen Sie in Ihrer Activity.onCreate()-Methode AndroidInjection.inject(this)auf, bevor Sie super.onCreate(); aufrufen:

    public class YourActivity extends Activity { public void onCreate(Bundle savedInstanceState) { AndroidInjection.inject(this); super.onCreate(savedInstanceState); }}
  6. 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 ActivityTypen gemacht wird, injizieren Sie Fragments in onAttach().

Im Gegensatz zu den Modulen, die für Activitys definiert sind, haben Sie die Wahl, wo Sie Module für Fragments 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 und DaggerIntentService
  • 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,NullPointerExceptions 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 Fragments von der vorherigen Aktivitätsinstanz anhängt, die wiederum dieFragments 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.