Plugin UI mobil

Gunakan plugin perpustakaan Car UI untuk membuat implementasi lengkap penyesuaian komponen di perpustakaan Car UI alih-alih menggunakan runtime resource overlay (RRO). RRO memungkinkan Anda mengubah hanya sumber daya XML komponen pustaka UI Mobil, yang membatasi sejauh mana Anda dapat menyesuaikannya.

Buat sebuah plugin

Plugin pustaka Car UI adalah APK yang berisi kelas yang mengimplementasikan sekumpulan API Plugin . Plugin API dapat dikompilasi menjadi plugin sebagai perpustakaan statis.

Lihat contoh di Soong dan Gradle:

Segera

Perhatikan contoh Soon 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 di perpustakaan UI Mobil. Penyedia harus diekspor sehingga dapat ditanyakan pada saat runtime. Selain itu, jika atribut enabled disetel ke false , implementasi default akan digunakan sebagai pengganti implementasi plugin. Kelas penyedia konten tidak harus ada. Dalam hal ini, pastikan untuk menambahkan tools:ignore="MissingClass" ke definisi penyedia. Lihat contoh entri manifes di bawah ini:

    <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 tindakan pengamanan, Tandatangani aplikasi Anda .

Plugin sebagai perpustakaan bersama

Berbeda dengan pustaka statis Android yang dikompilasi langsung ke dalam aplikasi, pustaka bersama Android dikompilasi menjadi APK mandiri yang direferensikan oleh aplikasi lain saat runtime.

Plugin yang diimplementasikan sebagai pustaka bersama Android memiliki kelas yang ditambahkan secara otomatis ke pemuat kelas bersama antar aplikasi. Saat aplikasi yang menggunakan pustaka Car UI menentukan ketergantungan waktu proses pada pustaka bersama plugin, pemuat kelasnya dapat mengakses kelas pustaka bersama plugin. Plugin yang diterapkan sebagai aplikasi Android biasa (bukan perpustakaan bersama) dapat berdampak negatif terhadap waktu mulai aplikasi yang tidak aktif.

Menerapkan dan membangun perpustakaan bersama

Pengembangan dengan pustaka bersama Android sama seperti pengembangan aplikasi Android pada umumnya, 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>
  • Konfigurasikan aturan pembangunan android_app Soong Anda ( Android.bp ) dengan tanda AAPT shared-lib , yang digunakan untuk membangun perpustakaan bersama:
android_app {
  ...
  aaptflags: ["--shared-lib"],
  ...
}

Ketergantungan pada perpustakaan bersama

Untuk setiap aplikasi di sistem yang menggunakan pustaka Car UI, sertakan tag uses-library di manifes aplikasi di bawah 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>

Instal sebuah plugin

Plugin HARUS diinstal sebelumnya pada partisi sistem dengan menyertakan modul di PRODUCT_PACKAGES . Paket pra-instal dapat diperbarui sama seperti aplikasi terinstal lainnya.

Jika Anda memperbarui plugin yang ada di sistem, aplikasi apa pun yang menggunakan plugin tersebut akan ditutup secara otomatis. Setelah dibuka kembali oleh pengguna, mereka mendapatkan perubahan yang diperbarui. Jika aplikasi tidak berjalan, saat dijalankan lagi, plugin akan diperbarui.

Saat memasang plugin dengan Android Studio, ada beberapa pertimbangan tambahan yang perlu dipertimbangkan. Pada saat penulisan, terdapat bug dalam proses instalasi aplikasi Android Studio yang menyebabkan pembaruan plugin tidak diterapkan. Hal ini dapat diperbaiki dengan memilih opsi Selalu instal dengan manajer paket (menonaktifkan pengoptimalan penerapan di Android 11 dan yang lebih baru) di konfigurasi build plugin.

Selain itu, saat memasang plugin, Android Studio melaporkan kesalahan karena tidak dapat menemukan aktivitas utama untuk diluncurkan. Hal ini diharapkan, karena plugin tidak memiliki aktivitas apa pun (kecuali maksud kosong yang digunakan untuk menyelesaikan suatu maksud). Untuk menghilangkan kesalahan, ubah opsi Peluncuran ke Tidak Ada di konfigurasi build.

Konfigurasi plugin Android Studio Gambar 1. Konfigurasi plugin Android Studio

Plugin proksi

Penyesuaian aplikasi menggunakan pustaka UI Mobil memerlukan RRO yang menargetkan setiap aplikasi spesifik yang akan dimodifikasi, termasuk ketika penyesuaian dilakukan secara identik di seluruh aplikasi. Ini berarti diperlukan RRO per aplikasi. Lihat aplikasi mana yang menggunakan perpustakaan UI Mobil.

Plugin proksi pustaka UI Mobil adalah contoh pustaka bersama plugin yang mendelegasikan implementasi komponennya ke versi statis pustaka UI Mobil. Plugin ini dapat ditargetkan dengan RRO, yang dapat digunakan sebagai satu titik penyesuaian untuk aplikasi yang menggunakan pustaka Car UI tanpa perlu mengimplementasikan plugin fungsional. Untuk informasi selengkapnya tentang RRO, lihat Mengubah nilai sumber daya aplikasi saat runtime .

Plugin proxy hanyalah sebuah contoh dan titik awal untuk melakukan kustomisasi menggunakan sebuah plugin. Untuk penyesuaian di luar RRO, seseorang dapat mengimplementasikan subset komponen plugin dan menggunakan plugin proxy untuk sisanya, atau mengimplementasikan semua komponen plugin seluruhnya dari awal.

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

Implementasikan API plugin

Titik masuk utama ke plugin ini adalah kelas com.android.car.ui.plugin.PluginVersionProviderImpl . Semua plugin harus menyertakan kelas dengan nama persis dan nama paket ini. Kelas ini harus memiliki konstruktor default dan mengimplementasikan antarmuka PluginVersionProviderOEMV1 .

Plugin CarUi harus berfungsi dengan aplikasi yang lebih lama atau lebih baru dari plugin tersebut. Untuk memfasilitasi hal ini, semua API plugin dibuat versinya dengan V# di akhir nama kelasnya. Jika versi baru pustaka UI Mobil dirilis dengan fitur baru, fitur tersebut merupakan bagian dari komponen versi V2 . Pustaka Car UI melakukan yang terbaik untuk membuat fitur baru berfungsi dalam cakupan komponen plugin lama. Misalnya dengan mengubah jenis tombol baru di toolbar menjadi MenuItems .

Namun, aplikasi dengan pustaka UI Mobil versi lama tidak dapat beradaptasi dengan plugin baru yang ditulis dengan API yang lebih baru. Untuk mengatasi masalah ini, kami mengizinkan plugin mengembalikan implementasi berbeda berdasarkan versi API OEM yang didukung oleh aplikasi.

PluginVersionProviderOEMV1 memiliki satu metode di dalamnya:

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

Metode ini mengembalikan objek yang mengimplementasikan versi tertinggi PluginFactoryOEMV# yang didukung oleh plugin, namun tetap kurang dari atau sama dengan maxVersion . Jika sebuah plugin tidak memiliki implementasi PluginFactory yang lama, plugin tersebut mungkin akan mengembalikan null , dalam hal ini implementasi komponen CarUi yang tertaut secara statis akan digunakan.

Untuk menjaga kompatibilitas dengan aplikasi yang dikompilasi dengan versi lama pustaka Car Ui statis, disarankan untuk mendukung maxVersion 2, 5, dan yang lebih tinggi dari dalam implementasi plugin Anda pada kelas PluginVersionProvider . Versi 1, 3, dan 4 tidak didukung. Untuk informasi selengkapnya, lihat PluginVersionProviderImpl .

PluginFactory adalah antarmuka yang membuat semua komponen CarUi lainnya. Ini juga menentukan versi antarmuka mana yang harus digunakan. Jika plugin tidak berusaha mengimplementasikan salah satu komponen ini, plugin mungkin mengembalikan null dalam fungsi pembuatannya (dengan pengecualian toolbar, yang memiliki fungsi customizesBaseLayout() terpisah).

pluginFactory membatasi versi komponen CarUi mana yang dapat digunakan secara bersamaan. Misalnya, tidak akan pernah ada pluginFactory yang dapat membuat Toolbar versi 100 dan juga RecyclerView versi 1, karena hanya ada sedikit jaminan bahwa berbagai versi komponen akan bekerja sama. Untuk menggunakan toolbar versi 100, pengembang diharapkan menyediakan implementasi versi pluginFactory yang membuat toolbar versi 100, yang kemudian membatasi pilihan pada versi komponen lain yang dapat dibuat. Versi komponen lainnya mungkin tidak sama, misalnya pluginFactoryOEMV100 dapat membuat ToolbarControllerOEMV100 dan RecyclerViewOEMV70 .

Bilah Alat

Tata letak dasar

Toolbar dan "tata letak dasar" sangat erat kaitannya, oleh karena itu 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 menutupi keseluruhan aplikasi. Hal ini dilakukan dengan meneruskan tampilan ke installBaseLayoutAround untuk membungkus toolbar/tata letak dasar.

Plugin harus mengambil tampilan yang disediakan, melepaskannya dari induknya, mengembangkan tata letak plugin itu sendiri dalam indeks induk yang sama dan dengan LayoutParams yang sama dengan tampilan yang baru saja dilepas, lalu memasang kembali tampilan di suatu tempat di dalam tata letak yang sebelumnya. baru saja meningkat. Tata letak yang diperluas akan berisi toolbar, jika diminta oleh aplikasi.

Aplikasi ini dapat meminta tata letak dasar tanpa toolbar. Jika ya, installBaseLayoutAround akan mengembalikan nol. Untuk sebagian besar plugin, hanya itu yang perlu dilakukan, namun jika pembuat plugin ingin menerapkan, misalnya, hiasan 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 dalam ruang persegi panjang dan menambahkan transisi yang rapi ke dalam ruang non-persegi panjang.

installBaseLayoutAround juga diteruskan ke Consumer<InsetsOEMV1> . Konsumen ini dapat digunakan untuk menyampaikan kepada aplikasi bahwa plugin menutupi sebagian konten aplikasi (dengan toolbar atau lainnya). Aplikasi kemudian akan tahu untuk terus menggambar di ruang ini, tetapi tidak memasukkan komponen penting yang dapat berinteraksi dengan pengguna. Efek ini digunakan dalam desain referensi kami, untuk membuat toolbar semi-transparan, dan membuat daftar bergulir di bawahnya. Jika fitur ini tidak diterapkan, item pertama dalam daftar akan tertahan di bawah toolbar dan tidak dapat diklik. Jika efek ini tidak diperlukan, plugin dapat mengabaikan Konsumen.

Konten bergulir di bawah toolbar Gambar 2. Konten bergulir di bawah toolbar

Dari sudut pandang aplikasi, saat plugin mengirimkan insets baru, plugin akan menerimanya dari aktivitas atau fragmen apa pun yang mengimplementasikan InsetsChangedListener . Jika aktivitas atau fragmen tidak mengimplementasikan InsetsChangedListener , pustaka Car Ui akan menangani inset secara default dengan menerapkan inset sebagai padding ke Activity atau FragmentActivity yang berisi fragmen tersebut. Pustaka tidak menerapkan insets secara default ke fragmen. Berikut contoh cuplikan implementasi yang menerapkan insets 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 diberikan petunjuk fullscreen , yang digunakan untuk menunjukkan apakah tampilan yang harus dibungkus menempati seluruh aplikasi atau hanya sebagian kecil. Hal ini dapat digunakan untuk menghindari penerapan beberapa dekorasi di sepanjang tepian yang hanya masuk akal jika dekorasi tersebut muncul di sepanjang tepi seluruh layar. Contoh aplikasi yang menggunakan tata letak dasar non-layar penuh adalah Pengaturan, di mana setiap panel tata letak dua panel memiliki toolbar sendiri.

Karena installBaseLayoutAround diharapkan mengembalikan null ketika toolbarEnabled bernilai false , agar plugin menunjukkan bahwa plugin tidak ingin menyesuaikan tata letak dasar, plugin harus mengembalikan false dari customizesBaseLayout .

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

Konteks yang digunakan untuk membuat tampilan Fokus harus konteks sumber, bukan konteks plugin. FocusParkingView harus sedekat mungkin dengan tampilan pertama di pohon, karena ini adalah fokus ketika tidak ada fokus yang terlihat oleh pengguna. FocusArea harus membungkus toolbar dalam tata letak dasar untuk menunjukkan bahwa itu adalah zona dorongan putar. Jika FocusArea tidak disediakan, pengguna tidak dapat menavigasi ke tombol apa pun di toolbar dengan pengontrol putar.

Pengontrol bilah alat

ToolbarController sebenarnya yang dikembalikan seharusnya lebih mudah diterapkan dibandingkan tata letak dasar. Tugasnya adalah mengambil informasi yang diteruskan ke setternya dan menampilkannya di tata letak dasar. Lihat Javadoc untuk informasi tentang sebagian besar metode. Beberapa metode yang lebih kompleks dibahas di bawah ini.

getImeSearchInterface digunakan untuk menampilkan hasil pencarian di jendela IME (keyboard). Ini berguna untuk menampilkan/menganimasikan hasil pencarian di samping keyboard, misalnya jika keyboard hanya menempati separuh layar. Sebagian besar fungsi diimplementasikan di perpustakaan CarUi statis, antarmuka pencarian di plugin hanya menyediakan metode untuk perpustakaan statis untuk mendapatkan callback TextView dan onPrivateIMECommand . Untuk mendukung hal ini, plugin harus menggunakan subkelas TextView yang menggantikan onPrivateIMECommand dan meneruskan panggilan ke pendengar yang disediakan sebagai TextView bilah pencariannya.

setMenuItems hanya menampilkan MenuItems di layar, tetapi secara mengejutkan akan sering dipanggil. Karena API plugin untuk MenuItems tidak dapat diubah, setiap kali MenuItem diubah, panggilan setMenuItems baru akan terjadi. Hal ini dapat terjadi karena hal sepele seperti pengguna mengklik tombol MenuItem, dan klik tersebut menyebabkan tombol beralih. Untuk alasan kinerja dan animasi, oleh karena itu disarankan untuk menghitung perbedaan antara daftar MenuItems lama dan baru, dan hanya memperbarui tampilan yang benar-benar berubah. MenuItems menyediakan bidang key yang dapat membantu dalam hal ini, karena kuncinya harus sama di berbagai panggilan ke setMenuItems untuk MenuItem yang sama.

Tampilan Bergaya Aplikasi

AppStyledView adalah wadah untuk tampilan yang tidak dikustomisasi sama sekali. Ini dapat digunakan untuk memberikan batas di sekeliling tampilan yang membuatnya menonjol dari aplikasi lainnya, dan menunjukkan kepada pengguna bahwa ini adalah jenis antarmuka yang berbeda. Tampilan yang dibungkus oleh AppStyledView diberikan dalam setContent . AppStyledView juga dapat memiliki tombol kembali atau tutup seperti yang diminta oleh aplikasi.

AppStyledView tidak segera memasukkan tampilannya ke dalam hierarki tampilan seperti yang dilakukan installBaseLayoutAround , melainkan hanya mengembalikan tampilannya ke perpustakaan statis melalui getView , yang kemudian melakukan penyisipan. Posisi dan ukuran AppStyledView juga dapat dikontrol dengan mengimplementasikan 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 sumber daya plugin di dalamnya. Artinya, ini satu-satunya konteks yang dapat digunakan untuk mengembangkan tata letak di plugin.

Namun, konteks plugin mungkin tidak memiliki konfigurasi yang benar. Untuk mendapatkan konfigurasi yang benar, kami menyediakan konteks sumber dalam metode yang membuat komponen. Konteks sumber biasanya berupa aktivitas, namun dalam beberapa kasus mungkin juga berupa Layanan atau komponen Android lainnya. Untuk menggunakan konfigurasi dari konteks sumber dengan sumber daya dari konteks plugin, konteks baru harus dibuat menggunakan createConfigurationContext . Jika konfigurasi yang benar tidak digunakan, akan terjadi pelanggaran mode ketat Android, dan tampilan yang diperbesar mungkin tidak memiliki dimensi yang benar.

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

Perubahan modus

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

Penulisan Jetpack

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

Plugin dapat menggunakan ComposeView untuk membuat permukaan berkemampuan Compose untuk dirender. ComposeView ini akan dikembalikan dari ke aplikasi dari metode getView di komponen.

Salah satu masalah utama dalam penggunaan ComposeView adalah ia menetapkan tag pada tampilan akar di tata letak untuk menyimpan variabel global yang dibagikan ke berbagai ComposeView dalam hierarki. Karena id sumber daya plugin tidak diberi namespace secara terpisah dari aplikasi, hal ini dapat menyebabkan konflik ketika aplikasi dan plugin menyetel tag pada tampilan yang sama. ComposeViewWithLifecycle khusus yang memindahkan variabel global ini ke ComposeView disediakan di bawah. Sekali lagi, hal 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)
//  }
}