Auto-UI-Plug-ins

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

Plug‑in erstellen

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

Beispiele in Soong und Gradle:

Soong

Hier ein Beispiel für Soong:

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 in seinem Manifest einen Contentanbieter deklariert haben, der die folgenden Attribute hat:

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

Durch android:authorities="com.android.car.ui.plugin" wird das Plug-in für die Auto-UI-Bibliothek auffindbar. Der Anbieter muss exportiert werden, damit er zur Laufzeit abgefragt werden kann. Wenn das Attribut enabled auf false gesetzt ist, wird die Standardimplementierung anstelle der Plug-in-Implementierung verwendet. Die Content-Provider-Klasse muss nicht vorhanden sein. In diesem Fall müssen Sie der Anbieterdefinition tools:ignore="MissingClass" hinzufügen. Hier 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>

Schließlich müssen Sie als Sicherheitsmaßnahme 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 zur Laufzeit von anderen Apps verwiesen wird.

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

Gemeinsam genutzte Bibliotheken implementieren und erstellen

Die Entwicklung mit Android-Freigabebibliotheken ähnelt der Entwicklung normaler Android-Apps, es gibt jedoch einige wesentliche Unterschiede.

  • Verwenden Sie das library-Tag unter dem application-Tag mit dem Namen des Plug-in-Pakets im App-Manifest Ihres Plug-ins:
    <application>
        <library android:name="com.chassis.car.ui.plugin" />
        ...
    </application>
  • Konfigurieren Sie Ihre Soong-Build-Regel android_app (Android.bp) mit dem AAPT-Flag shared-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 Paketnamen des Plug-ins ein:

<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 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 Plug-in auf dem System aktualisieren, werden alle Apps, die dieses Plug-in verwenden, automatisch geschlossen. Wenn der Nutzer die Datei wieder öffnet, sind die aktualisierten Änderungen verfügbar. Wenn die App nicht ausgeführt wurde, ist beim nächsten Start das aktualisierte Plug-in verfügbar.

Bei der Installation eines Plug-ins mit Android Studio sind einige zusätzliche Aspekte zu berücksichtigen. Zum Zeitpunkt der Erstellung dieses Dokuments gibt es einen Fehler im Installationsprozess von Android Studio-Apps, der dazu führt, dass Aktualisierungen eines Plug-ins nicht wirksam werden. Dieses 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 dem Paketmanager installieren (deaktiviert Bereitstellungsoptimierungen unter Android 11 und höher)) auswählen.

Außerdem meldet Android Studio beim Installieren des Plug-ins einen Fehler, dass keine Hauptaktivität zum Starten gefunden werden kann. Das ist normal, da das Plug-in keine Aktivitäten hat (mit Ausnahme des leeren Intents, der zum Auflösen eines Intents verwendet wird). Um den Fehler zu beheben, ändern Sie die Option Starten in der Build-Konfiguration in Nichts.

Plug‑in-Konfiguration für Android Studio Abbildung 1: Plug‑in-Konfiguration für Android Studio

Proxy-Plug‑in

Für die Anpassung von Apps mit der Auto-UI-Bibliothek ist ein RRO erforderlich, das auf jede zu ändernde App ausgerichtet ist, auch wenn die Anpassungen für alle Apps identisch sind. Das bedeutet, dass für jede App ein RRO erforderlich ist. Sehen Sie sich an, welche Apps die Car UI-Mediathek verwenden.

Das Car UI Library Proxy-Plug-in ist ein Beispiel für eine gemeinsam genutzte Plug-in-Bibliothek, die ihre Komponentenimplementierungen an die statische Version der Car UI Library delegiert. Dieses Plug-in kann mit einem RRO versehen werden, das als zentraler Anpassungspunkt für Apps verwendet werden kann, die die Auto-UI-Bibliothek nutzen, ohne dass ein funktionales Plug-in implementiert werden muss. Weitere Informationen zu RROs finden Sie unter Laufzeitwert der Ressourcen einer App ändern.

Das Proxy-Plug-in ist nur ein Beispiel und ein Ausgangspunkt für die Anpassung mithilfe eines Plug-ins. Für Anpassungen, die über RROs hinausgehen, kann eine Teilmenge von Plug-in-Komponenten implementiert und das Proxy-Plug-in für den Rest verwendet werden. Alternativ können alle Plug-in-Komponenten von Grund auf neu implementiert werden.

Das Proxy-Plug-in bietet zwar einen zentralen Punkt für die RRO-Anpassung für Apps, aber für Apps, die die Verwendung des Plug-ins deaktivieren, ist weiterhin ein RRO erforderlich, das direkt auf die App selbst ausgerichtet ist.

Plugin-APIs implementieren

Der Haupteinstiegspunkt für das Plug‑in 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 einen Standardkonstruktor haben und die PluginVersionProviderOEMV1-Schnittstelle implementieren.

CarUi-Plug-ins müssen mit Apps funktionieren, die älter oder neuer als das Plug-in sind. Um dies zu erleichtern, werden alle Plugin-APIs mit einer V# am Ende des 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 Auto-UI-Mediathek versucht, neue Funktionen im Rahmen einer älteren Plug-in-Komponente zu ermöglichen. Beispielsweise durch die Umwandlung einer neuen Schaltfläche in der Symbolleiste in MenuItems.

Eine App mit einer älteren Version der Auto-UI-Bibliothek kann jedoch nicht an ein neues Plug-in angepasst werden, das für neuere APIs entwickelt wurde. Um dieses Problem zu beheben, können Plugins je nach Version der OEM-API, die von den Apps unterstützt wird, unterschiedliche Implementierungen von sich selbst zurückgeben.

PluginVersionProviderOEMV1 enthält eine Methode:

Object getPluginFactory(int maxVersion, Context context, String packageName);

Diese Methode gibt ein Objekt zurück, das die höchste Version von PluginFactoryOEMV# implementiert, die vom Plug-in unterstützt wird und gleichzeitig kleiner oder gleich maxVersion ist. Wenn ein Plug-in keine Implementierung eines PluginFactory hat, das so alt ist, 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, maxVersions von 2, 5 und höher in der Implementierung der PluginVersionProvider-Klasse Ihres Plug-ins zu unterstützen. Versionen 1, 3 und 4 werden nicht unterstützt. Weitere Informationen finden Sie unter PluginVersionProviderImpl.

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

Die pluginFactory schränkt ein, welche Versionen von CarUi-Komponenten zusammen verwendet werden können. Es wird beispielsweise nie ein pluginFactory geben, das Version 100 eines Toolbar und Version 1 eines RecyclerView erstellen kann, da es kaum eine Garantie dafür gäbe, dass eine Vielzahl von Versionen von Komponenten zusammenarbeiten. Wenn Entwickler die Symbolleistenversion 100 verwenden möchten, müssen sie eine Implementierung einer Version von pluginFactory bereitstellen, mit der eine Symbolleistenversion 100 erstellt wird. Dadurch werden die Optionen für die Versionen anderer Komponenten eingeschränkt, die erstellt werden können. Die Versionen anderer Komponenten sind möglicherweise nicht gleich. So kann beispielsweise ein pluginFactoryOEMV100 ein ToolbarControllerOEMV100 und ein RecyclerViewOEMV70 erstellen.

Symbolleiste

Basislayout

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

Das Plug-in sollte die bereitgestellte Ansicht übernehmen, sie von ihrem übergeordneten Element trennen, das eigene Layout des Plug-ins am selben Index des übergeordneten Elements und mit demselben LayoutParams wie die gerade getrennte Ansicht einfügen und die Ansicht dann wieder an einer beliebigen Stelle im gerade eingefügten Layout anfügen. Das aufgeblähte Layout enthält die Symbolleiste, wenn sie von der App angefordert wird.

Die App kann ein Basislayout ohne Symbolleiste anfordern. Wenn ja, sollte installBaseLayoutAround „null“ zurückgeben. Bei den meisten Plugins ist das alles, was passieren muss. Wenn der Plugin-Autor jedoch beispielsweise eine Dekoration am Rand der App anbringen möchte, kann dies weiterhin 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 Bereich verschieben und saubere Übergänge in den nicht rechteckigen Bereich einfügen können.

installBaseLayoutAround wird auch ein Consumer<InsetsOEMV1> übergeben. Mit diesem Consumer kann der App mitgeteilt werden, dass das Plug-in die Inhalte der App teilweise abdeckt (mit der Symbolleiste oder auf andere Weise). Die App weiß dann, dass sie in diesem Bereich weiter zeichnen soll, aber alle wichtigen, für Nutzer interaktiven Komponenten aus diesem Bereich herauszuhalten hat. 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, würde das erste Element in einer Liste unter der Symbolleiste hängen bleiben und wäre nicht anklickbar. Wenn dieser Effekt nicht benötigt wird, kann das Plug-in den Consumer ignorieren.

Inhalte unter der Symbolleiste scrollen Abbildung 2: Inhalte unter der Symbolleiste scrollen

Wenn das Plug-in neue Insets sendet, empfängt es diese aus allen Aktivitäten oder Fragmenten, die InsetsChangedListener implementieren. Wenn eine Aktivität oder ein Fragment InsetsChangedListener nicht implementiert, verarbeitet die Car UI-Bibliothek Insets standardmäßig, indem sie die Insets als Padding auf das Activity oder FragmentActivity anwendet, das das Fragment enthält. Die Bibliothek wendet die Insets standardmäßig nicht auf Fragmente an. Hier ist ein Beispiel-Snippet für eine Implementierung, bei der die Insets als Padding für ein RecyclerView in der App verwendet 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 erhält das Plug-in einen fullscreen-Hinweis, der angibt, ob die zu umschließende Ansicht die gesamte App oder nur einen kleinen Bereich einnimmt. So kann verhindert werden, dass bestimmte Dekorationen am Rand angewendet werden, die nur sinnvoll sind, wenn sie am Rand des gesamten Bildschirms erscheinen. Ein Beispiel für eine App, in der keine Vollbild-Basislayouts verwendet werden, sind die Einstellungen. Dort hat jeder Bereich des Layouts mit zwei Bereichen eine eigene Symbolleiste.

Da erwartet wird, dass installBaseLayoutAround „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 Basis-Layout muss ein FocusParkingView und ein FocusArea enthalten, um Drehregler vollständig zu unterstützen. Diese Ansichten können auf Geräten ausgelassen werden, die keine Drehung unterstützen. Die FocusParkingView/FocusAreas sind in der statischen CarUi-Bibliothek implementiert. Daher wird ein setRotaryFactories verwendet, um Factorys zum Erstellen der Ansichten aus Kontexten bereitzustellen.

Die Kontexte, die zum Erstellen von Fokusansichten verwendet werden, müssen der Quellkontext und nicht der Kontext des Plug-ins sein. Das FocusParkingView sollte so nah wie möglich an der ersten Ansicht im Baum liegen, da es fokussiert wird, wenn für den Nutzer kein Fokus sichtbar sein soll. Das FocusArea muss die Symbolleiste im Basislayout umschließen, um anzugeben, dass es sich um eine Dreh-Nudge-Zone handelt. Wenn FocusArea nicht angegeben ist, kann der Nutzer mit dem Drehregler nicht zu den Schaltflächen in der Symbolleiste navigieren.

Symbolleisten-Controller

Die tatsächliche ToolbarController sollte viel einfacher zu implementieren sein als das Basislayout. Seine Aufgabe ist es, Informationen, die an seine Setter übergeben werden, im Basislayout anzuzeigen. Informationen zu den meisten Methoden finden Sie in der Javadoc-Dokumentation. Einige der komplexeren Methoden werden unten beschrieben.

getImeSearchInterface wird verwendet, um Suchergebnisse im IME-Fenster (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. Das Suchinterface im Plug-in bietet lediglich 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 übergibt.

setMenuItems zeigt einfach MenuItems auf dem Bildschirm an, wird aber überraschend oft aufgerufen. Da die Plugin-API für MenuItems unveränderlich ist, wird bei jeder Änderung eines MenuItem ein neuer setMenuItems-Aufruf ausgeführt. Das kann bei einer so einfachen Aktion wie dem Klicken auf ein MenuItem mit Schalter passieren, wodurch der Schalter umgelegt wird. Aus Leistungs- und Animationsgründen empfiehlt es sich daher, die Differenz zwischen der alten und der neuen MenuItem-Liste zu berechnen und nur die Ansichten zu aktualisieren, die sich tatsächlich geändert haben. Die MenuItems enthalten ein key-Feld, das dabei helfen kann, da der Schlüssel bei verschiedenen Aufrufen von setMenuItems für dasselbe MenuItem gleich sein sollte.

AppStyledView

AppStyledView ist ein Container für eine Ansicht, die nicht angepasst wird. Damit kann ein Rahmen um die Ansicht erstellt werden, damit sie sich vom Rest der App abhebt und dem Nutzer signalisiert wird, dass es sich um eine andere Art von Benutzeroberfläche handelt. Die Ansicht, die von AppStyledView umschlossen wird, wird in setContent angegeben. Das AppStyledView kann auch eine Schaltfläche zum Zurückgehen oder Schließen enthalten, wie von der App angefordert.

AppStyledView fügt seine Ansichten nicht sofort in die Ansichtshierarchie ein, wie es bei installBaseLayoutAround der Fall ist. Stattdessen wird die Ansicht über getView an die statische Bibliothek zurückgegeben, die dann das Einfügen vornimmt. Die Position und Größe von AppStyledView können auch durch die Implementierung 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 dies der einzige Kontext ist, der zum Aufblähen von Layouts im Plug‑in verwendet werden kann.

Möglicherweise ist der Plugin-Kontext jedoch nicht richtig konfiguriert. Um die richtige Konfiguration zu erhalten, stellen wir Quellkontexte in Methoden bereit, die Komponenten erstellen. Der Quellkontext ist in der Regel eine Aktivität, kann in einigen Fällen aber 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, kommt es zu einem Verstoß gegen den Android-Strict-Modus und die aufgeblähten 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 und einen Eco-Modus, die sich optisch unterscheiden. CarUi bietet keine integrierte Unterstützung für solche Funktionen, aber das Plug-in kann sie vollständig intern implementieren. Das Plug-in kann beliebige Bedingungen überwachen, um herauszufinden, wann der Modus gewechselt werden soll, z. B. durch Abhören von Broadcasts. Das Plug-in kann keine Konfigurationsänderung auslösen, um den Modus zu ändern. Es wird jedoch ohnehin nicht empfohlen, sich auf Konfigurationsänderungen zu verlassen, da das manuelle Aktualisieren des Erscheinungsbilds jeder Komponente für den Nutzer 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. Diese Funktion befindet sich jedoch in der Alphaphase und sollte nicht als stabil betrachtet werden.

Plugins können ComposeView verwenden, um eine Compose-fähige Oberfläche zu erstellen, in die gerendert werden kann. Dieser ComposeView wird von der Methode getView in Komponenten an die App zurückgegeben.

Ein großes Problem bei der Verwendung von ComposeView besteht darin, dass Tags für die Stammansicht im Layout festgelegt werden, um globale Variablen zu speichern, die für verschiedene ComposeViews in der Hierarchie freigegeben sind. Da die Ressourcen-IDs des Plug-ins nicht separat vom Namespace der App sind, kann dies zu Konflikten führen, wenn sowohl die App als auch das Plug-in Tags für dieselbe Ansicht festlegen. Unten finden Sie ein benutzerdefiniertes ComposeViewWithLifecycle, mit dem diese globalen Variablen in die ComposeView verschoben werden. Auch diese Funktion 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)
//  }
}