Wtyczki interfejsu samochodowego

Zamiast używać nakładek zasobów w czasie wykonywania (RRO), użyj wtyczek biblioteki Car UI do tworzenia pełnych implementacji dostosowań komponentów w bibliotece Car UI. RRO umożliwiają zmianę tylko zasobów XML komponentów biblioteki Car UI, co ogranicza zakres dostosowywania.

Tworzenie wtyczki

Wtyczka biblioteki Car UI to plik APK zawierający klasy, które implementują zestaw interfejsów API wtyczki. Interfejsy API w pluginach można skompilować w pluginie jako bibliotekę statyczną.

Przykłady w Soong i Gradle:

Soong

Oto przykład Soong:

android_app {
    name: "my-plugin",

    min_sdk_version: "28",
    target_sdk_version: "30",
    aaptflags: ["--shared-lib"],
    sdk_version: "current",

    manifest: "src/main/AndroidManifest.xml",
    srcs: ["src/main/java/**/*.java"],
    resource_dirs: ["src/main/res"],
    static_libs: [
        "car-ui-lib-oem-apis",
    ],
    // Disable optimization is mandatory to prevent R.java class from being
    // stripped out
    optimize: {
        enabled: false,
    },

    certificate: ":my-plugin-certificate",
}

Gradle

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

W pliku manifestu wtyczki musi być określony dostawca treści z tymi atrybutami:

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

android:authorities="com.android.car.ui.plugin" sprawia, że wtyczka jest dostępna w bibliotece Car UI. Dostawca musi zostać wyeksportowany, aby można było wysyłać do niego zapytania w czasie wykonywania. Jeśli atrybut enabled ma wartość false, zamiast implementacji wtyczki zostanie użyta domyślna implementacja. Klasa dostawcy treści nie musi istnieć. W takim przypadku pamiętaj, aby dodać parametr tools:ignore="MissingClass" do definicji dostawcy. Poniżej znajdziesz przykładowy wpis w pliku manifestu:

    <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, jako środek bezpieczeństwa, podpisz aplikację.

Wtyczki jako zasoby wspólne

W przeciwieństwie do bibliotek statycznych na Androida, które są kompilowane bezpośrednio w aplikacji, biblioteki współdzielone na Androida są kompilowane w samodzielny plik APK, do którego inne aplikacje odwołują się w czasie wykonywania.

W przypadku wtyczek zaimplementowanych jako współdzielone biblioteki Androida klasy są automatycznie dodawane do współdzielonego ładownika klas między aplikacjami. Gdy aplikacja korzystająca z biblioteki Car UI określa zależność w czasie wykonywania od biblioteki udostępnionej wtyczki, jej ładowarka klas może uzyskać dostęp do klas tej biblioteki. Wtyczki zaimplementowane jako zwykłe aplikacje na Androida (a nie jako biblioteki współdzielone) mogą negatywnie wpływać na czas uruchamiania aplikacji.

wdrażać i tworzyć biblioteki udostępnione.

Tworzenie aplikacji z użyciem udostępnionych bibliotek Androida jest podobne do tworzenia zwykłych aplikacji na Androida, ale różni się od nich kilkoma kluczowymi elementami.

  • Użyj tagu library pod tagiem application z nazwą pakietu w pliku manifestu aplikacji:
    <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 służy do kompilowania współdzielonej biblioteki:
android_app {
  ...
  aaptflags: ["--shared-lib"],
  ...
}

Zależności dotyczące bibliotek udostępnionych

W przypadku każdej aplikacji w systemie, która korzysta z biblioteki Car UI, dodaj tag uses-library w pliku manifestu 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>

Instalowanie wtyczki

Wtyczki MUSZĄ być wstępnie zainstalowane na partycji systemowej poprzez uwzględnienie modułu w PRODUCT_PACKAGES. Preinstalowany pakiet można aktualizować podobnie jak każdą inną zainstalowaną aplikację.

Jeśli aktualizujesz istniejący w systemie dodatek, wszystkie aplikacje korzystające z tego dodatku zostaną automatycznie zamknięte. Gdy użytkownik ponownie otworzy dokument, zobaczy wprowadzone zmiany. Jeśli aplikacja była wyłączona, przy następnym uruchomieniu będzie zawierać zaktualizowany wtyczek.

Podczas instalowania wtyczki w Android Studio należy wziąć pod uwagę kilka dodatkowych kwestii. W momencie pisania tego tekstu w procesie instalowania aplikacji Android Studio występuje błąd, który powoduje, że aktualizacje wtyczki nie wchodzą w życie. Aby rozwiązać ten problem, w konfiguracji kompilacji wtyczki wybierz opcję Zawsze instaluj za pomocą menedżera pakietów (wyłącza optymalizacje wdrożenia w Androidzie 11 i nowszych).

Ponadto podczas instalowania wtyczki Android Studio zgłasza błąd, że nie może znaleźć głównej czynności do uruchomienia. To jest oczekiwane, ponieważ wtyczka nie ma żadnych działań (z wyjątkiem pustej intencji używanej do rozwiązywania intencji). Aby wyeliminować błąd, zmień opcję Uruchom na Nic w konfiguracji kompilacji.

Konfiguracja wtyczki w Android Studio Rysunek 1. Konfiguracja wtyczki w Android Studio

Wtyczka proxy

Personalizacja aplikacji za pomocą biblioteki interfejsu użytkownika samochodu wymaga RRO, które kieruje na każdą konkretną aplikację, którą należy zmodyfikować, w tym w przypadku, gdy personalizacja jest identyczna w różnych aplikacjach. Oznacza to, że wymagana jest RRO na aplikację. Sprawdź, które aplikacje korzystają z biblioteki Car UI.

Wtyczka proxy biblioteki Car UI to przykładowa biblioteka udostępniona, która deleguje implementacje komponentów do statycznej wersji biblioteki Car UI. Ten wtyczka może być kierowany na RRO, który może służyć jako jedno miejsce do dostosowywania aplikacji korzystających z biblioteki Car UI bez konieczności implementowania funkcjonalnego wtyczka. Więcej informacji o RRO znajdziesz w artykule Zmienianie wartości zasobów aplikacji w czasie wykonywania.

Wtyczka proxy to tylko przykład i punkt wyjścia do dostosowania za pomocą wtyczki. Aby dostosować funkcje wykraczające poza RRO, możesz zaimplementować podzbiór komponentów wtyczki i użyć wtyczki proxy dla reszty lub wdrożyć wszystkie komponenty wtyczki od podstaw.

Chociaż wtyczka proxy umożliwia jednopunktową personalizację RRO dla aplikacji, aplikacje, które nie korzystają z tej wtyczki, nadal będą wymagać RRO, które bezpośrednio kieruje się na samą aplikację.

Implementowanie interfejsów API wtyczki

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

Wtyczki CarUi muszą działać z aplikacją, która jest starsza lub nowsza od wtyczki. Aby ułatwić to zadanie, wszystkie interfejsy API wtyczek są numerowane za pomocą V# na końcu nazwy klasy. Jeśli zostanie wydana nowa wersja biblioteki Car UI z nowymi funkcjami, będą one dostępne w wersji V2 komponentu. Biblioteka Car UI dokłada wszelkich starań, aby nowe funkcje działały w ramach zakresu starszego komponentu wtyczki. Na przykład przez przekształcenie nowego typu przycisku na pasku narzędzi w element MenuItems.

Aplikacja ze starszą wersją biblioteki Car UI nie może jednak dostosować się do nowej wtyczki napisanej z użyciem nowszych interfejsów API. Aby rozwiązać ten problem, zezwalamy wtyczkom na zwracanie różnych implementacji na podstawie wersji interfejsu OEM API obsługiwanej przez aplikacje.

PluginVersionProviderOEMV1 zawiera 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ę, ale jest mniejszy lub równy maxVersion. Jeśli wtyczka nie ma tak starej implementacji PluginFactory, może zwrócić null. W takim przypadku używana jest implementacja komponentów CarUi połączona statycznie.

Aby zachować zgodność wsteczną z aplikacjami skompilowanymi w starszych wersjach statycznej biblioteki Car Ui, zalecamy obsługę wartości maxVersion 2, 5 i wyższych w ramach implementacji klasy PluginVersionProvider w pliku pluginu. Wersje 1, 3 i 4 nie są obsługiwane. Więcej informacji znajdziesz w artykule PluginVersionProviderImpl.

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

pluginFactory określa, które wersje komponentów CarUi mogą być używane razem. Na przykład nigdy nie będzie pluginFactory, które może tworzyć wersję 100 elementu Toolbar i również wersję 1 elementu RecyclerView, ponieważ nie ma gwarancji, że tak wiele różnych wersji komponentów będzie ze sobą współpracować. Aby używać paska narzędzi w wersji 100, deweloperzy muszą udostępnić implementację wersji pluginFactory, która tworzy pasek narzędzi w wersji 100. Ogranicza to opcje wersji innych komponentów, które można utworzyć. Wersje innych komponentów mogą się różnić. Na przykład pluginFactoryOEMV100 może tworzyć ToolbarControllerOEMV100 i RecyclerViewOEMV70.

Pasek narzędzi

Podstawowy układ

Pasek narzędzi i „schemat podstawowy” są ze sobą bardzo powiązane, dlatego funkcja tworząca pasek narzędzi nosi nazwę installBaseLayoutAround. Układ podstawowy to koncepcja, która umożliwia umieszczenie paska narzędzi w dowolnym miejscu na treściach aplikacji. Może to być pasek narzędzi u góry lub u dołu aplikacji, pionowo wzdłuż boków, a nawet okrągły pasek narzędzi otaczający całą aplikację. Aby to osiągnąć, należy przekazać widok do installBaseLayoutAround, aby pasek narzędzi lub układ podstawowy mógł się owinąć.

Wtyczka powinna wziąć podany widok, odłączyć go od elementu nadrzędnego, napompować własny układ wtyczki w tym samym indeksie elementu nadrzędnego i z tym samym LayoutParams co widok, który został właśnie odłączony, a następnie ponownie dołączyć widok gdzieś wewnątrz układu, który został właśnie napompowany. Rozwinięty układ będzie zawierać pasek narzędzi, jeśli aplikacja o to poprosi.

Aplikacja może poprosić o schemat podstawowy bez paska narzędzi. Jeśli tak, funkcja installBaseLayoutAround powinna zwrócić wartość null. W przypadku większości wtyczek to wszystko, co trzeba zrobić.Jeśli jednak autor wtyczki chce np. zastosować ozdobę na krawędzi aplikacji, może to zrobić za pomocą układu podstawowego. Te ozdoby są szczególnie przydatne na urządzeniach z ekranami o nieprostokątnych kształtach, ponieważ mogą przesunąć aplikację do prostokątnej przestrzeni i dodać płynne przejścia do przestrzeni nieprostokątnej.

Do funkcji installBaseLayoutAround przekazywany jest też element Consumer<InsetsOEMV1>. Ten konsument może być używany do informowania aplikacji, że wtyczka częściowo zakrywa zawartość aplikacji (za pomocą paska narzędzi lub w inny sposób). Aplikacja będzie wtedy wiedzieć, że ma rysować w tym obszarze, ale nie będzie uwzględniać żadnych ważnych elementów, z którymi użytkownik może wchodzić w interakcję. Ten efekt jest używany w naszym projekcie referencyjnym, aby pasek narzędzi był półprzezroczysty, a listy się pod nim przewijały. Gdyby ta funkcja nie była zaimplementowana, pierwszy element na liście znajdowałby się pod paskiem narzędzi i nie można byłoby go 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 wysyła nowe wstawione elementy, aplikacja otrzymuje je z dowolnych działań lub fragmentów, które implementują InsetsChangedListener. Jeśli aktywność lub fragment nie implementuje interfejsu InsetsChangedListener, biblioteka CarUi domyślnie będzie obsługiwać wstawki, stosując je jako wypełnienie do interfejsu Activity lub FragmentActivity zawierającego fragment. Domyślnie biblioteka nie stosuje wstawek do fragmentów. Oto przykładowy fragment kodu implementacji, który stosuje w aplikacji RecyclerView jako wypełnienie:

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

W pluginie jest też wyświetlany podpowiedź fullscreen, który wskazuje, czy widok, który ma być opakowany, zajmuje całą aplikację czy tylko jej niewielką część. Dzięki temu możesz uniknąć stosowania niektórych dekoracji na krawędzi, które mają sens tylko wtedy, gdy pojawiają się na krawędzi całego ekranu. Przykładem aplikacji, która używa układów podstawowych niepełnego ekranu, jest Ustawienia. Każda z paneli w układzie dwupanelowym ma własny pasek narzędzi.

Funkcja installBaseLayoutAround powinna zwracać wartość null, gdy parametr toolbarEnabled ma wartość false. Aby plugin mógł wskazać, że nie chce dostosowywać podstawowego układu, musi zwracać wartość false z funkcji customizesBaseLayout.

Aby w pełni obsługiwać elementy sterujące obrotowe, układ podstawowy musi zawierać element FocusParkingView i element FocusArea. Te widoki można pominąć na urządzeniach, które nie obsługują elementów obrotowych. FocusParkingView/FocusAreas są implementowane w statycznej bibliotece CarUi, więc do tworzenia widoków na podstawie kontekstów używa się setRotaryFactories.

Konteksty używane do tworzenia widoków Focus muszą być kontekstem źródłowym, a nie kontekstem wtyczki. Element FocusParkingView powinien być jak najbliżej pierwszego widoku w drzewie, ponieważ to on jest widoczny, gdy użytkownik nie ma możliwości wyboru. Element FocusArea musi otaczać pasek narzędzi w układzie bazowym, aby wskazywać, że jest to strefa przesunięcia obrotowego. Jeśli FocusArea nie jest podany, użytkownik nie może przejść do żadnych przycisków na pasku narzędzi za pomocą kontrolera obrotowego.

Kontroler paska narzędzi

Zwracana wartość ToolbarController powinna być znacznie łatwiejsza do zaimplementowania niż podstawowy układ. Jego zadaniem jest pobieranie informacji przekazywanych przez zestawiciele i wyświetlanie ich w układzie podstawowym. Informacje o większości metod znajdziesz w dokumentacji Javadoc. Poniżej omawiamy niektóre bardziej złożone metody.

getImeSearchInterface służy do wyświetlania wyników wyszukiwania w oknie IME (klawiatury). Może to być przydatne do wyświetlania lub animowania wyników wyszukiwania obok klawiatury, np. jeśli klawiatura zajmuje tylko połowę ekranu. Większość funkcji jest implementowana w statycznej bibliotece CarUi, a interfejs wyszukiwania we wtyczce udostępnia tylko metody dla statycznej biblioteki, aby uzyskać wywołania zwrotne TextViewonPrivateIMECommand. Aby to umożliwić, w pluginie należy użyć podklasy TextView, która zastąpi funkcję onPrivateIMECommand i przekaże wywołanie do przekazanego odbiornika jako TextView paska wyszukiwania.

setMenuItems wyświetla tylko elementy menu na ekranie, ale jest wywoływany zaskakująco często. Interfejs API wtyczki dla elementów menu jest niezmienny, więc gdy nastąpi zmiana elementu menu, zostanie wywołane nowe wywołanie setMenuItems. Może się to zdarzyć w przypadku czegoś tak trywialnego jak kliknięcie przez użytkownika przełącznika MenuItem, które powoduje jego włączenie lub wyłączenie. Z powodów związanych z wydajnością i animowaniem zalecamy obliczenie różnicy między starą a nową listą menu i zaktualizowanie tylko tych widoków, które się zmieniły. W elementach menu jest dostępne pole key, które może Ci w tym pomóc, ponieważ klucz powinien być taki sam w różnych wywołaniach funkcji setMenuItems dla tego samego elementu menu.

AppStyledView

AppStyledView to kontener widoku, który nie jest w żaden sposób dostosowany. Możesz użyć tego elementu, aby dodać do widoku obramowanie, które wyróżni go na tle reszty aplikacji i wskaże użytkownikowi, że jest to inny rodzaj interfejsu. Widok zawinięty przez AppStyledView jest podany w setContent. Element AppStyledView może też zawierać przycisk Wstecz lub Zamknij, jeśli aplikacja tego wymaga.

Element AppStyledView nie wstawia od razu widoków do hierarchii widoków, jak to robi element installBaseLayoutAround, tylko zwraca widok do biblioteki statycznej za pomocą elementu getView, który następnie wykonuje wstawienie. Pozycję i rozmiar AppStyledView można też kontrolować, stosując element getDialogWindowLayoutParam.

Konteksty

Wtyczka musi ostrożnie używać kontekstów, ponieważ istnieją konteksty wtyczki i konteksty „źródła”. Kontekst wtyczki jest przekazywany jako argument do funkcji getPluginFactory i jest jedynym kontekstem, który zawiera zasoby wtyczki. Oznacza to, że jest to jedyny kontekst, który można wykorzystać do napełniania układów w pluginie.

Kontekst wtyczki może jednak nie mieć prawidłowej konfiguracji. Aby uzyskać prawidłową konfigurację, w metodach tworzących komponenty podajemy konteksty źródła. Kontekst źródłowy to zwykle aktywność, ale w niektórych przypadkach może to być też 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ą funkcji createConfigurationContext. Jeśli nie użyjesz prawidłowej konfiguracji, nastąpi naruszenie rygorystycznego trybu Androida, a wyświetlane widoki mogą mieć nieprawidłowe wymiary.

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

Zmiany trybu

Niektóre wtyczki mogą obsługiwać kilka trybów dla swoich komponentów, np. tryb sportowy lub tryb ekonomiczny, które różnią się wizualnie. W CarUi nie ma wbudowanego wsparcia dla takich funkcji, ale nic nie stoi na przeszkodzie, aby wdrożyć je całkowicie wewnętrznie w pluginie. Wtyczka może monitorować dowolne warunki, aby określić, kiedy przełączyć tryby, np. nasłuchiwanie transmisji. Wtyczka nie może wywołać zmiany konfiguracji w celu zmiany trybów, ale nie zalecamy polegania na zmianach konfiguracji, ponieważ ręczna aktualizacja wyglądu każdego komponentu jest dla użytkownika płynniejsza i umożliwia przejścia, które nie są możliwe przy zmianach konfiguracji.

Jetpack Compose

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

Wtyczki mogą używać interfejsu ComposeView, aby tworzyć powierzchnie obsługiwane przez Compose, na których można renderować. Ten ComposeView to wartość zwracana z aplikacji przez metodę getView w komponentach.

Jedną z głównych wad używania ComposeView jest to, że ustawia ona tagi w widoku rdzeniowym w układzie, aby przechowywać zmienne globalne, które są współdzielone przez różne widoki kompozytowe w hierarchii. Identyfikatory zasobów w pliku plugina nie są oddzielne od identyfikatorów zasobów aplikacji, co może powodować konflikty, gdy aplikacja i plugin ustawiają tagi w tym samym widoku. Poniżej znajdziesz niestandardową funkcję ComposeViewWithLifecycle, która przenosi te zmienne globalne do funkcji ComposeView. Ponownie: nie należy ich 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)
//  }
}