Wtyczki interfejsu samochodowego

Użyj wtyczek biblioteki Car UI, aby utworzyć kompletne implementacje dostosowań komponentów w bibliotece Car UI, zamiast korzystać z nakładek zasobów środowiska wykonawczego (RRO). RRO umożliwiają zmianę jedynie zasobów XML komponentów biblioteki Car UI, co ogranicza zakres możliwości dostosowywania.

Utwórz wtyczkę

Wtyczka biblioteki Car UI to plik APK zawierający klasy implementujące zestaw interfejsów API wtyczek . Interfejsy API wtyczek można wkompilować we wtyczkę jako bibliotekę statyczną.

Zobacz przykłady w Soong i Gradle:

Wkrótce

Rozważmy ten przykład wkrótce:

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

Zobacz ten plik build.gradle :

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

Wtyczka musi mieć zadeklarowanego w swoim manifeście dostawcę treści, który ma następujące atrybuty:

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

android:authorities="com.android.car.ui.plugin" umożliwia wykrycie wtyczki w bibliotece Car UI. Dostawca musi zostać wyeksportowany, aby można było odpytywać go w czasie wykonywania. Ponadto, jeśli atrybut enabled jest ustawiony na false , zamiast implementacji wtyczki zostanie użyta domyślna implementacja. Klasa dostawcy treści nie musi istnieć. W takim przypadku pamiętaj o dodaniu tools:ignore="MissingClass" do definicji dostawcy. Zobacz przykładowy wpis manifestu poniżej:

    <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>

Na koniec, ze względów bezpieczeństwa, podpisz swoją aplikację .

Wtyczki jako biblioteka współdzielona

W przeciwieństwie do bibliotek statycznych systemu Android, które są kompilowane bezpośrednio w aplikacjach, biblioteki udostępnione systemu Android są kompilowane w samodzielny plik APK, do którego odwołują się inne aplikacje w czasie wykonywania.

Wtyczki zaimplementowane jako biblioteka współdzielona systemu Android mają swoje klasy automatycznie dodawane do modułu ładującego klasy współdzielone między aplikacjami. Gdy aplikacja korzystająca z biblioteki Car UI określi zależność środowiska wykonawczego od biblioteki współdzielonej wtyczki, jej moduł ładujący klasy może uzyskać dostęp do klas biblioteki współdzielonej wtyczki. Wtyczki zaimplementowane jako zwykłe aplikacje na Androida (a nie biblioteka współdzielona) mogą negatywnie wpływać na czas zimnego startu aplikacji.

Implementuj i twórz biblioteki współdzielone

Programowanie przy użyciu bibliotek współdzielonych na Androida przebiega podobnie jak w przypadku zwykłych aplikacji na Androida, z kilkoma kluczowymi różnicami.

  • Użyj tagu library pod tagiem application z nazwą pakietu wtyczki w manifeście aplikacji wtyczki:
    <application>
        <library android:name="com.chassis.car.ui.plugin" />
        ...
    </application>
  • Skonfiguruj regułę kompilacji Soong android_app ( Android.bp ) za pomocą flagi AAPT shared-lib , która jest używana do budowania biblioteki współdzielonej:
android_app {
  ...
  aaptflags: ["--shared-lib"],
  ...
}

Zależności od bibliotek współdzielonych

Dla każdej aplikacji w systemie, która korzysta z biblioteki Car UI, umieść tag uses-library w manifeście aplikacji pod tagiem application z nazwą pakietu wtyczki:

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

Zainstaluj wtyczkę

Wtyczki MUSZĄ być preinstalowane na partycji systemowej poprzez dołączenie modułu do PRODUCT_PACKAGES . Preinstalowany pakiet można aktualizować podobnie jak każdą inną zainstalowaną aplikację.

Jeśli aktualizujesz istniejącą wtyczkę w systemie, wszystkie aplikacje korzystające z tej wtyczki zostaną automatycznie zamknięte. Po ponownym otwarciu przez użytkownika mają zaktualizowane zmiany. Jeśli aplikacja nie była uruchomiona, przy następnym uruchomieniu będzie zawierała zaktualizowaną wtyczkę.

Podczas instalowania wtyczki w Android Studio należy wziąć pod uwagę kilka dodatkowych kwestii. W chwili pisania tego tekstu w procesie instalacji aplikacji Android Studio występuje błąd, który powoduje, że aktualizacje wtyczki nie zaczynają obowiązywać. Można to naprawić, wybierając opcję Zawsze instaluj za pomocą menedżera pakietów (wyłącza wdrażanie optymalizacji na Androidzie 11 i nowszych) w konfiguracji kompilacji wtyczki.

Dodatkowo podczas instalacji wtyczki Android Studio zgłasza błąd polegający na tym, że nie może znaleźć głównej aktywności do uruchomienia. Jest to oczekiwane, ponieważ wtyczka nie wykonuje żadnych działań (z wyjątkiem pustej intencji używanej do rozwiązania intencji). Aby wyeliminować błąd, zmień opcję Uruchom na Nic w konfiguracji kompilacji.

Konfiguracja wtyczki Android Studio Rysunek 1. Konfiguracja wtyczki Android Studio

Wtyczka proxy

Dostosowywanie aplikacji przy użyciu biblioteki Car UI wymaga RRO ukierunkowanego na każdą konkretną aplikację, która ma zostać zmodyfikowana, także wtedy, gdy dostosowania są identyczne w różnych aplikacjach. Oznacza to, że wymagany jest RRO na aplikację. Zobacz, które aplikacje korzystają z biblioteki Car UI.

Wtyczka proxy biblioteki Car UI to przykładowa biblioteka współdzielona wtyczki, która deleguje implementacje swoich komponentów do statycznej wersji biblioteki Car UI. Do tej wtyczki można zastosować RRO, które można wykorzystać jako pojedynczy punkt dostosowywania aplikacji korzystających z biblioteki Car UI bez konieczności implementowania funkcjonalnej wtyczki. Aby uzyskać więcej informacji na temat RRO, zobacz temat Zmiana wartości zasobów aplikacji w czasie wykonywania .

Wtyczka proxy to tylko przykład i punkt wyjścia do dostosowywania za pomocą wtyczki. W celu dostosowania wykraczającego poza RRO można zaimplementować podzbiór komponentów wtyczki i użyć wtyczki proxy do reszty lub zaimplementować wszystkie komponenty wtyczki całkowicie od zera.

Chociaż wtyczka proxy zapewnia pojedynczy punkt dostosowywania RRO dla aplikacji, aplikacje, które zrezygnują z używania wtyczki, nadal będą wymagać RRO bezpośrednio skierowanego do samej aplikacji.

Zaimplementuj interfejsy API wtyczek

Głównym punktem wejścia do wtyczki jest klasa com.android.car.ui.plugin.PluginVersionProviderImpl . Wszystkie wtyczki muszą zawierać klasę o dokładnie takiej nazwie i nazwie pakietu. Ta klasa musi mieć domyślny konstruktor i implementować interfejs PluginVersionProviderOEMV1 .

Wtyczki CarUi muszą działać z aplikacjami starszymi lub nowszymi niż wtyczka. Aby to ułatwić, wszystkie interfejsy API wtyczek mają wersję z literą V# na końcu nazwy klasy. Jeśli zostanie wydana nowa wersja biblioteki Car UI z nowymi funkcjami, będą one częścią wersji V2 komponentu. Biblioteka Car UI dokłada wszelkich starań, aby nowe funkcje działały w ramach starszego komponentu wtyczki. Na przykład poprzez konwersję nowego typu przycisku na pasku narzędzi na MenuItems .

Jednak aplikacja ze starszą wersją biblioteki Car UI nie może dostosować się do nowej wtyczki napisanej dla nowszych interfejsów API. Aby rozwiązać ten problem, umożliwiamy wtyczkom zwracanie różnych implementacji samych siebie w zależności od wersji OEM API obsługiwanej przez aplikacje.

PluginVersionProviderOEMV1 ma jedną metodę:

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

Ta metoda zwraca obiekt, który implementuje najwyższą wersję PluginFactoryOEMV# obsługiwaną przez wtyczkę, a jednocześnie jest mniejszy lub równy maxVersion . Jeśli wtyczka nie ma tak starej implementacji PluginFactory , może zwrócić null , w którym to przypadku używana jest statycznie połączona implementacja komponentów CarUi.

Aby zachować kompatybilność wsteczną z aplikacjami skompilowanymi ze starszymi wersjami statycznej biblioteki Car Ui, zaleca się obsługę maxVersion s 2, 5 i wyższych z poziomu implementacji klasy PluginVersionProvider w wtyczce. Wersje 1, 3 i 4 nie są obsługiwane. Aby uzyskać więcej informacji, zobacz PluginVersionProviderImpl .

PluginFactory to interfejs, który tworzy wszystkie pozostałe komponenty CarUi. Określa także, która wersja ich interfejsów powinna być używana. Jeśli wtyczka nie będzie dążyć do implementacji żadnego z tych komponentów, może zwrócić null w funkcji ich tworzenia (z wyjątkiem paska narzędzi, który posiada osobną funkcję customizesBaseLayout() ).

pluginFactory ogranicza, które wersje komponentów CarUi mogą być używane razem. Na przykład nigdy nie będzie pluginFactory , która będzie w stanie utworzyć wersję 100 Toolbar , a także wersję 1 elementu RecyclerView , ponieważ nie będzie gwarancji, że różnorodne wersje komponentów będą ze sobą współdziałać. Aby móc używać paska narzędzi w wersji 100, od programistów oczekuje się implementacji wersji pluginFactory , która tworzy pasek narzędzi w wersji 100, co następnie ogranicza opcje dotyczące wersji innych komponentów, które można utworzyć. Wersje innych komponentów mogą nie być równe, na przykład pluginFactoryOEMV100 może utworzyć ToolbarControllerOEMV100 i RecyclerViewOEMV70 .

pasek narzędzi

Układ podstawowy

Pasek narzędzi i „układ podstawowy” są ze sobą bardzo ściśle powiązane, stąd funkcja tworząca pasek narzędzi nazywa się installBaseLayoutAround . Układ podstawowy to koncepcja, która umożliwia umieszczenie paska narzędzi w dowolnym miejscu wokół zawartości aplikacji, co pozwala na umieszczenie paska narzędzi u góry/na dole aplikacji, pionowo wzdłuż boków, a nawet okrągłego paska narzędzi otaczającego całą aplikację. Osiąga się to poprzez przekazanie widoku do installBaseLayoutAround w celu zawinięcia paska narzędzi/układu podstawowego.

Wtyczka powinna przyjąć dostarczony widok, odłączyć go od elementu nadrzędnego, powiększyć własny układ wtyczki w tym samym indeksie elementu nadrzędnego i z tymi samymi parametrami LayoutParams co widok, który właśnie został odłączony, a następnie ponownie dołączyć widok gdzieś wewnątrz układu, który został po prostu napompowany. Na żądanie aplikacji powiększony układ będzie zawierał pasek narzędzi.

Aplikacja może zażądać układu podstawowego bez paska narzędzi. Jeśli tak, installBaseLayoutAround powinien zwrócić wartość null. W przypadku większości wtyczek to wszystko, co musi się wydarzyć, ale jeśli autor wtyczki chciałby zastosować np. dekorację wokół krawędzi aplikacji, nadal można to zrobić za pomocą układu podstawowego. Dekoracje te są szczególnie przydatne w przypadku urządzeń z ekranami nieprostokątnymi, ponieważ mogą wypchnąć aplikację w prostokątną przestrzeń i dodać czyste przejścia w nieprostokątną przestrzeń.

installBaseLayoutAround jest również przekazywany Consumer<InsetsOEMV1> . Za pomocą tego konsumenta można przekazać aplikacji informację, że wtyczka częściowo zakrywa zawartość aplikacji (za pomocą paska narzędzi lub w inny sposób). Aplikacja będzie wtedy wiedzieć, że ma dalej rysować w tym obszarze, ale trzymać z dala od niego wszelkie krytyczne komponenty, z którymi może wchodzić w interakcję użytkownik. Efekt ten wykorzystaliśmy w naszym projekcie referencyjnym, aby pasek narzędzi stał się półprzezroczysty i można było pod nim przewijać listy. Gdyby ta funkcja nie została zaimplementowana, pierwszy element na liście utknąłby pod paskiem narzędzi i nie można go było kliknąć. Jeśli ten efekt nie jest potrzebny, wtyczka może zignorować Konsumenta.

Przewijanie treści pod paskiem narzędzi Rysunek 2. Przewijanie treści pod paskiem narzędzi

Z punktu widzenia aplikacji, gdy wtyczka wyśle ​​nowe wstawki, otrzyma je z wszelkich działań lub fragmentów, które implementują InsetsChangedListener . Jeśli działanie lub fragment nie implementuje InsetsChangedListener , biblioteka Car Ui domyślnie obsłuży wstawki, stosując wstawki jako uzupełnienie Activity lub FragmentActivity zawierającego fragment. Biblioteka domyślnie nie stosuje wstawek do fragmentów. Oto przykładowy fragment implementacji, która stosuje wstawki jako uzupełnienie elementu RecyclerView w aplikacji:

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

Na koniec wtyczka otrzymuje podpowiedź dotyczącą fullscreen , która służy do wskazania, czy widok, który powinien zostać zawinięty, zajmuje całą aplikację, czy tylko jej małą sekcję. Można to wykorzystać, aby uniknąć stosowania niektórych dekoracji wzdłuż krawędzi, które mają sens tylko wtedy, gdy pojawiają się wzdłuż krawędzi całego ekranu. Przykładową aplikacją korzystającą z układów podstawowych innych niż pełnoekranowe są Ustawienia, w których każdy panel układu z dwoma panelami ma własny pasek narzędzi.

Ponieważ oczekuje się, że installBaseLayoutAround zwróci wartość null, gdy toolbarEnabled ma false , aby wtyczka wskazywała, że ​​nie chce dostosowywać układu podstawowego, musi zwrócić false z customizesBaseLayout .

Układ podstawowy musi zawierać FocusParkingView i FocusArea , aby w pełni obsługiwać sterowanie obrotowe. Widoki te można pominąć na urządzeniach, które nie obsługują funkcji obrotowych. FocusParkingView/FocusAreas są zaimplementowane w statycznej bibliotece CarUi, więc setRotaryFactories służy do udostępniania fabryk do tworzenia widoków na podstawie kontekstów.

Konteksty używane do tworzenia widoków Fokus muszą być kontekstem źródłowym, a nie kontekstem wtyczki. Widok FocusParkingView powinien znajdować się jak najbliżej pierwszego widoku w drzewie, ponieważ jest to obszar skupiony, podczas gdy użytkownik nie powinien go widzieć. FocusArea musi otaczać pasek narzędzi w układzie podstawowym, aby wskazać, że jest to strefa obrotowego przesuwania. Jeśli FocusArea nie jest dostępny, użytkownik nie może przejść do żadnego przycisku na pasku narzędzi za pomocą kontrolera obrotowego.

Kontroler paska narzędzi

Rzeczywisty zwrócony ToolbarController powinien być znacznie prostszy w implementacji niż układ podstawowy. Jego zadaniem jest przejmowanie informacji przekazywanych ustawiającym i wyświetlanie ich w układzie podstawowym. Informacje na temat większości metod można znaleźć w dokumencie Javadoc. Niektóre z bardziej złożonych metod omówiono poniżej.

getImeSearchInterface służy do wyświetlania wyników wyszukiwania w oknie IME (klawiatura). Może to być przydatne do wyświetlania/animowania wyników wyszukiwania obok klawiatury, na przykład jeśli klawiatura zajmowała tylko połowę ekranu. Większość funkcjonalności jest zaimplementowana w statycznej bibliotece CarUi, interfejs wyszukiwania we wtyczce udostępnia jedynie metody dla biblioteki statycznej umożliwiające uzyskanie wywołań zwrotnych TextView i onPrivateIMECommand . Aby to obsłużyć, wtyczka powinna używać podklasy TextView , która zastępuje onPrivateIMECommand i przekazuje wywołanie do podanego odbiornika jako TextView paska wyszukiwania.

setMenuItems po prostu wyświetla elementy MenuItems na ekranie, ale będą one wywoływane zaskakująco często. Ponieważ interfejs API wtyczki dla MenuItems jest niezmienny, za każdym razem, gdy zmieniany jest MenuItem, nastąpi zupełnie nowe wywołanie setMenuItems . Może się to zdarzyć w przypadku czegoś tak trywialnego, jak kliknięcie przełącznika MenuItem przez użytkownika i kliknięcie to powoduje przełączenie przełącznika. Dlatego ze względu na wydajność i animację zaleca się obliczanie różnicy między starą i nową listą MenuItems i aktualizowanie tylko tych widoków, które faktycznie się zmieniły. MenuItems udostępniają pole key , które może w tym pomóc, ponieważ klucz powinien być taki sam w przypadku różnych wywołań setMenuItems dla tego samego MenuItem.

Widok w stylu aplikacji

AppStyledView to kontener widoku, który w ogóle nie jest dostosowany. Można go użyć do zapewnienia obramowania wokół tego widoku, co odróżnia go od reszty aplikacji i wskazuje użytkownikowi, że jest to inny rodzaj interfejsu. Widok opakowany przez AppStyledView jest podany w setContent . AppStyledView może również mieć przycisk Wstecz lub Zamknij, zgodnie z żądaniem aplikacji.

AppStyledView nie wstawia od razu swoich widoków do hierarchii widoków, tak jak robi to installBaseLayoutAround , zamiast tego po prostu zwraca swój widok do biblioteki statycznej poprzez getView , który następnie dokonuje wstawienia. Położenie i rozmiar AppStyledView można również kontrolować, implementując getDialogWindowLayoutParam .

Konteksty

Wtyczka musi zachować ostrożność podczas korzystania z kontekstów, ponieważ istnieją zarówno konteksty wtyczek , jak i „źródłowe”. Kontekst wtyczki jest podawany jako argument metody getPluginFactory i jest jedynym kontekstem, w którym znajdują się zasoby wtyczki. Oznacza to, że jest to jedyny kontekst, którego można użyć do nadmuchania układów we wtyczce.

Jednakże kontekst wtyczki może nie mieć ustawionej prawidłowej konfiguracji. Aby uzyskać poprawną konfigurację, udostępniamy konteksty źródłowe w metodach tworzących komponenty. Kontekst źródłowy to zwykle działanie, ale w niektórych przypadkach może to być także usługa lub inny komponent Androida. Aby użyć konfiguracji z kontekstu źródłowego z zasobami z kontekstu wtyczki, należy utworzyć nowy kontekst za pomocą metody createConfigurationContext . Jeśli nie zostanie użyta prawidłowa konfiguracja, nastąpi naruszenie trybu ścisłego systemu Android, a zawyżone widoki mogą nie mieć prawidłowych wymiarów.

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

Zmiany trybu

Niektóre wtyczki mogą obsługiwać wiele trybów dla swoich komponentów, takich jak tryb sportowy lub tryb ekologiczny , które wizualnie się różnią. W CarUi nie ma wbudowanej obsługi takiej funkcjonalności, jednak nic nie stoi na przeszkodzie, aby wtyczka zaimplementowała ją całkowicie wewnętrznie. Wtyczka może monitorować dowolne warunki i ustalać, kiedy przełączać tryby, na przykład słuchać audycji. Wtyczka nie może wywołać zmiany konfiguracji w celu zmiany trybów, ale i tak nie zaleca się polegania na zmianach konfiguracji, ponieważ ręczna aktualizacja wyglądu każdego komponentu jest dla użytkownika wygodniejsza, a także pozwala na przejścia, które nie są możliwe w przypadku zmian konfiguracji.

Komponowanie Jetpacka

Wtyczki można implementować za pomocą Jetpack Compose, ale jest to funkcja na poziomie alfa i nie należy jej uważać za stabilną.

Wtyczki mogą używać ComposeView do tworzenia powierzchni obsługującej funkcję Compose do renderowania. Ten ComposeView będzie tym, co zostanie zwrócone do aplikacji przez metodę getView w komponentach.

Jednym z głównych problemów związanych z używaniem ComposeView jest to, że ustawia on znaczniki w widoku głównym układu w celu przechowywania zmiennych globalnych, które są współdzielone przez różne widoki ComposeView w hierarchii. Ponieważ identyfikatory zasobów wtyczki nie są podzielone na przestrzeni nazw oddzielnie od identyfikatorów aplikacji, może to powodować konflikty, gdy zarówno aplikacja, jak i wtyczka ustawiają znaczniki w tym samym widoku. Poniżej znajduje się niestandardowy ComposeViewWithLifecycle , który przenosi te zmienne globalne do ComposeView . Ponownie nie należy tego uważać za stabilne.

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)
//  }
}