Wtyczki interfejsu samochodowego

Używaj wtyczek do biblioteki interfejsu Car UI, by tworzyć pełne implementacje komponentu dostosowania w bibliotece interfejsu samochodu, zamiast korzystać z nakładek zasobów środowiska wykonawczego. (RRO). RRO umożliwiają zmianę tylko zasobów XML biblioteki interfejsu Car UI komponentów, co ogranicza zakres możliwości dostosowania.

Utwórz wtyczkę

Wtyczka biblioteki interfejsu Car UI to plik APK zawierający klasy implementujące zestaw Interfejsy API wtyczek. Interfejsy API wtyczek można skompilować w jako biblioteki statycznej.

Zobacz przykłady w narzędziach Soong i Gradle:

Soong

Przyjrzyjmy się przykładowi Song:

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ć w pliku manifestu zadeklarowanego dostawcy treści z następujące atrybuty:

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

android:authorities="com.android.car.ui.plugin" sprawia, że wtyczka jest wykrywalna do biblioteki UI samochodu. Dostawca musi zostać wyeksportowany, aby można było wysłać do niego zapytania na stronie w środowisku wykonawczym. Ponadto, jeśli atrybut enabled ma wartość false, zostanie użyta wartość domyślna. zamiast implementacji wtyczki. Treść klasa dostawcy nie musi istnieć. W takim przypadku dodaj tools:ignore="MissingClass" do definicji dostawcy. Zobacz przykład wpis w pliku 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>

Wreszcie, ze względów bezpieczeństwa, Podpisywanie aplikacji.

Wtyczki jako biblioteka udostępniona

W przeciwieństwie do bibliotek statycznych Androida, które są kompilowane bezpośrednio w aplikacje, Biblioteki udostępnione Androida są scalane w samodzielny plik APK, do którego odwołuje się aplikacja przez inne aplikacje w czasie działania.

Wtyczki wdrożone jako biblioteka udostępniona na Androidzie mają swoje klasy. są automatycznie dodawane do udostępnianego modułu klasy między aplikacjami. Gdy aplikacja, która korzysta z biblioteki Car UI określa zależność środowiska wykonawczego od biblioteki współdzielonej wtyczki, program classloader może uzyskać dostęp do klas z biblioteki udostępnionej wtyczki. Wdrożone wtyczki Zwykłe aplikacje na Androida (nie zasoby wspólne) mogą negatywnie wpływać na wybór aplikacji. czasy rozpoczęcia.

Wdrażanie i tworzenie bibliotek udostępnionych

Programowanie przy użyciu bibliotek udostępnionych Androida przebiega prawie tak samo jak w przypadku zwykłego Androida. aplikacji mobilnych.

  • Użyj tagu library pod tagiem application z pakietem wtyczki nazwa w pliku manifestu aplikacji wtyczki:
    <application>
        <library android:name="com.chassis.car.ui.plugin" />
        ...
    </application>
  • Skonfiguruj regułę kompilacji Soong android_app (Android.bp) z użyciem AAPT flaga shared-lib, która służy do tworzenia zasobów wspólnych:
android_app {
  ...
  aaptflags: ["--shared-lib"],
  ...
}

Zależności od bibliotek udostępnionych

W przypadku każdej aplikacji w systemie, która korzysta z biblioteki interfejsu Car UI, dodaj parametr uses-library w pliku manifestu aplikacji w sekcji tag application z nazwą pakietu wtyczki:

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

Instalowanie wtyczki

Wtyczki MUSZĄ być wstępnie zainstalowane na partycji systemowej poprzez dołączenie modułu za PRODUCT_PACKAGES. Wstępnie zainstalowany pakiet można zaktualizować w podobny sposób z innych zainstalowanych aplikacji.

Jeśli aktualizujesz w systemie wtyczkę, wszystkie aplikacje używające tej wtyczki zamknij automatycznie. Po ponownym otwarciu aplikacji przez użytkownika wprowadzone zostaną zaktualizowane zmiany. Jeśli aplikacja nie była uruchomiona, po następnym uruchomieniu zostanie zaktualizowana wtyczki.

Podczas instalowania wtyczki w Android Studio które trzeba wziąć pod uwagę. W momencie pisania wystąpił błąd w procesu instalacji aplikacji Android Studio, który powoduje aktualizację wtyczki nie będzie obowiązywać. Można to naprawić, wybierając opcję Zawsze instaluj za pomocą menedżera pakietów (wyłącza optymalizacje wdrażania w Androidzie 11 i nowszych) w konfiguracji kompilacji wtyczki.

Dodatkowo podczas instalowania wtyczki Android Studio zgłasza błąd, nie może znaleźć głównego działania do uruchomienia. To normalne, bo wtyczka nie nie mają żadnych działań (z wyjątkiem pustej intencji używanej do rozwiązania intencji). Do Wyeliminuj błąd, zmień opcję Launch (Uruchom) na Nothing (Brak) w kompilacji konfiguracji.

Konfiguracja wtyczki Android Studio Rysunek 1. Konfiguracja wtyczki Android Studio

Wtyczka serwera proxy

Dostosowywanie aplikacje korzystające z biblioteki interfejsu Car wymaga RRO kierowanego na każdą konkretną aplikację, która ma zostać zmodyfikowana; Dotyczy to też sytuacji, w których dostosowania są identyczne w poszczególnych aplikacjach. Oznacza to, że RRO w każdym przypadku Aplikacja jest wymagana. Zobacz, które aplikacje korzystają z biblioteki interfejsu Car UI.

Przykładem jest wtyczka proxy biblioteki Car UI biblioteka współużytkowana wtyczki, która przekazuje swoje implementacje komponentu statycznemu wersji biblioteki interfejsu użytkownika. Na tę wtyczkę można kierować RRO, czyli Używany jako pojedynczy punkt dostosowania w aplikacjach korzystających z biblioteki Car UI bez konieczności wdrażania funkcjonalnej wtyczki. Więcej informacji na temat: Instrukcje dla RRO znajdziesz w sekcji Zmienianie wartości zasobów aplikacji na stronie w czasie działania.

Wtyczka serwera proxy to tylko przykład i punkt wyjścia do dostosowywania ani wtyczki. Aby dostosować go do własnych potrzeb, można zaimplementować podzbiór wtyczki a w pozostałych przypadkach użyj wtyczki serwera proxy lub wdróż całą wtyczkę całkowicie od zera.

Wtyczka serwera proxy zapewnia jeden punkt dostosowania RRO dla aplikacji, aplikacje, które zrezygnowały z korzystania z wtyczki, nadal będą wymagały RRO, które jest kierowana na samą aplikację.

Wdróż interfejsy API wtyczki

Głównym punktem wejścia wtyczki jest com.android.car.ui.plugin.PluginVersionProviderImpl zajęcia. Wszystkie wtyczki muszą dodaj klasę o tej samej nazwie i nazwie pakietu. Te zajęcia muszą mieć domyślnego konstruktora i wdrażać interfejs PluginVersionProviderOEMV1.

Wtyczki CarUi muszą działać w aplikacjach, które są starsze lub nowsze od wtyczki. Do aby to ułatwić, wszystkie interfejsy API wtyczek mają wersję V# na końcu classname (nazwa klasy). Jeśli zostanie udostępniona nowa wersja biblioteki interfejsu Car UI z nowymi funkcjami, wchodzą w skład komponentu w wersji V2. Biblioteka UI samochodu żeby nowe funkcje działały w zakresie starszego komponentu wtyczki. Możesz na przykład przekonwertować przycisk nowego typu na pasku narzędzi na MenuItems.

Jednak aplikacji ze starszą wersją biblioteki UI samochodu nie można dostosować do nowej wtyczki napisanej w nowszych interfejsach API. Aby rozwiązać ten problem, zezwalamy wtyczkom na zwracają różne implementacje w zależności od wersji interfejsu API OEM obsługiwanych przez aplikacje.

Pole PluginVersionProviderOEMV1 zawiera 1 metodę:

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

Ta metoda zwraca obiekt, w którym zaimplementowano najwyższą wersję PluginFactoryOEMV# obsługiwane przez wtyczkę, ale nadal będzie mniejsze niż lub równe maxVersion. Jeśli wtyczka nie ma implementacji PluginFactory, może zwrócić null. W takim przypadku statycznie- stosowane są połączone implementacje komponentów CarUi.

Aby zachować zgodność wsteczną z aplikacjami skompilowanymi na starszych wersji statycznej biblioteki Car Ui, zaleca się obsługę maxVersion z 2, 5 lub wyższego poziomu z implementacji wtyczki klasy PluginVersionProvider. Wersje 1, 3 i 4 nie są obsługiwane. Dla: więcej informacji znajdziesz w PluginVersionProviderImpl

PluginFactory to interfejs, który tworzy wszystkie pozostałe elementy CarUi. Określa też, której wersji ich interfejsów należy użyć. Jeśli wtyczka nie dąży do implementacji żadnego z tych komponentów, może zwrócić null w funkcji tworzenia (z wyjątkiem paska narzędzi, w którym jest osobną funkcję customizesBaseLayout()).

pluginFactory określa, które wersje komponentów CarUi mogą być używane razem. Na przykład nigdy nie będzie elementu pluginFactory, który mógłby utworzyć wersji 100 systemu Toolbar oraz wersji 1 systemu RecyclerView, nie daje żadnej gwarancji, że szeroki wybór wersji komponentów nad ich współpracą. Aby można było używać paska narzędzi w wersji 100, programiści powinni udostępnia implementację wersji pluginFactory, która tworzy paska narzędzi w wersji 100, która ogranicza opcje w wersjach innych które można utworzyć. Wersje innych komponentów mogą nie równy, np. pluginFactoryOEMV100 może utworzyć ToolbarControllerOEMV100 i RecyclerViewOEMV70.

Pasek narzędzi

Układ podstawowy

Pasek narzędzi i „układ podstawowy” są ściśle powiązane, dlatego funkcja który tworzy pasek narzędzi, nazywa się installBaseLayoutAround. układ podstawowy umożliwia umieszczenie paska narzędzi w dowolnym miejscu treści, aby umieścić pasek narzędzi u góry/u dołu aplikacji, w pionie wzdłuż boków, a nawet okrągły pasek narzędzi otaczający całą aplikację. To jest można uzyskać, przekazując widok do interfejsu installBaseLayoutAround dla paska narzędzi/bazy. układ, który można zawinąć.

Wtyczka powinna przyjąć udostępniony widok, odłączyć go od elementu nadrzędnego, z własnym układem wtyczki w tym samym indeksie elementu nadrzędnego i tym samym LayoutParams jako widok, który właśnie został odłączony, a następnie ponownie dołącz widok gdzieś w układzie, który został przed chwilą powiększony. Powiększony układ zawierać pasek narzędzi, jeśli poprosi o to aplikacja.

Aplikacja może żądać układu podstawowego bez paska narzędzi. Jeśli tak, Funkcja installBaseLayoutAround powinna zwrócić wartość null. To wszystko. ale nie jest to konieczne, ale jeśli autor wtyczki zechce go zastosować np. dekoracja wokół krawędzi aplikacji, co nadal można zrobić przy użyciu układu podstawowego. Te są szczególnie przydatne w przypadku urządzeń z nieprostokątnymi ekranami, umieścić aplikację w prostokątnej przestrzeni i umieścić wyraźne przejścia do przestrzeni nieprostokątnej.

installBaseLayoutAround przekroczył(a) też ocenę Consumer<InsetsOEMV1>. Ten można użyć do poinformowania aplikacji, że wtyczka jest częściowo zasłaniając zawartość aplikacji (za pomocą paska narzędzi lub w inny sposób). Aplikacja będzie aby móc dalej rysować w tym obszarze, ale zachować wszystkie kluczowe elementy interaktywne, z każdego z nich. Ten efekt jest używany w naszym projekcie referencyjnym, aby zapewnić półprzezroczystego paska narzędzi, a pod nim listy przewijają się. Jeśli ta funkcja była nie zaimplementowano, pierwszy element na liście utknie pod paskiem narzędzi i nieklikalny. Jeśli ten efekt nie jest potrzebny, wtyczka może zignorować parametr Klientowi.

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 ich z wszelkich działań lub fragmentów, które implementują InsetsChangedListener. Jeśli działanie lub fragment nie implementuje funkcji InsetsChangedListener, interfejs użytkownika Car Ui będzie domyślnie obsługiwać wstawki, stosując wstawki jako dopełnienie Activity lub FragmentActivity zawierający fragment. Biblioteka nie domyślnie stosuje wstawki do fragmentów. Oto przykładowy fragment i stosuje wstawki jako dopełnienie w elemencie RecyclerView w tabeli aplikacja:

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 wskazówkę fullscreen, która wskazuje, czy widok, który należy uwzględnić, obejmuje całą aplikację lub tylko jej małą sekcję. Pozwala to uniknąć stosowania dekoracji na krawędzi, mają sens tylko wtedy, gdy wyświetlają się wzdłuż krawędzi ekranu. Przykład aplikacji, która korzysta z niepełnoekranowych układów podstawowych, to Ustawienia, w której każdy panel układ z dwoma panelami ma własny pasek narzędzi.

Ponieważ funkcja installBaseLayoutAround powinna zwrócić wartość null, Parametr toolbarEnabled ma wartość false, co oznacza, że wtyczka wskazuje, że nie chcesz dostosować układ podstawowy, musi on zwracać wartość false z customizesBaseLayout

Układ podstawowy musi zawierać elementy FocusParkingView i FocusArea, aby w pełni obsługują pokrętła. Te widoki można pominąć na urządzeniach, które ale nie obsługują pokrętła. FocusParkingView/FocusAreas są zaimplementowane w statycznej biblioteki CarUi, więc setRotaryFactories umożliwia fabrykom tworzyć widoki na podstawie kontekstów.

Konteksty użyte do utworzenia widoków skupienia muszą być kontekstem źródła, a nie o kontekście wtyczki. Wartość FocusParkingView powinna być najbliższa pierwszemu widokowi które można kontrolować, skupiając się na aspektach, nie może być widoczna dla użytkownika. FocusArea musi otaczać pasek narzędzi w układ podstawowy, aby wskazać, że jest to strefa obrotowa. Jeśli FocusArea nie jest użytkownik nie może przejść do żadnych przycisków na pasku narzędzi z z kontrolerem obrotowym.

Kontroler paska narzędzi

Rzeczywista wartość ToolbarController powinna być znacznie łatwiejsza do niż w układzie podstawowym. Jego zadaniem jest przekazywanie informacji i wyświetlaj je w układzie podstawowym. Więcej informacji na ten temat można znaleźć w dokumencie Javadoc . większości metod. Poniżej omówiliśmy niektóre z bardziej złożonych metod.

getImeSearchInterface służy do wyświetlania wyników wyszukiwania w IME (klawiaturze) okno. Jest to przydatne do wyświetlania/animowania wyników wyszukiwania obok klawiatury – np. zajmowała tylko połowę ekranu. Większość jest zaimplementowana w bibliotece statycznej CarUi, udostępnia tylko metody dla biblioteki statycznej umożliwiające TextView i onPrivateIMECommand wywołań zwrotnych. Żeby to umożliwić, wtyczka powinien używać podklasy TextView, która zastępuje onPrivateIMECommand i przekazuje wywołanie podanego detektora jako TextView na pasku wyszukiwania.

setMenuItems po prostu wyświetla na ekranie element MenuItems, ale ma on nazwę zaskakująco często. Interfejs API wtyczki dla MenuItems jest stały, Element MenuItem został zmieniony, nastąpi zupełnie nowe wywołanie funkcji setMenuItems. Może to spowodować może się zdarzyć w przypadku czegoś tak błahego, jak to, że użytkownik kliknął przełącznik MenuItem, spowodowało przełączenie przełącznika. Zarówno w przypadku wydajności, jak i animacji, dlatego zachęcamy do obliczania różnicy między starym a nowym kontem MenuItems oraz aktualizować tylko te widoki, które rzeczywiście się zmieniły. Element menu podaj pole key, które może w tym pomóc, ponieważ klucz powinien być taki sam w różnych wywołaniach setMenuItems dla tego samego elementu MenuItem.

Widok stylu aplikacji

AppStyledView to kontener dla widoku, który w ogóle nie jest dostosowany. it można ustawić za pomocą obramowania pozostałej części aplikacji i poinformować użytkownika, że jest to inny rodzaj za pomocą prostego interfejsu online. Widok opakowany przez AppStyledView jest setContent AppStyledView może też mieć przycisk Wstecz lub Zamknij, żądania aplikacji.

Element AppStyledView nie wstawia od razu swoich widoków do hierarchii widoków jak ma to miejsce w przypadku installBaseLayoutAround, zwraca go tylko statyczną za pomocą funkcji getView, która następnie przeprowadza wstawianie. Pozycja rozmiaru pliku AppStyledView można również kontrolować, implementując getDialogWindowLayoutParam

Konteksty

Podczas korzystania z kontekstów wtyczka musi być ostrożna, ponieważ zawiera ona zarówno wtyczkę, „source” kontekstach. Kontekst wtyczki jest podawany jako argument dla getPluginFactory i jest jedynym kontekstem, w którym występuje parametr zasobach wtyczki. Oznacza to, że jest to jedyny kontekst, którego można użyć do powiększa układy we wtyczce.

Jednak kontekst wtyczki może nie mieć ustawionej prawidłowej konfiguracji. Do aby uzyskać prawidłową konfigurację, w metodach tworzenia Kontekstem źródła jest zwykle działanie, ale w niektórych przypadkach może być również Usługą lub innym komponentem Androida. Aby użyć konfiguracji z z zasobami z kontekstu wtyczki, nowy kontekst musi być utworzone za pomocą funkcji createConfigurationContext. Jeśli poprawna konfiguracja jest nieprawidłowa pojawi się naruszenie trybu ścisłego w Androidzie i większa liczba wyświetleń może ma nieprawidłowe wymiary.

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

Zmiany trybu

Niektóre wtyczki mogą obsługiwać wiele trybów komponentów, na przykład tryb sportowy lub eko, które wyglądają indywidualnie. Brak wbudowanej obsługi takich funkcji w CarUi, ale nic nie stoi na przeszkodzie, całkowicie wewnętrznie wdrożyć wtyczkę. Wtyczka może monitorować w warunkach, w których chce określić, kiedy przełączyć tryby: nasłuchiwanie transmisji. Wtyczka nie może wywołać zmiany konfiguracji do zmiany trybów, ale nie zalecamy polegania na zmianach konfiguracji a ręczna aktualizacja wyglądu każdego komponentu jest płynniejsza. dla użytkownika, a także umożliwia przejścia, które są niedostępne w przypadku zmian konfiguracji.

Jetpack Compose

Wtyczki można wdrażać za pomocą Jetpack Compose, ale jest to poziom alfa i nie powinny być uważane za stabilne.

Wtyczki, których można używać ComposeView aby utworzyć platformę z obsługą funkcji tworzenia wiadomości, na której będą renderowane. ComposeView będzie jakie dane są zwracane do aplikacji za pomocą metody getView w komponentach.

Jednym z poważnych problemów z używaniem atrybutu ComposeView jest to, że ustawia on tagi w widoku głównym. w układzie, aby przechowywać zmienne globalne, które są udostępniane różnych obiektów ComposeView w hierarchii. Ponieważ identyfikatory zasobów wtyczki nie są w przestrzeni nazw niezależnie od aplikacji, może to powodować konflikty, gdy tagów aplikacji i zestawu wtyczek w tym samym widoku. Niestandardowy ComposeViewWithLifecycle, który przenosi te zmienne globalne do sekcji ComposeView podano poniżej. Również w tym przypadku nie należy traktować tego parametru jako stabilnego.

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