Auto-UI-Plugins

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 dem application -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-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 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“ .

Plugin-Android-Studio-Konfiguration 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.

Scrollen des Inhalts unterhalb der Symbolleiste 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)
//  }
}