Plugin UI Mobil

Gunakan plugin library UI Mobil untuk membuat penerapan lengkap penyesuaian komponen di library UI Mobil, bukan menggunakan overlay resource runtime (RRO). RRO memungkinkan Anda mengubah hanya komponen library UI Mobil yang berupa resource XML, yang membatasi tingkat penyesuaian yang dapat Anda lakukan.

Membuat plugin

Plugin library UI Mobil adalah APK yang berisi class yang mengimplementasikan serangkaian Plugin API. Plugin API dapat dikompilasi ke dalam plugin sebagai library statis.

Lihat contoh di Soong dan Gradle:

Soong

Perhatikan contoh Soong ini:

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

Lihat file build.gradle ini:

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

Plugin harus memiliki penyedia konten yang dideklarasikan dalam manifesnya yang memiliki atribut berikut:

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

android:authorities="com.android.car.ui.plugin" membuat plugin dapat ditemukan oleh library UI Mobil. Penyedia harus diekspor agar dapat dikueri saat runtime. Selain itu, jika atribut enabled disetel ke false, implementasi default akan digunakan, bukan implementasi plugin. Class penyedia konten tidak harus ada. Dalam hal ini, pastikan untuk menambahkan tools:ignore="MissingClass" ke definisi penyedia. Lihat contoh entri manifest di bawah:

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

Terakhir, sebagai langkah pengamanan, Tandatangani aplikasi Anda.

Plugin sebagai library bersama

Tidak seperti library statis Android yang dikompilasi langsung ke dalam aplikasi, library bersama Android dikompilasi ke dalam APK mandiri yang dirujuk oleh aplikasi lain saat runtime.

Plugin yang diimplementasikan sebagai library bersama Android akan otomatis menambahkan class-nya ke classloader bersama antar-aplikasi. Saat aplikasi yang menggunakan library UI Mobil menentukan dependensi runtime pada library bersama plugin, classloader-nya dapat mengakses class library bersama plugin. Plugin yang diimplementasikan sebagai aplikasi Android normal (bukan library bersama) dapat berdampak negatif pada waktu mulai dingin aplikasi.

Menerapkan dan membangun library bersama

Pengembangan dengan library bersama Android sangat mirip dengan aplikasi Android normal, dengan beberapa perbedaan utama.

  • Gunakan tag library di bawah tag application dengan nama paket plugin di manifes aplikasi plugin Anda:
    <application>
        <library android:name="com.chassis.car.ui.plugin" />
        ...
    </application>
  • Konfigurasi aturan build Soong android_app (Android.bp) dengan flag AAPT shared-lib, yang digunakan untuk membangun library bersama:
android_app {
  ...
  aaptflags: ["--shared-lib"],
  ...
}

Dependensi pada library bersama

Untuk setiap aplikasi di sistem yang menggunakan library Car UI, sertakan tag uses-library di manifes aplikasi dalam tag application dengan nama paket plugin:

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

Menginstal plugin

Plugin HARUS diinstal sebelumnya di partisi sistem dengan menyertakan modul di PRODUCT_PACKAGES. Paket yang sudah diinstal sebelumnya dapat diupdate dengan cara yang sama seperti aplikasi terinstal lainnya.

Jika Anda mengupdate plugin yang ada di sistem, semua aplikasi yang menggunakan plugin tersebut akan ditutup secara otomatis. Setelah dibuka kembali oleh pengguna, pengguna akan melihat perubahan yang diperbarui. Jika aplikasi tidak berjalan, saat berikutnya aplikasi dimulai, aplikasi akan memiliki plugin yang diupdate.

Saat menginstal plugin dengan Android Studio, ada beberapa pertimbangan tambahan yang perlu diperhatikan. Pada saat penulisan, ada bug dalam proses penginstalan aplikasi Android Studio yang menyebabkan update pada plugin tidak diterapkan. Masalah ini dapat diperbaiki dengan memilih opsi Selalu instal dengan pengelola paket (menonaktifkan pengoptimalan deployment di Android 11 dan yang lebih baru) dalam konfigurasi build plugin.

Selain itu, saat menginstal plugin, Android Studio melaporkan error bahwa plugin tidak dapat menemukan aktivitas utama untuk diluncurkan. Hal ini sudah diperkirakan, karena plugin tidak memiliki aktivitas apa pun (kecuali intent kosong yang digunakan untuk menyelesaikan intent). Untuk menghilangkan error, ubah opsi Launch ke Nothing dalam konfigurasi build.

Konfigurasi Plugin Android Studio Gambar 1. Konfigurasi Plugin Android Studio

Plugin proxy

Penyesuaian aplikasi yang menggunakan library UI Mobil memerlukan RRO yang menargetkan setiap aplikasi tertentu yang akan diubah, termasuk saat penyesuaian identik di seluruh aplikasi. Artinya, RRO per aplikasi diperlukan. Lihat aplikasi mana yang menggunakan library Car UI.

Plugin proxy library UI Mobil adalah contoh library bersama plugin yang mendelegasikan implementasi komponennya ke versi statis library UI Mobil. Plugin ini dapat ditargetkan dengan RRO, yang dapat digunakan sebagai satu titik penyesuaian untuk aplikasi yang menggunakan library UI Mobil tanpa perlu menerapkan plugin fungsional. Untuk mengetahui informasi selengkapnya tentang RRO, lihat Mengubah nilai resource aplikasi saat runtime.

Plugin proxy hanyalah contoh dan titik awal untuk melakukan penyesuaian menggunakan plugin. Untuk penyesuaian di luar RRO, seseorang dapat menerapkan subset komponen plugin dan menggunakan plugin proxy untuk komponen lainnya, atau menerapkan semua komponen plugin sepenuhnya dari awal.

Meskipun plugin proxy menyediakan satu titik penyesuaian RRO untuk aplikasi, aplikasi yang tidak menggunakan plugin ini akan tetap memerlukan RRO yang menargetkan aplikasi itu sendiri secara langsung.

Mengimplementasikan API plugin

Titik entri utama ke plugin adalah class com.android.car.ui.plugin.PluginVersionProviderImpl. Semua plugin harus menyertakan class dengan nama dan nama paket yang sama persis ini. Class ini harus memiliki konstruktor default dan mengimplementasikan antarmuka PluginVersionProviderOEMV1.

Plugin CarUi harus berfungsi dengan aplikasi yang lebih lama atau lebih baru daripada plugin. Untuk memfasilitasi hal ini, semua API plugin diberi versi dengan V# di akhir nama kelasnya. Jika versi baru library UI Mobil dirilis dengan fitur baru, fitur tersebut adalah bagian dari komponen versi V2. Library UI Mobil berupaya sebaik mungkin agar fitur baru berfungsi dalam cakupan komponen plugin yang lebih lama. Misalnya, dengan mengonversi jenis tombol baru di toolbar menjadi MenuItems.

Namun, aplikasi dengan library UI Mobil versi lama tidak dapat beradaptasi dengan plugin baru yang ditulis terhadap API yang lebih baru. Untuk mengatasi masalah ini, kami mengizinkan plugin untuk menampilkan implementasi yang berbeda dari dirinya sendiri berdasarkan versi OEM API yang didukung oleh aplikasi.

PluginVersionProviderOEMV1 memiliki satu metode di dalamnya:

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

Metode ini menampilkan objek yang menerapkan versi tertinggi PluginFactoryOEMV# yang didukung oleh plugin, tetapi tetap kurang dari atau sama dengan maxVersion. Jika plugin tidak memiliki implementasi PluginFactory yang lama, plugin dapat menampilkan null, yang dalam hal ini implementasi komponen CarUi yang ditautkan secara statis akan digunakan.

Untuk mempertahankan kompatibilitas mundur dengan aplikasi yang dikompilasi terhadap versi lama library Car UI statis, sebaiknya dukung maxVersion 2, 5, dan yang lebih tinggi dari dalam penerapan plugin class PluginVersionProvider. Versi 1, 3, dan 4 tidak didukung. Untuk mengetahui informasi selengkapnya, lihat PluginVersionProviderImpl.

PluginFactory adalah antarmuka yang membuat semua komponen CarUi lainnya. Kode ini juga menentukan versi antarmuka yang harus digunakan. Jika plugin tidak berupaya menerapkan salah satu komponen ini, plugin dapat menampilkan null dalam fungsi pembuatannya (kecuali toolbar, yang memiliki fungsi customizesBaseLayout() terpisah).

pluginFactory membatasi versi komponen CarUi yang dapat digunakan bersama. Misalnya, tidak akan pernah ada pluginFactory yang dapat membuat Toolbar versi 100 dan juga RecyclerView versi 1, karena tidak ada jaminan bahwa berbagai versi komponen akan dapat bekerja sama. Untuk menggunakan toolbar versi 100, developer diharapkan memberikan penerapan versi pluginFactory yang membuat toolbar versi 100, yang kemudian membatasi opsi pada versi komponen lain yang dapat dibuat. Versi komponen lain mungkin tidak sama, misalnya, pluginFactoryOEMV100 dapat membuat ToolbarControllerOEMV100 dan RecyclerViewOEMV70.

Toolbar

Layout dasar

Toolbar dan "tata letak dasar" sangat terkait erat, sehingga fungsi yang membuat toolbar disebut installBaseLayoutAround. Tata letak dasar adalah konsep yang memungkinkan toolbar diposisikan di mana saja di sekitar konten aplikasi, untuk memungkinkan toolbar di bagian atas/bawah aplikasi, secara vertikal di sepanjang sisi, atau bahkan toolbar melingkar yang mengelilingi seluruh aplikasi. Hal ini dilakukan dengan meneruskan tampilan ke installBaseLayoutAround agar toolbar/tata letak dasar dapat mengelilingi.

Plugin harus mengambil tampilan yang diberikan, melepaskannya dari induknya, mem-inflate tata letak plugin sendiri dalam indeks induk yang sama dan dengan LayoutParams yang sama dengan tampilan yang baru saja dilepaskan, lalu melampirkan kembali tampilan di suatu tempat dalam tata letak yang baru saja di-inflate. Tata letak yang di-inflate akan berisi toolbar, jika diminta oleh aplikasi.

Aplikasi dapat meminta tata letak dasar tanpa toolbar. Jika ya, installBaseLayoutAround harus menampilkan null. Untuk sebagian besar plugin, hanya itu yang perlu dilakukan, tetapi jika penulis plugin ingin menerapkan, misalnya, dekorasi di sekitar tepi aplikasi, hal itu masih dapat dilakukan dengan tata letak dasar. Dekorasi ini sangat berguna untuk perangkat dengan layar non-persegi panjang, karena dapat mendorong aplikasi ke ruang persegi panjang dan menambahkan transisi yang bersih ke ruang non-persegi panjang.

installBaseLayoutAround juga meneruskan Consumer<InsetsOEMV1>. Konsumen ini dapat digunakan untuk mengomunikasikan ke aplikasi bahwa plugin sebagian menutupi konten aplikasi (dengan toolbar atau lainnya). Aplikasi akan mengetahui untuk terus menggambar di ruang ini, tetapi menjaga komponen penting yang dapat berinteraksi dengan pengguna agar tidak berada di dalamnya. Efek ini digunakan dalam desain referensi kami, untuk membuat toolbar semi-transparan, dan membuat daftar dapat di-scroll di bawahnya. Jika fitur ini tidak diterapkan, item pertama dalam daftar akan tetap berada di bawah toolbar dan tidak dapat diklik. Jika efek ini tidak diperlukan, plugin dapat mengabaikan Consumer.

Men-scroll konten di bawah toolbar Gambar 2. Men-scroll konten di bawah toolbar

Dari perspektif aplikasi, saat plugin mengirimkan inset baru, plugin akan menerimanya dari aktivitas atau fragmen yang menerapkan InsetsChangedListener. Jika aktivitas atau fragmen tidak menerapkan InsetsChangedListener, library Car Ui akan menangani inset secara default dengan menerapkan inset sebagai padding ke Activity atau FragmentActivity yang berisi fragmen. Library tidak menerapkan inset ke fragmen secara default. Berikut adalah contoh cuplikan penerapan yang menerapkan inset sebagai padding pada RecyclerView di aplikasi:

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

Terakhir, plugin diberi petunjuk fullscreen, yang digunakan untuk menunjukkan apakah tampilan yang harus di-wrap menggunakan seluruh aplikasi atau hanya sebagian kecil. Hal ini dapat digunakan untuk menghindari penerapan beberapa dekorasi di sepanjang tepi yang hanya masuk akal jika muncul di sepanjang tepi seluruh layar. Contoh aplikasi yang menggunakan tata letak dasar non-layar penuh adalah Setelan, yang setiap panel tata letak panel ganda memiliki toolbar-nya sendiri.

Karena installBaseLayoutAround diharapkan menampilkan null saat toolbarEnabled adalah false, agar plugin menunjukkan bahwa plugin tidak ingin menyesuaikan tata letak dasar, plugin harus menampilkan false dari customizesBaseLayout.

Tata letak dasar harus berisi FocusParkingView dan FocusArea untuk sepenuhnya mendukung kontrol putar. Tampilan ini dapat dihilangkan di perangkat yang tidak mendukung putaran. FocusParkingView/FocusAreas diimplementasikan di library CarUi statis, sehingga setRotaryFactories digunakan untuk menyediakan factory guna membuat tampilan dari konteks.

Konteks yang digunakan untuk membuat tampilan Fokus harus berupa konteks sumber, bukan konteks plugin. FocusParkingView harus sedekat mungkin dengan tampilan pertama di hierarki, karena itulah yang difokuskan saat tidak ada fokus yang terlihat oleh pengguna. FocusArea harus membungkus toolbar dalam tata letak dasar untuk menunjukkan bahwa toolbar adalah zona sentuhan putar. Jika FocusArea tidak disediakan, pengguna tidak dapat menavigasi tombol apa pun di toolbar dengan pengontrol putar.

Pengontrol toolbar

ToolbarController yang sebenarnya ditampilkan akan jauh lebih mudah diterapkan daripada tata letak dasar. Tugasnya adalah mengambil informasi yang diteruskan ke setter-nya dan menampilkannya dalam tata letak dasar. Lihat Javadoc untuk mengetahui informasi tentang sebagian besar metode. Beberapa metode yang lebih kompleks dibahas di bawah.

getImeSearchInterface digunakan untuk menampilkan hasil penelusuran di jendela IME (keyboard). Hal ini dapat berguna untuk menampilkan/menganimasikan hasil penelusuran bersama keyboard, misalnya jika keyboard hanya menempati separuh layar. Sebagian besar fungsi diimplementasikan di library CarUi statis, antarmuka penelusuran di plugin hanya menyediakan metode agar library statis mendapatkan callback TextView dan onPrivateIMECommand. Untuk mendukung hal ini, plugin harus menggunakan subclass TextView yang menggantikan onPrivateIMECommand dan meneruskan panggilan ke pemroses yang disediakan sebagai TextView kolom penelusurannya.

setMenuItems hanya menampilkan MenuItems di layar, tetapi akan dipanggil cukup sering. Karena plugin API untuk MenuItems tidak dapat diubah, setiap kali MenuItem diubah, panggilan setMenuItems baru akan terjadi. Hal ini dapat terjadi untuk sesuatu yang sepele seperti pengguna mengklik MenuItem switch, dan klik tersebut menyebabkan switch beralih. Oleh karena itu, untuk alasan performa dan animasi, sebaiknya hitung perbedaan antara daftar MenuItems lama dan baru, lalu hanya perbarui tampilan yang benar-benar berubah. MenuItem menyediakan kolom key yang dapat membantu hal ini, karena kunci harus sama di seluruh panggilan yang berbeda ke setMenuItems untuk MenuItem yang sama.

AppStyledView

AppStyledView adalah penampung untuk tampilan yang tidak disesuaikan sama sekali. Hal ini dapat digunakan untuk memberikan batas di sekitar tampilan yang membuatnya menonjol dari bagian aplikasi lainnya, dan menunjukkan kepada pengguna bahwa ini adalah jenis antarmuka yang berbeda. Tampilan yang di-wrap oleh AppStyledView diberikan di setContent. AppStyledView juga dapat memiliki tombol kembali atau tutup seperti yang diminta oleh aplikasi.

AppStyledView tidak langsung menyisipkan tampilannya ke dalam hierarki tampilan seperti yang dilakukan installBaseLayoutAround, melainkan hanya menampilkan tampilannya ke library statis melalui getView, yang kemudian melakukan penyisipan. Posisi dan ukuran AppStyledView juga dapat dikontrol dengan menerapkan getDialogWindowLayoutParam.

Konteks

Plugin harus berhati-hati saat menggunakan Konteks, karena ada konteks plugin dan "sumber". Konteks plugin diberikan sebagai argumen ke getPluginFactory, dan merupakan satu-satunya konteks yang memiliki resource plugin di dalamnya. Artinya, ini adalah satu-satunya konteks yang dapat digunakan untuk meng-inflate tata letak di plugin.

Namun, konteks plugin mungkin tidak memiliki setelan konfigurasi yang benar. Untuk mendapatkan konfigurasi yang benar, kami menyediakan konteks sumber dalam metode yang membuat komponen. Konteks sumber biasanya adalah aktivitas, tetapi dalam beberapa kasus juga bisa berupa Layanan atau komponen Android lainnya. Untuk menggunakan konfigurasi dari konteks sumber dengan resource dari konteks plugin, konteks baru harus dibuat menggunakan createConfigurationContext. Jika konfigurasi yang benar tidak digunakan, akan ada pelanggaran mode ketat Android, dan tampilan yang di-inflate mungkin tidak memiliki dimensi yang benar.

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

Perubahan mode

Beberapa plugin dapat mendukung beberapa mode untuk komponennya, seperti mode olahraga atau mode hemat energi yang terlihat berbeda secara visual. Tidak ada dukungan bawaan untuk fungsi tersebut di CarUi, tetapi tidak ada yang menghentikan plugin untuk menerapkannya sepenuhnya secara internal. Plugin dapat memantau kondisi apa pun yang diinginkannya untuk mengetahui kapan harus beralih mode, seperti mendengarkan siaran. Plugin tidak dapat memicu perubahan konfigurasi untuk mengubah mode, tetapi tidak disarankan untuk mengandalkan perubahan konfigurasi, karena memperbarui tampilan setiap komponen secara manual lebih lancar bagi pengguna dan juga memungkinkan transisi yang tidak mungkin dilakukan dengan perubahan konfigurasi.

Jetpack Compose

Plugin dapat diimplementasikan menggunakan Jetpack Compose, tetapi ini adalah fitur tingkat alfa dan tidak boleh dianggap stabil.

Plugin dapat menggunakan ComposeView untuk membuat tujuan render yang kompatibel dengan Compose. ComposeView ini akan menjadi apa yang ditampilkan dari ke aplikasi dari metode getView di komponen.

Salah satu masalah utama saat menggunakan ComposeView adalah bahwa fungsi ini menetapkan tag pada tampilan root dalam tata letak untuk menyimpan variabel global yang dibagikan di ComposeView yang berbeda dalam hierarki. Karena ID resource plugin tidak diberi namespace secara terpisah dari aplikasi, hal ini dapat menyebabkan konflik saat aplikasi dan plugin menetapkan tag pada tampilan yang sama. ComposeViewWithLifecycle kustom yang memindahkan variabel global ini ke ComposeView disediakan di bawah. Sekali lagi, ini tidak boleh dianggap stabil.

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