Auto-UI-Plug-ins

Verwenden Sie Plug-ins für die Auto-UI-Mediathek, um vollständige Implementierungen von Komponentenanpassungen in der Auto-UI-Mediathek zu erstellen, anstatt Laufzeitressourcen-Overlays (RROs) zu verwenden. Mit RROs können Sie nur die XML-Ressourcen der Komponenten der Auto-UI-Bibliothek ändern. Das schränkt die Möglichkeiten zur Anpassung ein.

Plug-in erstellen

Ein Auto-UI-Mediathek-Plug-in ist ein APK, das Klassen enthält, die eine Reihe von Plug-in-APIs implementieren. Die Plug-in-APIs können als statische Bibliothek in ein Plug-in kompiliert werden.

Beispiele in Soong und Gradle:

Soong

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 Plug-in muss im Manifest einen Inhaltsanbieter mit den folgenden Attributen deklarieren:

  android:authorities="com.android.car.ui.plugin"
  android:enabled="true"
  android:exported="true"

android:authorities="com.android.car.ui.plugin" macht das Plug-in für die Auto-UI-Bibliothek sichtbar. Der Anbieter muss exportiert werden, damit er zur Laufzeit abgefragt werden kann. Wenn das Attribut enabled auf false festgelegt ist, wird anstelle der Plug-in-Implementierung die Standardimplementierung verwendet. Die Inhaltsanbieterklasse muss nicht vorhanden sein. Fügen Sie in diesem Fall der Anbieterdefinition tools:ignore="MissingClass" hinzu. Unten sehen Sie ein Beispiel für einen Manifesteintrag:

    <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 sollten Sie abschließend Ihre App signieren.

Plug-ins 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.

Die Klassen von Plug-ins, die als freigegebene Android-Bibliothek implementiert sind, werden automatisch dem gemeinsamen Classloader zwischen den Apps hinzugefügt. Wenn eine App, die die Auto-UI-Bibliothek verwendet, eine Laufzeitabhängigkeit auf die gemeinsam genutzte Plug-in-Bibliothek angibt, kann ihr Classloader auf die Klassen der gemeinsam genutzten Plug-in-Bibliothek zugreifen. Plug-ins, die als normale Android-Apps (nicht als gemeinsam genutzte Bibliothek) implementiert sind, können sich negativ auf den Kaltstart der App auswirken.

Gemeinsam genutzte Bibliotheken implementieren und erstellen

Die Entwicklung mit freigegebenen Android-Bibliotheken ähnelt der Entwicklung normaler Android-Apps, weist aber einige wesentliche Unterschiede auf.

  • Verwenden Sie das library-Tag unter dem application-Tag mit dem Namen des Plug-in-Pakets im App-Manifest des Plug-ins:
    <application>
        <library android:name="com.chassis.car.ui.plugin" />
        ...
    </application>
  • Konfigurieren Sie Ihre Soong-android_app-Build-Regel (Android.bp) mit dem AAPT-Flag shared-lib, das zum Erstellen einer freigegebenen Bibliothek verwendet wird:
android_app {
  ...
  aaptflags: ["--shared-lib"],
  ...
}

Abhängigkeiten von gemeinsam genutzten Bibliotheken

Fügen Sie für jede App im System, die die Auto-UI-Mediathek verwendet, das Tag uses-library im App-Manifest unter dem Tag application mit dem Namen des Plug-in-Pakets hinzu:

<manifest>
  <application
      android:name=".MyApp"
      ...>
    <uses-library android:name="com.chassis.car.ui.plugin" android:required="false"/>
    ...
  </application>
</manifest>

Plug-in installieren

Plug-ins MÜSSEN in der Systempartition vorinstalliert sein. Fügen Sie das Modul dazu in PRODUCT_PACKAGES ein. Das vorinstallierte Paket kann ähnlich wie jede andere installierte App aktualisiert werden.

Wenn Sie ein vorhandenes Plug-in auf dem System aktualisieren, werden alle Apps, die dieses Plug-in verwenden, automatisch geschlossen. Wenn der Nutzer die Seite dann wieder öffnet, werden ihm die aktualisierten Änderungen angezeigt. Wenn die App nicht ausgeführt wurde, wird beim nächsten Start das aktualisierte Plug-in verwendet.

Bei der Installation eines Plug-ins mit Android Studio sind einige zusätzliche Aspekte zu beachten. Zum Zeitpunkt der Erstellung dieses Artikels gibt es einen Fehler bei der Installation von Apps in Android Studio, durch den Updates für ein Plug-in nicht wirksam werden. Das Problem lässt sich beheben, indem Sie in der Build-Konfiguration des Plug-ins die Option Always install with package manager (disables deploy optimizations on Android 11 and later) (Immer mit Paketmanager installieren (deaktiviert Bereitstellungsoptimierungen unter Android 11 und höher)) auswählen.

Außerdem meldet Android Studio bei der Installation des Plug-ins den Fehler, dass keine Hauptaktivität zum Starten gefunden werden kann. Das ist normal, da das Plug-in keine Aktivitäten hat (außer dem leeren Intent, der zum Auflösen eines Intents verwendet wird). Ändern Sie in der Build-Konfiguration die Option Starten in Nichts, um den Fehler zu beheben.

Android Studio-Konfiguration des Plug-ins Abbildung 1 Android Studio-Konfiguration des Plug-ins

Proxy-Plug-in

Für die Anpassung von Apps mit der Car UI-Bibliothek ist ein RRO erforderlich, der auf jede App ausgerichtet ist, die geändert werden soll, auch wenn die Anpassungen für alle Apps identisch sind. Das bedeutet, dass für jede App eine RRO erforderlich ist. Hier sehen Sie, welche Apps die Auto-UI-Mediathek verwenden.

Das Proxy-Plug-in für die Auto-UI-Mediathek ist ein Beispiel für eine gemeinsam genutzte Plug-in-Bibliothek, die ihre Komponentenimplementierungen an die statische Version der Auto-UI-Mediathek delegiert. Dieses Plug-in kann auf eine RRO ausgerichtet werden, die als zentraler Anpassungspunkt für Apps verwendet werden kann, die die Auto-UI-Mediathek verwenden, ohne dass ein funktionales Plug-in implementiert werden muss. Weitere Informationen zu RROs finden Sie unter Wert der Ressourcen einer App zur Laufzeit ändern.

Das Proxy-Plug-in ist nur ein Beispiel und ein Ausgangspunkt für die Anpassung mit einem Plug-in. Für Anpassungen, die über RROs hinausgehen, können Sie einen Teil der Plug-in-Komponenten implementieren und für den Rest das Proxy-Plug-in verwenden oder alle Plug-in-Komponenten von Grund auf neu implementieren.

Das Proxy-Plug-in bietet zwar eine zentrale Anpassung von RROs für Apps, aber für Apps, die das Plug-in nicht verwenden, ist weiterhin ein RRO erforderlich, der direkt auf die App selbst ausgerichtet ist.

Plug-in-APIs implementieren

Der Haupteinstiegspunkt für das Plug-in ist die Klasse com.android.car.ui.plugin.PluginVersionProviderImpl. Alle Plug-ins müssen eine Klasse mit genau diesem Namen und Paketnamen enthalten. Diese Klasse muss einen Standardkonstruktor haben und die Schnittstelle PluginVersionProviderOEMV1 implementieren.

CarUi-Plug-ins müssen mit Apps funktionieren, die älter oder neuer als das Plug-in sind. Um dies zu vereinfachen, werden alle Plug-in-APIs am Ende ihrer Klassennamen mit einem V# versioniert. Wenn eine neue Version der Car UI-Bibliothek mit neuen Funktionen veröffentlicht wird, sind diese Teil der V2-Version der Komponente. Die Auto-UI-Mediathek ist bestrebt, neue Funktionen im Rahmen einer älteren Plug-in-Komponente zu implementieren. Beispielsweise können Sie eine neue Schaltfläche in der Symbolleiste in MenuItems konvertieren.

Eine App mit einer älteren Version der Auto-UI-Mediathek kann jedoch nicht an ein neues Plug-in angepasst werden, das für neuere APIs geschrieben wurde. Um dieses Problem zu lösen, erlauben wir Plugins, je nach Version der OEM API, die von den Apps unterstützt wird, unterschiedliche Implementierungen von sich 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 vom Plug-in unterstützte höchste Version von PluginFactoryOEMV# implementiert, aber kleiner oder gleich maxVersion ist. Wenn ein Plug-in keine Implementierung einer so alten PluginFactory hat, wird möglicherweise null zurückgegeben. In diesem Fall wird die statisch verknüpfte Implementierung der CarUi-Komponenten verwendet.

Um die Abwärtskompatibilität mit Apps zu gewährleisten, die mit älteren Versionen der Static Car UI-Bibliothek kompiliert wurden, wird empfohlen, in der Implementierung der PluginVersionProvider-Klasse Ihres Plug-ins maxVersion-Werte von 2, 5 und höher zu unterstützen. Versionen 1, 3 und 4 werden nicht unterstützt. Weitere Informationen finden Sie unter PluginVersionProviderImpl.

Die PluginFactory ist die Schnittstelle, über die alle anderen CarUi-Komponenten erstellt werden. Außerdem wird festgelegt, welche Version der Schnittstellen verwendet werden soll. Wenn das Plug-in keine dieser Komponenten implementieren soll, kann in der Erstellungsfunktion null zurückgegeben werden (mit Ausnahme der Symbolleiste, die eine separate customizesBaseLayout()-Funktion hat).

Mit der pluginFactory wird festgelegt, welche Versionen von CarUi-Komponenten zusammen verwendet werden können. Es wird beispielsweise nie eine pluginFactory geben, mit der Version 100 einer Toolbar und Version 1 einer RecyclerView erstellt werden kann, da es keine Garantie dafür gibt, dass eine Vielzahl von Komponentenversionen zusammenarbeiten. Wenn Entwickler die Symbolleiste Version 100 verwenden möchten, müssen sie eine Version von pluginFactory implementieren, die eine Symbolleiste Version 100 erstellt. Dies schränkt die Optionen für die Versionen anderer Komponenten ein, die erstellt werden können. Die Versionen anderer Komponenten müssen nicht gleich sein. So kann beispielsweise eine pluginFactoryOEMV100 eine ToolbarControllerOEMV100 und eine RecyclerViewOEMV70 erstellen.

Symbolleiste

Basislayout

Die Symbolleiste und das „Basislayout“ sind eng miteinander verbunden. Daher wird die Funktion, die die Symbolleiste erstellt, installBaseLayoutAround genannt. Das Basislayout ist ein Konzept, mit dem die Symbolleiste an beliebiger Stelle um den Inhalt der App herum positioniert werden kann. So kann die Symbolleiste oben oder unten in der App, vertikal an den Seiten oder sogar als kreisförmige Symbolleiste um die gesamte App herum platziert werden. Dazu wird eine Ansicht an installBaseLayoutAround übergeben, um das Layout der Symbolleiste/des Basislayouts zu umschließen.

Das Plug-in sollte die bereitgestellte Ansicht übernehmen, sie vom übergeordneten Element trennen, das eigene Layout des Plug-ins im selben Index des übergeordneten Elements und mit derselben LayoutParams wie die gerade getrennte Ansicht einfügen und die Ansicht dann irgendwo im gerade eingeblendeten Layout wieder anhängen. Das maximierte Layout enthält die Symbolleiste, sofern dies von der App angefordert wird.

Die App kann ein Basislayout ohne Symbolleiste anfordern. Andernfalls sollte installBaseLayoutAround null zurückgeben. Bei den meisten Plugins ist das alles, was erforderlich ist. Wenn der Plugin-Entwickler jedoch beispielsweise eine Verzierung am Rand der App anwenden möchte, kann dies auch mit einem Basislayout erfolgen. Diese Verzierungen sind besonders nützlich für Geräte mit nicht rechteckigen Bildschirmen, da sie die App in einen rechteckigen Bereich drängen und saubere Übergänge in den nicht rechteckigen Bereich hinzufügen können.

installBaseLayoutAround wird auch eine Consumer<InsetsOEMV1> übergeben. Über diesen Verbraucher kann der App mitgeteilt werden, dass das Plug-in die Inhalte der App teilweise (z. B. mit der Symbolleiste) verdeckt. Die App weiß dann, dass in diesem Bereich weiter gezeichnet werden soll, aber keine wichtigen interaktiven Komponenten für Nutzer enthalten sein dürfen. Dieser Effekt wird in unserem Referenzdesign verwendet, um die Symbolleiste halbtransparent zu machen und Listen darunter zu scrollen. Wenn diese Funktion nicht implementiert wäre, würde der erste Eintrag in einer Liste unter der Symbolleiste eingeklemmt und wäre nicht anklickbar. Wenn dieser Effekt nicht erforderlich ist, kann das Plug-in den Verbraucher ignorieren.

Inhalte scrollen unter der Symbolleiste Abbildung 2. Inhalte scrollen unter der Symbolleiste

Aus Sicht der App werden neue Einblendungen, die vom Plug-in gesendet werden, von allen Aktivitäten oder Fragmenten empfangen, die InsetsChangedListener implementieren. Wenn eine Aktivität oder ein Fragment InsetsChangedListener nicht implementiert, werden Einzüge standardmäßig von der Car UI-Bibliothek verarbeitet, indem sie als Abstand zum Activity oder FragmentActivity angewendet werden, das das Fragment enthält. Die Bibliothek wendet die Einzüge standardmäßig nicht auf Fragmente an. Hier ist ein Beispiel für ein Snippet einer Implementierung, bei der die Einzüge als Abstand auf ein RecyclerView in der App angewendet werden:

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());
  }
}

Schließlich wird dem Plug-in ein fullscreen-Hinweis gegeben, der angibt, ob die Ansicht, die umgebrochen werden soll, die gesamte App oder nur einen kleinen Bereich einnimmt. So können Sie vermeiden, dass Dekorationen am Rand angebracht werden, die nur dann sinnvoll sind, wenn sie am Rand des gesamten Bildschirms erscheinen. Ein Beispiel für eine App, die keine Vollbild-Basislayouts verwendet, ist die Einstellungen-App. Dort hat jeder Bereich des zweispaltigen Layouts eine eigene Symbolleiste.

Da installBaseLayoutAround normalerweise null zurückgibt, wenn toolbarEnabled false ist, muss das Plug-in false von customizesBaseLayout zurückgeben, um anzugeben, dass das Basislayout nicht angepasst werden soll.

Das Basislayout muss ein FocusParkingView und ein FocusArea enthalten, um Drehregler vollständig zu unterstützen. Diese Ansichten können auf Geräten, die keine Drehung unterstützen, weggelassen werden. Die FocusParkingView/FocusAreas werden in der statischen CarUi-Bibliothek implementiert. Daher wird ein setRotaryFactories verwendet, um Fabriken zum Erstellen der Ansichten aus Kontexten bereitzustellen.

Die Kontexte, die zum Erstellen von Fokusansichten verwendet werden, müssen der Quellkontext sein, nicht der Kontext des Plug-ins. Die FocusParkingView sollte sich so nah wie möglich an der ersten Ansicht im Baum befinden, da sie im Fokus steht, wenn für den Nutzer kein Fokus sichtbar sein sollte. Die FocusArea muss die Symbolleiste im Basislayout umbrechen, um anzugeben, dass es sich um eine Zone für die Drehung handelt. Wenn die FocusArea nicht vorhanden ist, kann der Nutzer mit dem Drehregler nicht zu den Schaltflächen in der Symbolleiste wechseln.

Symbolleistensteuerung

Die tatsächlich zurückgegebene ToolbarController sollte viel einfacher zu implementieren sein als das Basislayout. Seine Aufgabe besteht darin, Informationen, die an seine Setter übergeben werden, im Basislayout anzuzeigen. Informationen zu den meisten Methoden finden Sie in der Javadoc. Einige der komplexeren Methoden werden unten erläutert.

getImeSearchInterface wird verwendet, um Suchergebnisse im Fenster der Eingabemethode (Tastatur) anzuzeigen. Das kann nützlich sein, um Suchergebnisse neben der Tastatur anzuzeigen oder zu animieren, z. B. wenn die Tastatur nur die Hälfte des Bildschirms einnimmt. Der Großteil der Funktionen ist in der statischen CarUi-Bibliothek implementiert. Die Suchoberfläche im Plug-in bietet nur Methoden für die statische Bibliothek, um die TextView- und onPrivateIMECommand-Callbacks abzurufen. Dazu sollte das Plug-in eine TextView-Unterklasse verwenden, die onPrivateIMECommand überschreibt und den Aufruf als TextView der Suchleiste an den bereitgestellten Listener weitergibt.

setMenuItems zeigt einfach Menüpunkte auf dem Bildschirm an, wird aber überraschend oft aufgerufen. Da die Plugin-API für Menüpunkte unveränderlich ist, wird bei jeder Änderung eines Menüpunkts ein ganz neuer setMenuItems-Aufruf ausgeführt. Das kann auch bei etwas so Trivialem passieren, wie wenn ein Nutzer auf ein Menüelement für einen Schalter klickt und dieser durch den Klick aktiviert wird. Aus Leistungs- und Animationsgründen wird daher empfohlen, den Unterschied 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 bieten ein key-Feld, das dabei helfen kann, da der Schlüssel für verschiedene Aufrufe von setMenuItems für dasselbe MenuItem gleich sein sollte.

AppStyledView

AppStyledView ist ein Container für eine Ansicht, die nicht angepasst wurde. Sie können damit einen Rahmen um diese Ansicht setzen, der sie vom Rest der App abhebt und den Nutzern signalisiert, dass es sich um eine andere Art von Benutzeroberfläche handelt. Die Ansicht, die von der AppStyledView umschlossen wird, ist in setContent angegeben. Die AppStyledView kann auch eine Schaltfläche „Zurück“ oder „Schließen“ enthalten, wie von der App angefordert.

Die AppStyledView fügt ihre Ansichten nicht wie die installBaseLayoutAround sofort in die Ansichtshierarchie ein, sondern gibt sie einfach über getView an die statische Bibliothek zurück, die dann die Einfügung vornimmt. Die Position und Größe des AppStyledView kann auch durch Implementieren von getDialogWindowLayoutParam gesteuert werden.

Kontexte

Das Plug-in muss bei der Verwendung von Kontexten vorsichtig sein, da es sowohl Plug-in- als auch „Quell-“Kontexte gibt. Der Plug-in-Kontext wird als Argument an getPluginFactory übergeben und ist der einzige Kontext, der die Ressourcen des Plug-ins enthält. Das bedeutet, dass es der einzige Kontext ist, mit dem Layouts im Plug-in maximiert werden können.

Möglicherweise ist die Konfiguration für den Plug-in-Kontext jedoch nicht korrekt. Um die richtige Konfiguration zu erhalten, geben wir Quellkontexte in Methoden an, die Komponenten erstellen. Der Quellkontext ist in der Regel eine Aktivität, kann aber in einigen Fällen auch ein Dienst oder eine andere Android-Komponente sein. Wenn Sie die Konfiguration aus dem Quellkontext mit den Ressourcen aus dem Plug-in-Kontext verwenden möchten, muss mit createConfigurationContext ein neuer Kontext erstellt werden. Wenn die richtige Konfiguration nicht verwendet wird, liegt ein Verstoß gegen den strikten Android-Modus vor und die gesteigerten Aufrufe haben möglicherweise nicht die richtigen Abmessungen.

Context layoutInflationContext = pluginContext.createConfigurationContext(
        sourceContext.getResources().getConfiguration());

Modusänderungen

Einige Plug-ins unterstützen mehrere Modi für ihre Komponenten, z. B. einen Sportmodus oder einen Eco-Modus, die sich optisch unterscheiden. In CarUi gibt es keine integrierte Unterstützung für solche Funktionen. Es gibt jedoch nichts, was die Implementierung dieser Funktionen vollständig intern verhindert. Das Plug-in kann beliebige Bedingungen überwachen, um zu ermitteln, wann der Modus gewechselt werden soll, z. B. das Hören nach Übertragungen. Das Plug-in kann keine Konfigurationsänderung auslösen, um den Modus zu ändern. Es wird jedoch nicht empfohlen, sich auf Konfigurationsänderungen zu verlassen, da das manuelle Aktualisieren des Erscheinungsbildes jeder Komponente für den Nutzer flüssiger 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. Diese Funktion befindet sich jedoch in der Alphaphase und sollte nicht als stabil betrachtet werden.

Mit ComposeView können Plugins eine Compose-kompatible Oberfläche zum Rendern erstellen. Diese ComposeView wird von der getView-Methode in den Komponenten an die App zurückgegeben.

Ein großes Problem bei der Verwendung von ComposeView ist, dass damit Tags in der Stammansicht im Layout festgelegt werden, um globale Variablen zu speichern, die für verschiedene ComposeViews in der Hierarchie freigegeben werden. Da die Ressourcen-IDs des Plug-ins nicht separat vom Namensbereich der App erstellt werden, kann es zu Konflikten kommen, wenn sowohl die App als auch das Plug-in Tags für dieselbe Ansicht festlegen. Unten finden Sie eine benutzerdefinierte ComposeViewWithLifecycle, mit der diese globalen Variablen in die ComposeView verschoben werden. Auch diese Version sollte nicht als stabil betrachtet 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)
//  }
}