Verwenden Sie Car UI-Bibliotheks -Plugins , um vollständige Implementierungen von Komponentenanpassungen in der Car UI-Bibliothek zu erstellen, anstatt Runtime Resource Overlays (RROs) zu verwenden. Mit RROs können Sie nur die XML-Ressourcen von Car UI-Bibliothekskomponenten ändern, wodurch der Umfang Ihrer Anpassungsmöglichkeiten eingeschränkt wird.
Erstellen Sie ein Plugin
Ein Car UI-Bibliotheks-Plugin ist ein APK, das Klassen enthält, die eine Reihe von Plugin-APIs implementieren. Die Plugin-APIs können als statische Bibliothek in ein Plugin kompiliert werden.
Siehe Beispiele in Soong und Gradle:
Bald
Betrachten Sie dieses Soong-Beispiel:
android_app {
name: "my-plugin",
min_sdk_version: "28",
target_sdk_version: "30",
aaptflags: ["--shared-lib"],
sdk_version: "current",
manifest: "src/main/AndroidManifest.xml",
srcs: ["src/main/java/**/*.java"],
resource_dirs: ["src/main/res"],
static_libs: [
"car-ui-lib-oem-apis",
],
// Disable optimization is mandatory to prevent R.java class from being
// stripped out
optimize: {
enabled: false,
},
certificate: ":my-plugin-certificate",
}
Gradle
Sehen Sie sich diese build.gradle
-Datei an:
apply plugin: 'com.android.application'
android {
compileSdkVersion 30
defaultConfig {
minSdkVersion 28
targetSdkVersion 30
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
signingConfigs {
debug {
storeFile file('chassis_upload_key.jks')
storePassword 'chassis'
keyAlias 'chassis'
keyPassword 'chassis'
}
}
}
dependencies {
implementation project(':oem-apis')
// Or use the following if you'd like to use the maven artifact
// implementation 'com.android.car.ui:car-ui-lib-plugin-apis:1.0.0'
}
Settings.gradle
:
// You can remove the ':oem-apis' if you're using the maven artifact.
include ':oem-apis'
project(':oem-apis').projectDir = new File('./path/to/oem-apis')
include ':my-plugin'
project(':my-plugin').projectDir = new File('./my-plugin')
Das Plugin muss in seinem Manifest einen Inhaltsanbieter deklariert haben, der die folgenden Attribute aufweist:
android:authorities="com.android.car.ui.plugin"
android:enabled="true"
android:exported="true"
android:authorities="com.android.car.ui.plugin"
macht das Plugin für die Car UI-Bibliothek erkennbar. Der Anbieter muss exportiert werden, damit er zur Laufzeit abgefragt werden kann. Wenn das Attribut enabled
auf false
gesetzt ist, wird außerdem die Standardimplementierung anstelle der Plugin-Implementierung verwendet. Die Inhaltsanbieterklasse muss nicht vorhanden sein. Fügen Sie in diesem Fall unbedingt tools:ignore="MissingClass"
zur Anbieterdefinition hinzu. Sehen Sie sich den Beispielmanifesteintrag unten an:
<application>
<provider
android:name="com.android.car.ui.plugin.PluginNameProvider"
android:authorities="com.android.car.ui.plugin"
android:enabled="false"
android:exported="true"
tools:ignore="MissingClass"/>
</application>
Als Sicherheitsmaßnahme signieren Sie abschließend Ihre App .
Plugins als gemeinsam genutzte Bibliothek
Im Gegensatz zu statischen Android-Bibliotheken, die direkt in Apps kompiliert werden, werden gemeinsam genutzte Android-Bibliotheken in ein eigenständiges APK kompiliert, auf das andere Apps zur Laufzeit verweisen.
Bei Plugins, die als gemeinsam genutzte Android-Bibliothek implementiert sind, werden die Klassen automatisch zum gemeinsam genutzten Klassenlader zwischen Apps hinzugefügt. Wenn eine App, die die Car UI-Bibliothek verwendet, eine Laufzeitabhängigkeit von der gemeinsam genutzten Plugin-Bibliothek angibt, kann ihr Klassenlader auf die Klassen der gemeinsam genutzten Plugin-Bibliothek zugreifen. Als normale Android-Apps implementierte Plugins (keine gemeinsam genutzte Bibliothek) können sich negativ auf die Kaltstartzeiten von Apps auswirken.
Implementieren und erstellen Sie gemeinsam genutzte Bibliotheken
Die Entwicklung mit gemeinsam genutzten Android-Bibliotheken ähnelt mit einigen wesentlichen Unterschieden weitgehend der Entwicklung normaler Android-Apps.
- Verwenden Sie das
library
-Tag unter demapplication
-Tag mit dem Plugin-Paketnamen im App-Manifest Ihres Plugins:
<application>
<library android:name="com.chassis.car.ui.plugin" />
...
</application>
- Konfigurieren Sie Ihre Soong
android_app
Build-Regel (Android.bp
) mit dem AAPT-Flagshared-lib
, das zum Erstellen einer gemeinsam genutzten Bibliothek verwendet wird:
android_app {
...
aaptflags: ["--shared-lib"],
...
}
Abhängigkeiten von gemeinsam genutzten Bibliotheken
Fügen Sie für jede App auf dem System, die die Car UI-Bibliothek verwendet, das Tag uses-library
in das App-Manifest unter dem Tag „ application
“ mit dem Namen des Plugin-Pakets ein:
<manifest>
<application
android:name=".MyApp"
...>
<uses-library android:name="com.chassis.car.ui.plugin" android:required="false"/>
...
</application>
</manifest>
Installieren Sie ein Plugin
Plugins MÜSSEN auf der Systempartition vorinstalliert werden, indem das Modul in PRODUCT_PACKAGES
aufgenommen wird. Das vorinstallierte Paket kann wie jede andere installierte App aktualisiert werden.
Wenn Sie ein vorhandenes Plugin auf dem System aktualisieren, werden alle Apps, die dieses Plugin verwenden, automatisch geschlossen. Sobald der Benutzer sie erneut öffnet, verfügen sie über die aktualisierten Änderungen. Wenn die App nicht ausgeführt wurde, verfügt sie beim nächsten Start über das aktualisierte Plugin.
Bei der Installation eines Plugins mit Android Studio müssen einige zusätzliche Überlegungen berücksichtigt werden. Zum Zeitpunkt des Schreibens gibt es einen Fehler im Installationsprozess der Android Studio-App, der dazu führt, dass Aktualisierungen eines Plugins nicht wirksam werden. Dies kann durch Auswahl der Option „Immer mit Paketmanager installieren“ (deaktiviert Bereitstellungsoptimierungen auf Android 11 und höher) in der Build-Konfiguration des Plugins behoben werden.
Darüber hinaus meldet Android Studio bei der Installation des Plugins die Fehlermeldung, dass keine Hauptaktivität zum Starten gefunden werden kann. Dies ist zu erwarten, da das Plugin keine Aktivitäten hat (außer dem leeren Intent, der zum Auflösen eines Intents verwendet wird). Um den Fehler zu beheben, ändern Sie in der Build-Konfiguration die Option „Starten“ auf „Nichts“ .
Abbildung 1. Konfiguration des Plugins Android Studio
Proxy-Plugin
Die Anpassung von Apps mithilfe der Car UI-Bibliothek erfordert einen RRO, der auf jede spezifische App abzielt, die geändert werden soll, auch wenn die Anpassungen für alle Apps identisch sind. Dies bedeutet, dass eine RRO pro App erforderlich ist. Sehen Sie, welche Apps die Car UI-Bibliothek verwenden.
Das Car UI-Bibliotheks-Proxy-Plugin ist eine beispielhafte gemeinsam genutzte Plugin-Bibliothek, die ihre Komponentenimplementierungen an die statische Version der Car UI-Bibliothek delegiert. Dieses Plugin kann mit einem RRO angesprochen werden, das als zentraler Anpassungspunkt für Apps verwendet werden kann, die die Car UI-Bibliothek verwenden, ohne dass ein funktionales Plugin implementiert werden muss. Weitere Informationen zu RROs finden Sie unter Ändern des Werts der Ressourcen einer App zur Laufzeit .
Das Proxy-Plugin ist nur ein Beispiel und Ausgangspunkt für die Anpassung mithilfe eines Plugins. Zur Anpassung über RROs hinaus kann man eine Teilmenge der Plugin-Komponenten implementieren und für den Rest das Proxy-Plugin verwenden oder alle Plugin-Komponenten komplett von Grund auf implementieren.
Obwohl das Proxy-Plugin einen einzigen Punkt zur RRO-Anpassung für Apps bietet, benötigen Apps, die die Verwendung des Plugins ablehnen, dennoch ein RRO, das direkt auf die App selbst abzielt.
Implementieren Sie die Plugin-APIs
Der Haupteinstiegspunkt zum Plugin ist die Klasse com.android.car.ui.plugin.PluginVersionProviderImpl
. Alle Plugins müssen eine Klasse mit genau diesem Namen und Paketnamen enthalten. Diese Klasse muss über einen Standardkonstruktor verfügen und die PluginVersionProviderOEMV1
Schnittstelle implementieren.
CarUi-Plugins müssen mit Apps funktionieren, die älter oder neuer als das Plugin sind. Um dies zu erleichtern, werden alle Plugin-APIs mit einem V#
am Ende ihres Klassennamens versioniert. Wenn eine neue Version der Car UI-Bibliothek mit neuen Funktionen veröffentlicht wird, sind diese Teil der V2
Version der Komponente. Die Car UI-Bibliothek tut ihr Bestes, damit neue Funktionen im Rahmen einer älteren Plugin-Komponente funktionieren. Beispielsweise durch Konvertieren eines neuen Schaltflächentyps in der Symbolleiste in MenuItems
.
Eine App mit einer älteren Version der Car UI-Bibliothek kann sich jedoch nicht an ein neues Plugin anpassen, das für neuere APIs geschrieben wurde. Um dieses Problem zu lösen, ermöglichen wir Plugins, je nach der von den Apps unterstützten Version der OEM-API unterschiedliche Implementierungen ihrer selbst zurückzugeben.
PluginVersionProviderOEMV1
enthält eine Methode:
Object getPluginFactory(int maxVersion, Context context, String packageName);
Diese Methode gibt ein Objekt zurück, das die höchste vom Plugin unterstützte Version von PluginFactoryOEMV#
implementiert, aber immer noch kleiner oder gleich maxVersion
ist. Wenn ein Plugin keine so alte Implementierung einer PluginFactory
hat, kann es null
zurückgeben. In diesem Fall wird die statisch verknüpfte Implementierung von CarUi-Komponenten verwendet.
Um die Abwärtskompatibilität mit Apps aufrechtzuerhalten, die mit älteren Versionen der statischen Car Ui-Bibliothek kompiliert wurden, wird empfohlen, maxVersion
s von 2, 5 und höher in der Implementierung der PluginVersionProvider
Klasse Ihres Plugins zu unterstützen. Die Versionen 1, 3 und 4 werden nicht unterstützt. Weitere Informationen finden Sie unter PluginVersionProviderImpl
.
Die PluginFactory
ist die Schnittstelle, die alle anderen CarUi-Komponenten erstellt. Außerdem wird definiert, welche Version ihrer Schnittstellen verwendet werden soll. Wenn das Plugin keine dieser Komponenten implementieren möchte, gibt es in seiner Erstellungsfunktion möglicherweise null
zurück (mit Ausnahme der Symbolleiste, die über eine separate Funktion customizesBaseLayout()
verfügt).
Die pluginFactory
begrenzt, welche Versionen von CarUi-Komponenten zusammen verwendet werden können. Beispielsweise wird es niemals eine pluginFactory
geben, die Version 100 einer Toolbar
und auch Version 1 einer RecyclerView
erstellen kann, da es kaum eine Garantie dafür gäbe, dass eine Vielzahl von Versionen von Komponenten zusammenarbeiten würden. Um die Symbolleistenversion 100 verwenden zu können, wird von den Entwicklern erwartet, dass sie eine Implementierung einer pluginFactory
Version bereitstellen, die eine Symbolleistenversion 100 erstellt, die dann die Optionen auf die Versionen anderer Komponenten einschränkt, die erstellt werden können. Die Versionen anderer Komponenten sind möglicherweise nicht gleich. Beispielsweise könnte ein pluginFactoryOEMV100
einen ToolbarControllerOEMV100
und einen RecyclerViewOEMV70
erstellen.
Symbolleiste
Grundlayout
Die Symbolleiste und das „Basislayout“ sind sehr eng miteinander verbunden, daher heißt die Funktion, die die Symbolleiste erstellt, installBaseLayoutAround
. Das Grundlayout ist ein Konzept, das es ermöglicht, die Symbolleiste an einer beliebigen Stelle rund um den Inhalt der App zu positionieren, um eine Symbolleiste oben/unten in der App, vertikal entlang der Seiten oder sogar eine kreisförmige Symbolleiste, die die gesamte App umschließt, zu ermöglichen. Dies wird erreicht, indem eine Ansicht an installBaseLayoutAround
übergeben wird, damit das Symbolleisten-/Basislayout umbrochen wird.
Das Plugin sollte die bereitgestellte Ansicht übernehmen, sie von der übergeordneten Ansicht trennen, das eigene Layout des Plugins im selben Index der übergeordneten Ansicht und mit denselben LayoutParams
wie die gerade getrennte Ansicht aufblasen und die Ansicht dann irgendwo innerhalb des ursprünglichen Layouts wieder anhängen einfach aufgeblasen. Das vergrößerte Layout enthält die Symbolleiste, sofern dies von der App angefordert wird.
Die App kann ein Basislayout ohne Symbolleiste anfordern. Wenn dies der Fall ist, sollte installBaseLayoutAround
null zurückgeben. Bei den meisten Plugins ist das alles, was passieren muss, aber wenn der Plugin-Autor beispielsweise eine Dekoration am Rand der App anbringen möchte, könnte dies immer noch mit einem Basislayout erfolgen. Diese Dekorationen sind besonders nützlich für Geräte mit nicht rechteckigen Bildschirmen, da sie die App in einen rechteckigen Raum verschieben und saubere Übergänge in den nicht rechteckigen Raum hinzufügen können.
installBaseLayoutAround
wird außerdem ein Consumer<InsetsOEMV1>
übergeben. Dieser Verbraucher kann verwendet werden, um der App mitzuteilen, dass das Plugin den Inhalt der App teilweise verdeckt (über die Symbolleiste oder auf andere Weise). Die App weiß dann, dass sie weiterhin in diesem Bereich zeichnen muss, aber alle wichtigen, vom Benutzer interagierbaren Komponenten davon fernhalten muss. Dieser Effekt wird in unserem Referenzdesign verwendet, um die Symbolleiste halbtransparent zu machen und Listen darunter scrollen zu lassen. Wenn diese Funktion nicht implementiert wäre, bliebe das erste Element in einer Liste unter der Symbolleiste hängen und wäre nicht anklickbar. Wenn dieser Effekt nicht benötigt wird, kann das Plugin den Consumer ignorieren.
Abbildung 2. Scrollen des Inhalts unter der Symbolleiste
Aus Sicht der App empfängt das Plugin beim Senden neuer Insets diese von allen Aktivitäten oder Fragmenten, die InsetsChangedListener
implementieren. Wenn eine Aktivität oder ein Fragment InsetsChangedListener
nicht implementiert, verarbeitet die Car Ui-Bibliothek Einfügungen standardmäßig, indem sie die Einfügungen als Auffüllung auf die Activity
oder FragmentActivity
anwendet, die das Fragment enthält. Die Bibliothek wendet die Einfügungen standardmäßig nicht auf Fragmente an. Hier ist ein Beispielausschnitt einer Implementierung, die die Einfügungen als Auffüllung auf eine RecyclerView
in der App anwendet:
public class MainActivity extends Activity implements InsetsChangedListener {
@Override
public void onCarUiInsetsChanged(Insets insets) {
CarUiRecyclerView rv = requireViewById(R.id.recyclerview);
rv.setPadding(insets.getLeft(), insets.getTop(),
insets.getRight(), insets.getBottom());
}
}
Abschließend erhält das Plugin einen fullscreen
, der anzeigt, ob die Ansicht, die umschlossen werden soll, die gesamte App oder nur einen kleinen Abschnitt einnimmt. Dies kann verwendet werden, um das Anbringen einiger Dekorationen am Rand zu vermeiden, die nur dann Sinn machen, wenn sie am Rand des gesamten Bildschirms erscheinen. Eine Beispiel-App, die Nicht-Vollbild-Basislayouts verwendet, ist „Einstellungen“, in der jeder Bereich des Dual-Panee-Layouts über eine eigene Symbolleiste verfügt.
Da erwartet wird, dass installBaseLayoutAround
null zurückgibt, wenn toolbarEnabled
false
ist, muss das Plug-in von customizesBaseLayout
false
zurückgeben, damit es anzeigt, dass es das Basislayout nicht anpassen möchte.
Das Basislayout muss eine FocusParkingView
und eine FocusArea
enthalten, um Drehsteuerungen vollständig zu unterstützen. Diese Ansichten können auf Geräten weggelassen werden, die keine Rotation unterstützen. Die FocusParkingView/FocusAreas
werden in der statischen CarUi-Bibliothek implementiert, daher wird ein setRotaryFactories
verwendet, um Fabriken bereitzustellen, um die Ansichten aus Kontexten zu erstellen.
Die zum Erstellen von Focus-Ansichten verwendeten Kontexte müssen der Quellkontext und nicht der Kontext des Plugins sein. Die FocusParkingView
sollte möglichst nahe an der ersten Ansicht in der Baumstruktur liegen, da sie fokussiert ist, wenn für den Benutzer kein Fokus sichtbar sein sollte. Der FocusArea
muss die Symbolleiste in das Basislayout einschließen, um anzuzeigen, dass es sich um eine rotierende Nudge-Zone handelt. Wenn die FocusArea
nicht bereitgestellt wird, kann der Benutzer mit dem Drehregler nicht zu den Schaltflächen in der Symbolleiste navigieren.
Symbolleisten-Controller
Der tatsächlich zurückgegebene ToolbarController
sollte viel einfacher zu implementieren sein als das Basislayout. Seine Aufgabe besteht darin, die an seine Setter übergebenen Informationen zu übernehmen und im Basislayout anzuzeigen. Informationen zu den meisten Methoden finden Sie im Javadoc. Einige der komplexeren Methoden werden im Folgenden erläutert.
getImeSearchInterface
wird zum Anzeigen von Suchergebnissen im IME-Fenster (Tastatur) verwendet. Dies kann nützlich sein, um Suchergebnisse neben der Tastatur anzuzeigen/animieren, beispielsweise wenn die Tastatur nur die Hälfte des Bildschirms einnimmt. Der größte Teil der Funktionalität ist in der statischen CarUi-Bibliothek implementiert. Die Suchschnittstelle im Plugin stellt lediglich Methoden für die statische Bibliothek bereit, um die Rückrufe TextView
und onPrivateIMECommand
abzurufen. Um dies zu unterstützen, sollte das Plugin eine TextView
Unterklasse verwenden, die onPrivateIMECommand
überschreibt und den Aufruf an den bereitgestellten Listener als TextView
seiner Suchleiste übergibt.
setMenuItems
zeigt einfach MenuItems auf dem Bildschirm an, wird aber überraschend oft aufgerufen. Da die Plugin-API für MenuItems unveränderlich ist, erfolgt bei jeder Änderung eines MenuItems ein völlig neuer setMenuItems
Aufruf. Dies kann bei etwas so Trivialem passieren, dass ein Benutzer auf einen Schalter „MenuItem“ klickt und dieser Klick dazu führt, dass der Schalter umgeschaltet wird. Aus Leistungs- und Animationsgründen wird daher empfohlen, die Differenz zwischen der alten und der neuen MenuItems-Liste zu berechnen und nur die Ansichten zu aktualisieren, die sich tatsächlich geändert haben. Die MenuItems stellen ein key
bereit, das dabei helfen kann, da der Schlüssel bei verschiedenen Aufrufen von setMenuItems
für dasselbe MenuItem gleich sein sollte.
AppStyledView
Die AppStyledView
ist ein Container für eine Ansicht, die überhaupt nicht angepasst ist. Es kann verwendet werden, um einen Rahmen um diese Ansicht herum bereitzustellen, der sie vom Rest der App abhebt und dem Benutzer anzeigt, dass es sich um eine andere Art von Schnittstelle handelt. Die von AppStyledView umschlossene Ansicht ist in setContent
angegeben. Die AppStyledView
kann je nach Anforderung der App auch über eine Zurück- oder Schließen-Schaltfläche verfügen.
Die AppStyledView
fügt ihre Ansichten nicht sofort in die Ansichtshierarchie ein, wie dies bei installBaseLayoutAround
der Fall ist, sondern gibt ihre Ansicht einfach über getView
an die statische Bibliothek zurück, die dann die Einfügung vornimmt. Die Position und Größe von AppStyledView
kann auch durch die Implementierung getDialogWindowLayoutParam
gesteuert werden.
Kontexte
Das Plugin muss bei der Verwendung von Kontexten vorsichtig sein, da es sowohl Plugin- als auch „Quell“-Kontexte gibt. Der Plugin-Kontext wird als Argument an getPluginFactory
übergeben und ist der einzige Kontext, der die Ressourcen des Plugins enthält. Dies bedeutet, dass es der einzige Kontext ist, der zum Aufblasen von Layouts im Plugin verwendet werden kann.
Für den Plugin-Kontext ist jedoch möglicherweise nicht die richtige Konfiguration festgelegt. Um die richtige Konfiguration zu erhalten, stellen wir Quellkontexte in Methoden bereit, die Komponenten erstellen. Der Quellkontext ist normalerweise eine Aktivität, kann aber in manchen Fällen auch ein Dienst oder eine andere Android-Komponente sein. Um die Konfiguration aus dem Quellkontext mit den Ressourcen aus dem Plugin-Kontext zu verwenden, muss mit createConfigurationContext
ein neuer Kontext erstellt werden. Wenn nicht die richtige Konfiguration verwendet wird, liegt ein Verstoß gegen den strengen Android-Modus vor und die vergrößerten Ansichten haben möglicherweise nicht die richtigen Abmessungen.
Context layoutInflationContext = pluginContext.createConfigurationContext(
sourceContext.getResources().getConfiguration());
Modusänderungen
Einige Plugins können mehrere Modi für ihre Komponenten unterstützen, z. B. einen Sportmodus oder einen Eco-Modus , die optisch unterschiedlich aussehen. In CarUi gibt es keine integrierte Unterstützung für solche Funktionen, aber nichts hindert das Plugin daran, sie vollständig intern zu implementieren. Das Plugin kann alle gewünschten Bedingungen überwachen, um herauszufinden, wann der Modus gewechselt werden muss, z. B. das Abhören von Sendungen. Das Plugin kann keine Konfigurationsänderung auslösen, um Modi zu ändern. Es wird jedoch nicht empfohlen, sich auf Konfigurationsänderungen zu verlassen, da die manuelle Aktualisierung des Erscheinungsbilds jeder Komponente für den Benutzer reibungsloser ist und auch Übergänge ermöglicht, die mit Konfigurationsänderungen nicht möglich sind.
Jetpack Compose
Plugins können mit Jetpack Compose implementiert werden, dies ist jedoch eine Alpha-Level-Funktion und sollte nicht als stabil angesehen werden.
Plugins können ComposeView
verwenden, um eine Compose-fähige Oberfläche zum Rendern zu erstellen. Diese ComposeView
wäre das, was von der getView
Methode in Komponenten an die App zurückgegeben wird.
Ein Hauptproblem bei der Verwendung ComposeView
besteht darin, dass Tags für die Stammansicht im Layout festgelegt werden, um globale Variablen zu speichern, die von verschiedenen ComposeViews in der Hierarchie gemeinsam genutzt werden. Da die Ressourcen-IDs des Plugins nicht getrennt von denen der App benannt werden, kann dies zu Konflikten führen, wenn sowohl die App als auch das Plugin Tags für dieselbe Ansicht festlegen. Unten wird ein benutzerdefinierter ComposeViewWithLifecycle
bereitgestellt, der diese globalen Variablen nach unten in den ComposeView
verschiebt. Auch dies sollte nicht als stabil angesehen werden.
ComposeViewWithLifecycle
:
class ComposeViewWithLifecycle @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr),
LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {
private val lifeCycle = LifecycleRegistry(this)
private val modelStore = ViewModelStore()
private val savedStateRegistryController = SavedStateRegistryController.create(this)
private var composeView: ComposeView? = null
private var content = @Composable {}
init {
ViewTreeLifecycleOwner.set(this, this)
ViewTreeViewModelStoreOwner.set(this, this)
ViewTreeSavedStateRegistryOwner.set(this, this)
compositionContext = createCompositionContext()
}
fun setContent(content: @Composable () -> Unit) {
this.content = content
composeView?.setContent(content)
}
override fun getLifecycle(): Lifecycle {
return lifeCycle
}
override fun getViewModelStore(): ViewModelStore {
return modelStore
}
override fun getSavedStateRegistry(): SavedStateRegistry {
return savedStateRegistryController.savedStateRegistry
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
savedStateRegistryController.performRestore(Bundle())
lifeCycle.currentState = Lifecycle.State.RESUMED
composeView = ComposeView(context)
composeView?.setContent(content)
addView(composeView, LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
lifeCycle.currentState = Lifecycle.State.DESTROYED
modelStore.clear()
removeAllViews()
composeView = null
}
// Exact copy of View.createCompositionContext() in androidx's WindowRecomposer.android.kt
private fun createCompositionContext(): CompositionContext {
val currentThreadContext = AndroidUiDispatcher.CurrentThread
val pausableClock = currentThreadContext[MonotonicFrameClock]?.let {
PausableMonotonicFrameClock(it).apply { pause() }
}
val contextWithClock = currentThreadContext + (pausableClock ?: EmptyCoroutineContext)
val recomposer = Recomposer(contextWithClock)
val runRecomposeScope = CoroutineScope(contextWithClock)
val viewTreeLifecycleOwner = checkNotNull(ViewTreeLifecycleOwner.get(this)) {
"ViewTreeLifecycleOwner not found from $this"
}
viewTreeLifecycleOwner.lifecycle.addObserver(
LifecycleEventObserver { _, event ->
@Suppress("NON_EXHAUSTIVE_WHEN")
when (event) {
Lifecycle.Event.ON_CREATE ->
// Undispatched launch since we've configured this scope
// to be on the UI thread
runRecomposeScope.launch(start = CoroutineStart.UNDISPATCHED) {
recomposer.runRecomposeAndApplyChanges()
}
Lifecycle.Event.ON_START -> pausableClock?.resume()
Lifecycle.Event.ON_STOP -> pausableClock?.pause()
Lifecycle.Event.ON_DESTROY -> {
recomposer.cancel()
}
}
}
)
return recomposer
}
// TODO: ComposeViewWithLifecycle should handle saving state and other lifecycle things
// override fun onSaveInstanceState(): Parcelable? {
// val superState = super.onSaveInstanceState()
// val bundle = Bundle()
// savedStateRegistryController.performSave(bundle)
// }
}