Wtyczki Car UI

Używaj wtyczek biblioteki Car UI, aby tworzyć pełne implementacje dostosowań komponentów w bibliotece Car UI zamiast korzystać z nakładek zasobów w czasie działania (RRO). Nakładki 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 wtyczek. Interfejsy API wtyczek można skompilować we wtyczce jako bibliotekę statyczną.

Przykłady znajdziesz w sekcjach Soong i Gradle:

Soong

Rozważmy ten 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 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 dostawcę treści, który ma te 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 przez bibliotekę Car UI. Dostawcę należy wyeksportować, aby można było wysyłać do niego zapytania w czasie działania programu. Jeśli atrybut enabled ma wartość false, zamiast implementacji wtyczki zostanie użyta implementacja domyślna. Klasa dostawcy treści nie musi istnieć. W takim przypadku dodaj do definicji dostawcy wartość tools:ignore="MissingClass". Zobacz 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, ze względów bezpieczeństwa, podpisz aplikację.

Wtyczki jako biblioteka udostępniona

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

Wtyczki zaimplementowane jako biblioteka współdzielona Androida mają klasy automatycznie dodawane do współdzielonego modułu ładującego klasy między aplikacjami. Gdy aplikacja korzystająca z biblioteki Car UI określa zależność w czasie działania od wtyczki biblioteki udostępnionej, jej program ładujący klasy może uzyskać dostęp do klas wtyczki biblioteki udostępnionej. Wtyczki zaimplementowane jako zwykłe aplikacje na Androida (nie jako biblioteka współdzielona) mogą negatywnie wpływać na czas zimnego startu aplikacji.

wdrażanie i tworzenie bibliotek udostępnionych;

Tworzenie aplikacji z użyciem bibliotek udostępnionych Androida jest podobne do tworzenia zwykłych aplikacji na Androida, ale występuje kilka kluczowych różnic.

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

Zależności od bibliotek udostępnionych

W przypadku każdej aplikacji w systemie, która korzysta z biblioteki Car UI, umieść 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ć preinstalowane na partycji systemowej przez dodanie modułu do elementu PRODUCT_PACKAGES. Preinstalowany pakiet można aktualizować podobnie jak każdą inną zainstalowaną aplikację.

Jeśli aktualizujesz istniejącą wtyczkę w systemie, wszystkie aplikacje, które jej używają, zostaną automatycznie zamknięte. Gdy użytkownik ponownie otworzy plik, zobaczy w nim zaktualizowane zmiany. Jeśli aplikacja nie była uruchomiona, przy następnym uruchomieniu będzie miała zaktualizowaną wtyczkę.

Podczas instalowania wtyczki w Android Studio musisz wziąć pod uwagę kilka dodatkowych kwestii. W momencie pisania tego artykułu w procesie instalacji aplikacji Android Studio występuje błąd, który powoduje, że aktualizacje wtyczki nie są uwzględniane. Możesz to naprawić, wybierając opcję Always install with package manager (disables deploy optimizations on Android 11 and later) w konfiguracji kompilacji wtyczki.

Podczas instalacji wtyczki Android Studio zgłasza też błąd, że nie może znaleźć głównej aktywności do uruchomienia. Jest to oczekiwane zachowanie, ponieważ wtyczka nie ma żadnych aktywności (z wyjątkiem pustej intencji używanej do rozpoznawania intencji). Aby wyeliminować błąd, w konfiguracji kompilacji zmień opcję Uruchom na Nic.

Konfiguracja wtyczki Android Studio Rysunek 1. Konfiguracja wtyczki Android Studio

Wtyczka serwera proxy

Dostosowywanie aplikacji za pomocą biblioteki interfejsu samochodu wymaga RRO, który jest kierowany na każdą konkretną aplikację, która ma zostać zmodyfikowana, w tym wtedy, gdy dostosowania są identyczne w różnych aplikacjach. Oznacza to, że wymagane jest RRO dla każdej aplikacji. Sprawdź, które aplikacje korzystają z biblioteki Car UI.

Wtyczka proxy biblioteki Car UI to przykładowa biblioteka udostępniona wtyczki, która przekazuje implementacje komponentów do statycznej wersji biblioteki Car UI. Tę wtyczkę można kierować za pomocą RRO, które może służyć jako pojedynczy punkt dostosowywania aplikacji korzystających z biblioteki Car UI bez konieczności implementowania funkcjonalnej wtyczki. Więcej informacji o nakładkach zasobów środowiska wykonawczego znajdziesz w artykule Zmiana wartości zasobów aplikacji w czasie działania.

Wtyczka proxy jest tylko przykładem i punktem wyjścia do dostosowywania za pomocą wtyczki. Aby dostosować funkcje wykraczające poza RRO, można wdrożyć podzbiór komponentów wtyczki i użyć wtyczki proxy w przypadku pozostałych lub wdrożyć wszystkie komponenty wtyczki od zera.

Chociaż wtyczka proxy zapewnia jeden punkt dostosowywania RRO dla aplikacji, aplikacje, które nie korzystają z tej wtyczki, nadal będą wymagać RRO, który jest bezpośrednio kierowany na samą aplikację.

Wdrażanie interfejsów 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 samej nazwie i nazwie pakietu. Ta klasa musi mieć konstruktor domyślny i implementować interfejs PluginVersionProviderOEMV1.

Wtyczki CarUi muszą działać z aplikacjami starszymi i nowszymi niż wtyczka. Aby to ułatwić, wszystkie interfejsy API wtyczek mają na końcu nazwy klasy symbol V#. Jeśli nowa wersja biblioteki Car UI zostanie wydana z nowymi funkcjami, będą one częścią V2 wersji komponentu. Biblioteka Car UI dokłada wszelkich starań, aby nowe funkcje działały w ramach starszego komponentu wtyczki. Na przykład przez przekształcenie nowego typu przycisku na pasku narzędzi w 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, umożliwiamy wtyczkom zwracanie różnych implementacji w zależności od wersji interfejsu 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ę, ale jest mniejszy lub równy maxVersion. Jeśli wtyczka nie ma implementacji PluginFactory w takiej wersji, 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 pod kątem starszych wersji statycznej biblioteki Car Ui, zalecamy obsługę wartości maxVersion 2, 5 i wyższych w ramach implementacji klasy PluginVersionProvider w Twojej wtyczce. Wersje 1, 3 i 4 nie są obsługiwane. Więcej informacji znajdziesz w sekcji PluginVersionProviderImpl.

PluginFactory to interfejs, który tworzy wszystkie inne komponenty CarUi. Określa też, której wersji interfejsów należy używać. Jeśli wtyczka nie ma na celu implementacji żadnego z tych komponentów, może zwrócić null w funkcji tworzenia (z wyjątkiem paska narzędzi, który ma 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óry może utworzyć wersję 100 Toolbar i wersję 1 RecyclerView, ponieważ nie ma gwarancji, że różne wersje komponentów będą 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ć, np. pluginFactoryOEMV100 może utworzyć ToolbarControllerOEMV100RecyclerViewOEMV70.

Pasek narzędzi

Układ podstawowy

Pasek narzędzi i „układ podstawowy” są ze sobą ściśle powiązane, dlatego 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ół treści aplikacji, dzięki czemu może on znajdować się u góry lub u dołu aplikacji, pionowo po bokach, a nawet w formie okrągłego paska narzędzi otaczającego całą aplikację. Aby to osiągnąć, należy przekazać widok do installBaseLayoutAround, aby pasek narzędzi lub układ podstawowy mógł go otaczać.

Wtyczka powinna pobrać podany widok, odłączyć go od elementu nadrzędnego, rozwinąć własny układ wtyczki w tym samym indeksie elementu nadrzędnego i z tymi samymi LayoutParams co odłączony widok, a następnie ponownie dołączyć widok w miejscu w rozwiniętym układzie. Rozwinięty układ będzie zawierać pasek narzędzi, jeśli aplikacja o to poprosi.

Aplikacja może poprosić o układ podstawowy bez paska narzędzi. Jeśli tak jest, funkcja installBaseLayoutAround powinna zwrócić wartość null. W przypadku większości wtyczek to wystarczy, ale jeśli autor wtyczki chce zastosować np. dekorację na krawędzi aplikacji, nadal może to zrobić za pomocą układu podstawowego. Te dekoracje są szczególnie przydatne w przypadku urządzeń z ekranami o kształcie innym niż prostokątny, ponieważ mogą one umieścić aplikację w prostokątnym obszarze i zapewnić płynne przejścia do obszaru o innym kształcie.

installBaseLayoutAround jest też przekazywany Consumer<InsetsOEMV1>. Ten interfejs API może służyć do informowania aplikacji, że wtyczka częściowo zasłania jej zawartość (za pomocą paska narzędzi lub w inny sposób). Aplikacja będzie wtedy wiedzieć, że ma nadal rysować w tym obszarze, ale nie będzie w nim umieszczać żadnych ważnych komponentó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 przewijały się pod nim. Gdyby ta funkcja nie została wdrożona, pierwszy element na liście byłby ukryty pod paskiem narzędzi i nie można by go kliknąć. Jeśli ten efekt nie jest potrzebny, wtyczka może zignorować element Consumer.

Treść przewijana pod paskiem narzędzi Rysunek 2. Treść przewijana pod paskiem narzędzi

Z perspektywy aplikacji, gdy wtyczka wysyła nowe wstawki, otrzymuje je z dowolnych aktywności lub fragmentów, które implementują interfejs InsetsChangedListener. Jeśli aktywność lub fragment nie implementuje InsetsChangedListener, biblioteka Car Ui domyślnie obsługuje wstawki, stosując je jako dopełnienie elementu Activity lub FragmentActivity zawierającego fragment. Biblioteka domyślnie nie stosuje marginesów do fragmentów. Oto przykładowy fragment kodu, który stosuje wcięcia jako dopeł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());
  }
}

Wtyczka otrzymuje też fullscreenwskazówkę, która informuje, czy widok, który ma zostać opakowany, zajmuje całą aplikację, czy tylko jej niewielką część. Możesz dzięki temu 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ą, która korzysta z układów podstawowych innych niż pełnoekranowe, są Ustawienia. Każdy panel w układzie dwupanelowym ma własny pasek narzędzi.

Oczekuje się, że w przypadku, gdy toolbarEnabled ma wartość false, funkcja installBaseLayoutAround zwróci wartość null. Aby wtyczka wskazała, że nie chce dostosowywać układu podstawowego, musi zwrócić wartość false z funkcji customizesBaseLayout.

Aby w pełni obsługiwać elementy sterujące obrotowe, układ podstawowy musi zawierać elementy FocusParkingViewFocusArea. Na urządzeniach, które nie obsługują obrotowego sterowania, te widoki można pominąć. FocusParkingView/FocusAreas są zaimplementowane w statycznej bibliotece CarUi, więc setRotaryFactories służy do udostępniania fabryk, które tworzą widoki na podstawie kontekstów.

Konteksty używane do tworzenia widoków Focus muszą być kontekstem źródłowym, a nie kontekstem wtyczki. Element FocusParkingView powinien znajdować się jak najbliżej pierwszego widoku w drzewie, ponieważ to na nim skupia się uwaga, gdy użytkownik nie powinien widzieć żadnego elementu, na którym skupia się uwaga. Element FocusArea musi otaczać pasek narzędzi w układzie podstawowym, aby wskazywać, że jest to strefa dotknięcia obrotowego. Jeśli nie podasz FocusArea, użytkownik nie będzie mógł przejść do żadnego przycisku na pasku narzędzi za pomocą kontrolera obrotowego.

Kontroler paska narzędzi

Rzeczywisty element ToolbarController powinien być znacznie łatwiejszy do wdrożenia niż układ podstawowy. Jego zadaniem jest pobieranie informacji przekazywanych do jego setterów i wyświetlanie ich w układzie podstawowym. Informacje o większości metod znajdziesz w dokumentacji Javadoc. Poniżej omawiamy niektóre z bardziej złożonych metod.

getImeSearchInterface służy do wyświetlania wyników wyszukiwania w oknie IME (klawiatury). Może to być przydatne np. do wyświetlania lub animowania wyników wyszukiwania obok klawiatury, jeśli klawiatura zajmuje tylko połowę ekranu. Większość funkcji jest zaimplementowana w statycznej bibliotece CarUi, a interfejs wyszukiwania we wtyczce udostępnia tylko metody, dzięki którym biblioteka statyczna może uzyskiwać wywołania zwrotne TextViewonPrivateIMECommand. W tym celu wtyczka powinna używać podklasy TextView, która zastępuje onPrivateIMECommand i przekazuje wywołanie do podanego odbiorcy jako TextView paska wyszukiwania.

setMenuItems po prostu wyświetla elementy MenuItems na ekranie, ale będzie wywoływana zaskakująco często. Interfejs API wtyczek dla elementów menu jest niezmienny, więc za każdym razem, gdy element menu zostanie zmieniony, nastąpi nowe wywołanie setMenuItems. Może to nastąpić w przypadku tak prostej czynności, jak kliknięcie przez użytkownika elementu menu przełącznika, które spowodowało jego przełączenie. Ze względu na wydajność i animację zalecamy obliczenie różnicy między starą a nową listą MenuItems i aktualizowanie tylko tych widoków, które uległy zmianie. Elementy MenuItems zawierają pole key, które może w tym pomóc, ponieważ klucz powinien być taki sam w różnych wywołaniach funkcji setMenuItems dla tego samego elementu MenuItems.

AppStyledView

AppStyledView to kontener widoku, który nie jest w żaden sposób dostosowany. Można go użyć do utworzenia obramowania wokół widoku, które wyróżni go na tle reszty aplikacji i wskaże użytkownikowi, że jest to inny rodzaj interfejsu. Widok, który jest opakowany przez AppStyledView, jest podany w setContent. AppStyledView może też zawierać przycisk Wstecz lub Zamknij, jeśli zażąda tego aplikacja.

Funkcja AppStyledView nie wstawia od razu swoich widoków do hierarchii widoków, tak jak robi to funkcja installBaseLayoutAround. Zamiast tego zwraca swój widok do biblioteki statycznej za pomocą funkcji getView, która następnie wstawia widok. Położenie i rozmiar AppStyledView można też kontrolować, wdrażając getDialogWindowLayoutParam.

Konteksty

Wtyczka musi zachować ostrożność podczas korzystania z kontekstów, ponieważ istnieją konteksty wtyczki i „ź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órego można używać do rozwijania układów we wtyczce.

Jednak w kontekście wtyczki może nie być ustawiona prawidłowa konfiguracja. Aby uzyskać prawidłową konfigurację, udostępniamy konteksty źródłowe w metodach, które tworzą komponenty. Kontekst źródłowy jest zwykle aktywnością, 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, musisz utworzyć nowy kontekst za pomocą funkcji createConfigurationContext. Jeśli nie użyjesz prawidłowej konfiguracji, nastąpi naruszenie trybu ścisłego Androida, a rozszerzone 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, np. tryb sportowytryb ekologiczny, które różnią się wyglądem. CarUi nie ma wbudowanej obsługi takich funkcji, ale nic nie stoi na przeszkodzie, aby wtyczka zaimplementowała je w całości wewnętrznie. Wtyczka może monitorować dowolne warunki, aby określić, kiedy przełączyć tryby, np. nasłuchiwać transmisji. Wtyczka nie może wywoływać zmiany konfiguracji, aby zmieniać tryby, ale nie zalecamy polegania na zmianach konfiguracji, ponieważ ręczne aktualizowanie wyglądu każdego komponentu jest płynniejsze dla użytkownika i umożliwia przejścia, które nie są możliwe w przypadku zmian konfiguracji.

Jetpack Compose

Wtyczki można wdrażać za pomocą Jetpack Compose, ale jest to funkcja w wersji alfa, więc nie należy jej traktować jako stabilnej.

Wtyczki mogą używać ComposeView do tworzenia powierzchni z obsługą Compose, na której będą renderowane. Ten ComposeView będzie zwracany do aplikacji przez metodę getView w komponentach.

Jednym z głównych problemów z używaniem ComposeView jest to, że ustawia tagi w widoku głównym w układzie, aby przechowywać zmienne globalne, które są udostępniane w różnych elementach ComposeView w hierarchii. Identyfikatory zasobów wtyczki nie są oddzielone od identyfikatorów aplikacji, co może powodować konflikty, gdy zarówno aplikacja, jak i wtyczka ustawiają tagi w tym samym widoku. Poniżej znajdziesz niestandardową ComposeViewWithLifecycle, która przenosi te zmienne globalne do ComposeView. Pamiętaj, że nie należy jej uważać za stabilną.

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