Auto-UI-Plug-ins

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

Plug‑in erstellen

Ein Plug‑in der Auto-UI-Bibliothek 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

Ein 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')

Im Manifest des Plug‑ins muss ein Content Provider deklariert sein, der die folgenden Attribute hat:

  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 auffindbar. Der Provider 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 Plug‑in-Implementierung verwendet. Die Content Provider-Klasse muss nicht vorhanden sein. In diesem Fall müssen Sie der Providerdefinition 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>

Als Sicherheitsmaßnahme sollten Sie 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 gemeinsam genutzte Android-Bibliothek implementiert sind, werden die Klassen automatisch dem gemeinsam genutzten 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. Plug‑ins, die als normale Android-Apps implementiert sind (nicht als gemeinsam genutzte Bibliothek), können sich negativ auf die Kaltstartzeiten von Apps auswirken.

Gemeinsam genutzte Bibliotheken implementieren und erstellen

Die Entwicklung mit gemeinsam genutzten Android-Bibliotheken ähnelt der Entwicklung normaler Android-Apps, es gibt jedoch einige wichtige Unterschiede.

  • Verwenden Sie das Tag library unter dem Tag application mit dem Paketnamen des Plug‑ins im App-Manifest des 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 Auto-UI-Bibliothek verwendet, das uses-library Tag im App-Manifest unter dem application Tag 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 sein. Dazu muss das Modul in PRODUCT_PACKAGES enthalten sein. 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. Nachdem sie vom Nutzer wieder geöffnet wurden, sind die aktualisierten Änderungen verfügbar. Wenn die App nicht ausgeführt wurde, enthält sie beim nächsten Start das aktualisierte Plug‑in.

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 Android Studio-App-Installationsprozess, der dazu führt, dass Aktualisierungen eines Plug‑ins nicht wirksam werden. Sie können dieses Problem 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) auswählen.

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

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

Proxy-Plug‑in

Für die Anpassung von Apps, die die Auto-UI-Bibliothek verwenden, ist ein RRO erforderlich, das auf jede zu ändernde App ausgerichtet ist. Das gilt auch dann, wenn die Anpassungen für alle Apps identisch sind. Das bedeutet, dass für jede App ein RRO erforderlich ist. Hier sehen Sie, welche Apps die Auto-UI-Bibliothek verwenden.

Das Proxy-Plug‑in der Auto-UI-Bibliothek ist ein Beispiel für eine gemeinsam genutzte Plug‑in-Bibliothek, die ihre Komponentenimplementierungen an die statische Version der Auto-UI-Bibliothek delegiert. Dieses Plug‑in kann mit einem RRO verwendet werden, das als zentraler Anpassungspunkt für Apps dienen kann, die die Auto-UI-Bibliothek 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, 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 das Plug‑in nicht verwenden, ist weiterhin ein RRO erforderlich, das direkt auf die App 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.

Auto-UI-Plug‑ins müssen mit Apps funktionieren, die älter oder neuer als das Plug‑in sind. Um dies zu ermöglichen, werden alle Plug‑in-APIs mit V# am Ende des Klassennamens versioniert. Wenn eine neue Version der Auto-UI-Bibliothek mit neuen Funktionen veröffentlicht wird, sind diese Teil der Version V2 der Komponente. Die Auto-UI-Bibliothek versucht, neue Funktionen im Rahmen einer älteren Plug‑in-Komponente zu verwenden. Beispielsweise durch Konvertieren eines neuen Schaltflächentyps 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 Plug‑ins verschiedene Implementierungen von sich selbst zurückgeben, je nach der Version der OEM-API, die von den Apps unterstützt wird.

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 einer so alten PluginFactory hat, kann es null zurückgeben. In diesem Fall wird die statisch verknüpfte Implementierung von Auto-UI-Komponenten verwendet.

Um die Abwärtskompatibilität mit Apps aufrechtzuerhalten, die für ältere Versionen der statischen Auto-UI-Bibliothek kompiliert wurden, empfiehlt es sich, maxVersions von 2, 5 und höher in der Implementierung der Klasse PluginVersionProvider des Plug‑ins zu unterstützen. Die Versionen 1, 3 und 4 werden nicht unterstützt. Weitere Informationen finden Sie unter PluginVersionProviderImpl.

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

Die pluginFactory begrenzt, welche Versionen von Auto-UI-Komponenten zusammen verwendet werden können. Es gibt beispielsweise keine pluginFactory, die Version 100 einer Toolbar und Version 1 einer RecyclerView erstellen kann, da es kaum eine Garantie dafür gäbe, dass eine Vielzahl von Versionen von Komponenten zusammen funktionieren. Um die Symbolleistenversion 100 zu verwenden, müssen Entwickler eine Implementierung einer Version von pluginFactory bereitstellen, die eine Symbolleistenversion 100 erstellt. Dadurch werden die Optionen für die Versionen anderer Komponenten eingeschränkt, die erstellt werden können. Die Versionen anderer Komponenten müssen nicht gleich sein. So könnte eine pluginFactoryOEMV100 beispielsweise 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 ermöglicht es, die Symbolleiste an einer beliebigen Stelle um den Inhalt der App zu positionieren, z. B. oben oder unten in der App, vertikal an den Seiten oder sogar als kreisförmige Symbolleiste, die die gesamte App umschließt. Dazu wird eine Ansicht an installBaseLayoutAround übergeben, um die Symbolleiste bzw. das Basis layout zu umschließen.

Das Plug‑in sollte die bereitgestellte Ansicht verwenden, sie von ihrem übergeordneten Element trennen, das eigene Layout des Plug‑ins am selben Index des übergeordneten Elements und mit denselben 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 eingefügte Layout enthält die Symbolleiste, falls von der App angefordert.

Die App kann ein Basislayout ohne Symbolleiste anfordern. In diesem Fall sollte installBaseLayoutAround „null“ zurückgeben. Für die meisten Plug‑ins ist das alles, was passieren muss. Wenn der Plug‑in-Autor jedoch beispielsweise eine Dekoration um den Rand der App anbringen möchte, kann das 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 ermöglichen.

installBaseLayoutAround wird auch ein Consumer<InsetsOEMV1> übergeben. Mit diesem Consumer kann der App mitgeteilt werden, dass das Plug‑in den Inhalt der App teilweise verdeckt (mit der Symbolleiste oder auf andere Weise). Die App weiß dann, dass sie in diesem Bereich weiter zeichnen muss, aber alle wichtigen interaktiven Komponenten für Nutzer aus diesem Bereich heraushalten 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, würde das erste Element in einer Liste unter der Symbolleiste feststecken und wäre nicht anklickbar. Wenn dieser Effekt nicht erforderlich ist, kann das Plug‑in den Consumer ignorieren.

Inhalte unter der Symbolleiste scrollen Abbildung 2 Inhalt, der unter der Symbolleiste scrollt

Aus Sicht der App erhält sie neue Insets vom Plug‑in von allen Aktivitäten oder Fragmenten, die InsetsChangedListener implementieren. Wenn eine Aktivität oder ein Fragment InsetsChangedListener nicht implementiert, verarbeitet die Auto-UI Bibliothek Insets standardmäßig, indem sie die Insets als Padding auf die Activity oder FragmentActivity anwendet, die das Fragment enthält. Die Bibliothek wendet die Insets standardmäßig nicht auf Fragmente an. Hier ein Beispiel für eine Implementierung, die die Insets als Padding 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());
  }
}

Schließlich erhält das Plug‑in einen Hinweis fullscreen, der angibt, ob die Ansicht, die umschlossen werden soll, die gesamte App oder nur einen kleinen Bereich einnimmt. So können Sie vermeiden, dass einige Dekorationen am Rand angewendet werden, die nur sinnvoll sind, wenn sie am Rand des gesamten Bildschirms erscheinen. Eine Beispiel-App, die Basislayouts ohne Vollbildmodus verwendet, ist die App „Einstellungen“, in der jeder Bereich des Layouts mit zwei Bereichen eine eigene Symbolleiste hat.

Da erwartet wird, dass installBaseLayoutAround „null“ zurückgibt, wenn toolbarEnabled auf false gesetzt ist, muss das Plug‑in false von customizesBaseLayout zurückgeben, um anzugeben, dass es das Basislayout nicht anpassen möchte.

Das Basislayout muss eine FocusParkingView und eine FocusArea enthalten, um Drehregler vollständig zu unterstützen. Diese Ansichten können auf Geräten weggelassen werden, die keine Drehregler unterstützen. Die FocusParkingView/FocusAreas werden in der statischen Auto-UI-Bibliothek implementiert. Daher wird setRotaryFactories verwendet, um Factories bereitzustellen, mit denen die Ansichten aus Kontexten erstellt werden können.

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

Symbolleisten-Controller

Die tatsächliche ToolbarController-Rückgabe sollte viel einfacher zu implementieren sein als das Basislayout. Seine Aufgabe ist es, Informationen zu übernehmen, die an seine Setter übergeben werden, und sie 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 IME-Fenster (Tastatur) anzuzeigen. Das kann nützlich sein, um Suchergebnisse neben der Tastatur anzuzeigen oder zu animieren, wenn die Tastatur beispielsweise nur die Hälfte des Bildschirms einnimmt. Die meisten Funktionen werden in der statischen Auto-UI-Bibliothek implementiert. Die Suchoberfläche im Plug‑in bietet nur Methoden, mit denen die statische Bibliothek die TextView und onPrivateIMECommand-Callbacks abrufen kann. 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 Plug‑in-API für MenuItems unveränderlich ist, wird bei jeder Änderung eines MenuItem ein völlig neuer setMenuItems-Aufruf ausgeführt. Das kann bei etwas so Trivialem passieren, wie wenn ein Nutzer auf ein MenuItem mit einem Schalter klickt und dieser Klick dazu führt, dass der Schalter umgelegt wird. Aus Leistungs- und Animationsgründen empfiehlt es sich daher, 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 bieten ein Feld key, das dabei helfen kann, da der Schlüssel bei verschiedenen Aufrufen von setMenuItems für dasselbe MenuItem gleich sein sollte.

Kontexte

Das Plug‑in muss bei der Verwendung von Kontexten vorsichtig sein, da es sowohl Plug‑in-Kontexte als auch „Quellkontexte“ 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 er der einzige Kontext ist, der zum Einfügen von Layouts im Plug‑in verwendet werden kann.

Der Plug‑in-Kontext hat jedoch möglicherweise nicht die richtige Konfiguration. 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 aber in einigen Fällen auch ein Dienst oder eine andere Android-Komponente sein. Um die Konfiguration aus dem Quellkontext mit den Ressourcen aus dem Plug‑in-Kontext zu verwenden, muss mit createConfigurationContext ein neuer Kontext erstellt werden. Wenn die richtige Konfiguration nicht verwendet wird, kommt es zu einer Verletzung des Android-Strikt-Modus und die eingefügten Ansichten haben möglicherweise nicht die richtigen Abmessungen.

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

Modusänderungen

Einige Plug‑ins können mehrere Modi für ihre Komponenten unterstützen, z. B. einen Sportmodus oder einen Eco-Modus, die sich visuell unterscheiden. Die Auto-UI-Bibliothek bietet keine integrierte Unterstützung für solche Funktionen, aber nichts hindert das Plug‑in daran, sie vollständig intern zu implementieren. Das Plug‑in kann alle gewünschten Bedingungen überwachen, um herauszufinden, wann der Modus gewechselt werden soll, z. B. auf Broadcasts hören. 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 die manuelle Aktualisierung 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

Plug‑ins können mit Jetpack Compose implementiert werden. Diese Funktion befindet sich jedoch in der Alpha-Phase und sollte nicht als stabil betrachtet werden.

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

Ein großes Problem bei der Verwendung von ComposeView besteht darin, dass Tags in der 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 von denen der App mit einem Namespace versehen sind, kann es zu Konflikten kommen, wenn sowohl die App als auch das Plug‑in Tags in derselben Ansicht festlegen. Unten finden Sie eine benutzerdefinierte ComposeViewWithLifecycle, die diese globalen Variablen in die ComposeView verschiebt. 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)
//  }
}