Ich mache Sie auf eine Übersetzung des Originalartikels von Jamie Sanson aufmerksam

Aktivitäten vor Android 9 Pie erstellen
Die Abhängigkeitsinjektion (DI) ist ein gängiges Modell, das aus verschiedenen Gründen in allen Entwicklungsformen verwendet wird. Dank des Dagger-Projekts wird es als Vorlage für die Entwicklung für Android verwendet. Die jüngsten Änderungen in Android 9 Pie haben dazu geführt, dass wir jetzt mehr Optionen für DI haben, insbesondere mit der neuen AppComponentFactory
Klasse.
DI ist sehr wichtig für die moderne Android-Entwicklung. Auf diese Weise können Sie die Gesamtmenge an Code reduzieren, wenn Sie Links zu Diensten erhalten, die zwischen Klassen verwendet werden, und die Anwendung im Allgemeinen gut in Komponenten unterteilen. In diesem Artikel konzentrieren wir uns auf Dagger 2, die in der Android-Entwicklung am häufigsten verwendete DI-Bibliothek. Es wird davon ausgegangen, dass Sie bereits Grundkenntnisse darüber haben, wie dies funktioniert, aber es ist nicht erforderlich, alle Feinheiten zu verstehen. Es ist erwähnenswert, dass dieser Artikel ein kleines Abenteuer ist. Das ist interessant und alles, aber zum Zeitpunkt des Schreibens erschien Android 9 Pie noch nicht einmal im Plattformversionsfenster , sodass dieses Thema wahrscheinlich für mindestens einige Jahre nicht für die alltägliche Entwicklung relevant sein wird.
Abhängigkeitsinjektion in Android heute
Einfach ausgedrückt, verwenden wir DI, um unseren abhängigen Klassen, dh denjenigen, die die Arbeit erledigen, Instanzen von Abhängigkeitsklassen bereitzustellen. Angenommen, wir verwenden das Repository-Muster , um unsere datenbezogene Logik zu verarbeiten, und möchten unser Repository in Aktivität verwenden, um dem Benutzer einige Daten anzuzeigen. Möglicherweise möchten wir dasselbe Repository an mehreren Stellen verwenden. Daher verwenden wir die Abhängigkeitsinjektion, um die gemeinsame Nutzung derselben Instanz zwischen mehreren Klassen zu vereinfachen.
Zunächst stellen wir ein Repository bereit. Wir werden die Provides
Funktion im Modul definieren, damit Dagger weiß, dass dies genau die Instanz ist, die wir implementieren möchten. Bitte beachten Sie, dass unser Repository eine Kontextinstanz für die Arbeit mit Dateien und dem Netzwerk benötigt. Wir werden es mit dem Anwendungskontext versehen.
@Module class AppModule(val appContext: Context) { @Provides @ApplicationScope fun provideApplicationContext(): Context = appContext @Provides @ApplicationScope fun provideRepository(context: Context): Repository = Repository(context) }
Jetzt müssen wir Component
definieren, um die Implementierung der Klassen zu handhaben, in denen wir unser Repository
möchten.
@ApplicationScope @Component(modules = [AppModule::class]) interface ApplicationComponent { fun inject(activity: MainActivity) }
Schließlich können wir unsere Activity
so konfigurieren, dass sie unser Repository verwendet. Angenommen, wir haben eine Instanz unserer ApplicationComponent
anderen Stelle erstellt.
class MainActivity: AppCompatActivity() { @Inject lateinit var repository: Repository override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
Das ist alles! Wir haben gerade die Abhängigkeitsinjektion innerhalb der Anwendung mit Dagger eingerichtet. Es gibt verschiedene Möglichkeiten, dies zu tun, aber dies scheint der einfachste Ansatz zu sein.
Was ist falsch an dem aktuellen Ansatz?
In den obigen Beispielen haben wir zwei verschiedene Arten von Injektionen gesehen, eine offensichtlicher als die andere.
Das erste, das Sie möglicherweise übersehen haben, wird als Einbettung in den Konstruktor bezeichnet . Dies ist eine Methode zum Bereitstellen von Abhängigkeiten über den Konstruktor einer Klasse. Dies bedeutet, dass eine Klasse, die Abhängigkeiten verwendet, keine Ahnung vom Ursprung der Instanzen hat. Dies wird als die reinste Form der Abhängigkeitsinjektion angesehen, da sie unsere Injektionslogik perfekt in unsere Module
einkapselt. In unserem Beispiel haben wir diesen Ansatz verwendet, um ein Repository bereitzustellen:
fun provideRepository(context: Context): Repository = Repository(context)
Dafür brauchten wir Context
, den wir in der Funktion provideApplicationContext()
bereitgestellt haben.
Das zweite, offensichtlichere, was wir gesehen haben, ist die Implementierung der Klasse vor Ort . Diese Methode wurde in unserer MainActivity
, um unser Geschäft bereitzustellen. Hier definieren wir die Felder als Empfänger der Injektionen unter Verwendung der Inject
Annotation. Anschließend onCreate
wir ApplicationComponent
in unserer Funktion onCreate
dass Abhängigkeiten in unsere Felder onCreate
werden müssen. Es sieht nicht so sauber aus wie das Einbetten in einen Konstruktor, da wir einen expliziten Verweis auf unsere Komponente haben, was bedeutet, dass das Konzept des Einbetten in unsere abhängigen Klassen eindringt. Ein weiterer Fehler in den Android Framework-Klassen, da wir sicher sein müssen, dass wir als erstes Abhängigkeiten bereitstellen. Wenn dies zum falschen Zeitpunkt im Lebenszyklus geschieht, versuchen wir möglicherweise versehentlich, ein Objekt zu verwenden, das noch nicht initialisiert wurde.
Idealerweise sollten Sie die Implementierungen in den Klassenfeldern vollständig entfernen. Dieser Ansatz überspringt Implementierungsinformationen für Klassen, die nichts darüber wissen müssen, und kann möglicherweise Lebenszyklusprobleme verursachen. Wir haben Versuche gesehen, es besser zu machen, und Dagger auf Android ist ein ziemlich zuverlässiger Weg, aber am Ende wäre es besser, wenn wir nur die Konstruktorinjektion verwenden könnten. Derzeit können wir diesen Ansatz nicht für eine Reihe von Framework-Klassen verwenden, z. B. "Aktivität", "Service", "Anwendung" usw., da diese vom System für uns erstellt werden. Es scheint, dass wir im Moment nicht in der Lage sind, Klassen in Felder einzuführen. Trotzdem bereitet Android 9 Pie etwas Interessantes vor, das vielleicht alles grundlegend verändern wird.
Abhängigkeitsinjektion in Android 9 Pie
Wie am Anfang des Artikels erwähnt, verfügt Android 9 Pie über eine AppComponentFactory-Klasse. Die Dokumentation dafür ist eher knapp und wird einfach als solche auf der Entwickler-Website veröffentlicht:
Die Schnittstelle zur Steuerung der Erstellung von Manifestelementen.
Es ist faszinierend. "Manifest-Elemente" bezieht sich hier auf die Klassen, die wir in unserer AndroidManifest
Datei AndroidManifest
, z. B. Aktivität, Dienst und unsere Anwendungsklasse. Auf diese Weise können wir die Erstellung dieser Elemente "steuern". Können wir nun die Regeln für die Erstellung unserer Aktivitäten festlegen? Was für eine Freude!
Lassen Sie uns tiefer graben. AppComponentFactory
erweitern wir die AppComponentFactory
und überschreiben die instantiateActivity
Methode.
class InjectionComponentFactory: AppComponentFactory() { private val repository = NonContextRepository() override fun instantiateActivity(cl: ClassLoader, className: String, intent: Intent?): Activity { return when { className == MainActivity::class.java.name -> MainActivity(repository) else -> super.instantiateActivity(cl, className, intent) } } }
Jetzt müssen wir unsere Komponentenfactory im Manifest innerhalb des Anwendungs- Tags deklarieren.
<application android:allowBackup="true" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:name=".InjectionApp" android:appComponentFactory="com.mypackage.injectiontest.component.InjectionComponentFactory" android:theme="@style/AppTheme" tools:replace="android:appComponentFactory">
Endlich können wir unsere Anwendung starten ... und es funktioniert! Unser NonContextRepository
über den MainActivity-Konstruktor bereitgestellt. Anmutig!
Bitte beachten Sie, dass es einige Vorbehalte gibt. Wir können Context
hier nicht verwenden, da bereits vor seiner Existenz ein Aufruf unserer Funktion erfolgt - das ist verwirrend! Wir können noch weiter gehen, damit der Konstruktor unsere Anwendungsklasse implementiert, aber lassen Sie uns sehen, wie Dagger dies noch einfacher machen kann.
Treffen Sie - Dolch Multi-Binds
Ich werde nicht auf die Details des Dolch-Mehrfachbindungsvorgangs unter der Haube eingehen, da dies den Rahmen dieses Artikels sprengt. Alles, was Sie wissen müssen, ist, dass es eine gute Möglichkeit bietet, in den Klassenkonstruktor einzufügen, ohne den Konstruktor manuell aufrufen zu müssen. Wir können dies verwenden, um Framework-Klassen auf skalierbare Weise einfach zu implementieren. Mal sehen, wie sich alles summiert.
Lassen Sie uns zuerst unsere Aktivität einrichten, um herauszufinden, wohin wir als nächstes gehen sollen.
class MainActivity @Inject constructor( private val repository: NonContextRepository ): Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
Dies zeigt sofort, dass die Abhängigkeitsinjektion fast nicht erwähnt wird. Das einzige, was wir sehen, ist die Inject
Annotation vor dem Konstruktor.
Jetzt müssen Sie die Komponente und das Dolchmodul ändern:
@Component(modules = [ApplicationModule::class]) interface ApplicationComponent { fun inject(factory: InjectionComponentFactory) }
@Module(includes = [ComponentModule::class]) class ApplicationModule { @Provides fun provideRepository(): NonContextRepository = NonContextRepository() }
Es hat sich nicht viel geändert. Jetzt müssen wir nur noch unsere Komponentenfabrik implementieren, aber wie erstellen wir unsere Manifestelemente? Hier brauchen wir ein ComponentModule
. Mal sehen:
@Module abstract class ComponentModule { @Binds @IntoMap @ComponentKey(MainActivity::class) abstract fun bindMainActivity(activity: MainActivity): Any @Binds abstract fun bindComponentHelper(componentHelper: ComponentHelper): ComponentInstanceHelper } @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) @Retention(AnnotationRetention.RUNTIME) @MapKey internal annotation class ComponentKey(val clazz: KClass<out Any>)
Ja, nur ein paar Anmerkungen. Hier verbinden wir unsere Activity
mit einer Map, implementieren diese Map in unsere ComponentHelper
Klasse und stellen diesen ComponentHelper
bereit - alles in zwei Binds
Anweisungen. Dagger weiß dank der MainActivity
Annotation Inject
wie er unsere MainActivity
instanziiert Inject
sodass er den Provider an diese Klasse binden und automatisch die Abhängigkeiten bereitstellen kann, die wir für den Konstruktor benötigen. Unser ComponentHelper
wie folgt.
class ComponentHelper @Inject constructor( private val creators: Map<Class<out Any>, @JvmSuppressWildcards Provider<Any>> ): ComponentInstanceHelper { @Suppress("UNCHECKED_CAST") override fun <T> resolve(className: String): T? = creators .filter { it.key.name == className } .values .firstOrNull() ?.get() as? T } interface InstanceComponentHelper { fun <T> resolve(className: String): T? }
Einfach ausgedrückt, wir haben jetzt eine Klassenübersicht für Lieferanten für diese Klassen. Wenn wir versuchen, eine Klasse nach Namen aufzulösen, suchen wir einfach den Anbieter für diese Klasse (falls vorhanden), rufen ihn auf, um eine neue Instanz dieser Klasse abzurufen, und geben ihn zurück.
Schließlich müssen wir Änderungen an unserer AppComponentFactory
, um unsere neue AppComponentFactory
verwenden zu können.
class InjectionComponentFactory: AppComponentFactory() { @Inject lateinit var componentHelper: ComponentInstanceHelper init { DaggerApplicationComponent.create().inject(this) } override fun instantiateActivity(cl: ClassLoader, className: String, intent: Intent?): Activity { return componentHelper .resolve<Activity>(className) ?.apply { setIntent(intent) } ?: super.instantiateActivity(cl, className, intent) } }
Führen Sie den Code erneut aus. Es funktioniert alles! Was für eine Freude.
Probleme bei der Implementierung des Konstruktors
Ein solcher Titel mag nicht sehr beeindruckend aussehen. Obwohl wir die meisten Instanzen im normalen Modus einbetten können, indem wir sie in den Konstruktor einfügen, haben wir keine offensichtliche Möglichkeit, den Kontext für unsere Abhängigkeiten auf standardmäßige Weise bereitzustellen. Aber der Context
in Android ist alles. Es wird für den Zugriff auf Einstellungen, Netzwerk, Anwendungskonfiguration und vieles mehr benötigt. Bei unseren Abhängigkeiten handelt es sich häufig um Dinge, die datenbezogene Dienste wie Netzwerk und Einstellungen verwenden. Wir können dies umgehen, indem wir unsere Abhängigkeiten in reine Funktionen umschreiben oder alles mit Kontextinstanzen in unserer Application
initialisieren. Es ist jedoch viel mehr Arbeit erforderlich, um den besten Weg zu finden, dies zu tun.
Ein weiterer Nachteil dieses Ansatzes ist die Definition des Anwendungsbereichs. In Dagger ist eines der Schlüsselkonzepte für die Implementierung einer Hochleistungs-Abhängigkeitsinjektion mit einer guten Trennung der Klassenbeziehungen die Modularität des Objektgraphen und die Verwendung des Bereichs. Obwohl dieser Ansatz die Verwendung von Modulen nicht verbietet, schränkt er die Verwendung des Anwendungsbereichs ein. AppComponentFactory
befindet sich auf einer völlig anderen Abstraktionsebene als unsere Standard-Framework-Klassen. Wir können keinen programmgesteuerten Link dazu erhalten, sodass wir keine Anweisung haben, Abhängigkeiten für Activity
in einem anderen Bereich bereitzustellen.
Es gibt viele Möglichkeiten, unsere Probleme mit Bereichen in der Praxis zu lösen. Eine davon ist die Verwendung einer FragmentFactory
, um unsere Fragmente in einen Konstruktor mit Bereichen einzubetten. Ich werde nicht auf Details eingehen, aber es stellt sich heraus, dass wir jetzt eine Methode zur Steuerung der Erstellung von Fragmenten haben, die uns nicht nur viel mehr Spielraum gibt, sondern auch Abwärtskompatibilität bietet.
Fazit
Android 9 Pie hat eine Möglichkeit eingeführt, die Einbettung in den Konstruktor zu verwenden, um Abhängigkeiten in unseren Framework-Klassen wie "Aktivität" und "Anwendung" bereitzustellen. Wir haben gesehen, dass wir mit Dagger Multi-Binding problemlos Abhängigkeiten auf Anwendungsebene bereitstellen können.
Ein Konstruktor, der alle unsere Komponenten implementiert, ist äußerst attraktiv, und wir können sogar etwas tun, damit er mit Kontextinstanzen ordnungsgemäß funktioniert. Dies ist eine vielversprechende Zukunft, die jedoch erst ab API 28 verfügbar ist. Wenn Sie weniger als 0,5% der Benutzer erreichen möchten, können Sie es versuchen. Andernfalls sollten Sie abwarten, ob eine solche Methode in einigen Jahren relevant bleibt.