Araba kullanıcı arayüzü eklentileri

Çalışma zamanı kaynak yer paylaşımlarını (RRO'lar) kullanmak yerine, Car UI kitaplığındaki bileşen özelleştirmelerinin eksiksiz uygulamalarını oluşturmak için Car UI kitaplığı eklentilerini kullanın. RRO'lar, yalnızca Araba kullanıcı arayüzü kitaplığı bileşenlerinin XML kaynaklarını değiştirmenize olanak tanır. Bu da özelleştirebileceğiniz kapsamı sınırlar.

Eklenti oluşturma

Araba kullanıcı arayüzü kitaplığı eklentisi, bir dizi eklenti API'si uygulayan sınıfları içeren bir APK'dır. Eklenti API'leri, statik kitaplık olarak bir eklentiye derlenebilir.

Soong ve Gradle'daki örnekleri inceleyin:

Soong

Aşağıdaki Soong örneğini inceleyin:

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

Şu build.gradle dosyasına bakın:

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

Eklentinin manifestinde, aşağıdaki özelliklere sahip bir içerik sağlayıcı bildirilmelidir:

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

android:authorities="com.android.car.ui.plugin", eklentinin Car UI kitaplığı tarafından bulunmasını sağlar. Sağlayıcının, çalışma zamanında sorgulanabilmesi için dışa aktarılması gerekir. Ayrıca, enabled özelliği false olarak ayarlanırsa eklenti uygulaması yerine varsayılan uygulama kullanılır. İçerik sağlayıcı sınıfının mevcut olması gerekmez. Bu durumda, sağlayıcı tanımına tools:ignore="MissingClass" eklediğinizden emin olun. Aşağıdaki örnek manifest girişine bakın:

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

Son olarak, güvenlik önlemi olarak uygulamanızı imzalayın.

Paylaşılan kitaplık olarak eklentiler

Doğrudan uygulamalara derlenen Android statik kitaplıklarının aksine, Android paylaşılan kitaplıkları, çalışma zamanında diğer uygulamalar tarafından referans verilen bağımsız bir APK'ya derlenir.

Android paylaşılan kitaplığı olarak uygulanan eklentilerin sınıfları, uygulamalar arasındaki paylaşılan sınıf yükleyiciye otomatik olarak eklenir. Araba kullanıcı arayüzü kitaplığını kullanan bir uygulama, eklenti paylaşılan kitaplığında çalışma zamanı bağımlılığı belirttiğinde sınıf yükleyicisi, eklenti paylaşılan kitaplığının sınıflarına erişebilir. Normal Android uygulamaları (paylaşılan kitaplık değil) olarak uygulanan eklentiler, uygulamanın soğuk başlatma sürelerini olumsuz etkileyebilir.

Paylaşılan kitaplıkları uygulama ve oluşturma

Android paylaşılan kitaplıklarıyla geliştirme, birkaç temel fark dışında normal Android uygulamalarıyla geliştirmeye çok benzer.

  • Eklentinizin uygulama manifestinde eklenti paket adıyla birlikte application etiketi altında library etiketini kullanın:
    <application>
        <library android:name="com.chassis.car.ui.plugin" />
        ...
    </application>
  • Paylaşılan bir kitaplık oluşturmak için kullanılan AAPT işareti shared-lib ile Soong android_app derleme kuralınızı (Android.bp) yapılandırın:
android_app {
  ...
  aaptflags: ["--shared-lib"],
  ...
}

Paylaşılan kitaplıklara bağımlılıklar

Sistemdeki Araba kullanıcı arayüzü kitaplığını kullanan her uygulama için, eklenti paket adıyla birlikte application etiketi altındaki uygulama manifestine uses-library etiketini ekleyin:

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

Eklenti yükleme

Modül PRODUCT_PACKAGES'e dahil edilerek eklentiler sistem bölümüne önceden yüklenmelidir. Önceden yüklenmiş paket, diğer yüklü uygulamalar gibi güncellenebilir.

Sistemdeki mevcut bir eklentiyi güncelliyorsanız bu eklentiyi kullanan tüm uygulamalar otomatik olarak kapanır. Kullanıcı tarafından yeniden açıldığında güncellenmiş değişiklikler gösterilir. Uygulama çalışmıyorsa bir sonraki başlatma işleminde güncellenmiş eklentiye sahip olur.

Android Studio ile eklenti yüklerken dikkate almanız gereken bazı ek noktalar vardır. Bu makalenin yazıldığı sırada, Android Studio uygulama yükleme işleminde bir hata vardı. Bu hata, eklentilerdeki güncellemelerin geçerli olmamasına neden oluyordu. Bu sorun, eklentinin derleme yapılandırmasında Always install with package manager (disables deploy optimizations on Android 11 and later) (Her zaman paket yöneticisiyle yükle (Android 11 ve sonraki sürümlerde dağıtım optimizasyonlarını devre dışı bırakır)) seçeneği belirlenerek düzeltilebilir.

Ayrıca, eklenti yüklenirken Android Studio, başlatılacak bir ana etkinlik bulamadığına dair bir hata bildiriyor. Eklentide herhangi bir etkinlik (bir amacı çözmek için kullanılan boş amaç hariç) olmadığından bu durum beklenir. Hatayı gidermek için derleme yapılandırmasında Başlat seçeneğini Hiçbir şey olarak değiştirin.

Eklenti Android Studio yapılandırması 1. Şekil. Eklenti Android Studio yapılandırması

Proxy eklentisi

Car UI kitaplığını kullanan uygulamaların özelleştirilmesi, uygulamalar arasında özelleştirmeler aynı olsa bile, değiştirilecek her bir uygulamayı hedefleyen bir RRO gerektirir. Bu, uygulama başına RRO gerektiği anlamına gelir. Araba kullanıcı arayüzü kitaplığını kullanan uygulamaları görün.

Araba kullanıcı arayüzü kitaplığı proxy eklentisi, bileşen uygulamalarını Araba kullanıcı arayüzü kitaplığının statik sürümüne devreden paylaşılan bir eklenti kitaplığı örneğidir. Bu eklenti, işlevsel bir eklenti uygulamaya gerek kalmadan Car UI kitaplığını kullanan uygulamalar için tek bir özelleştirme noktası olarak kullanılabilen bir RRO ile hedeflenebilir. RRO'lar hakkında daha fazla bilgi için Bir uygulamanın kaynaklarının değerini çalışma zamanında değiştirme başlıklı makaleyi inceleyin.

Proxy eklentisi, eklenti kullanarak özelleştirme yapmaya yönelik yalnızca bir örnek ve başlangıç noktasıdır. RRO'ların ötesinde özelleştirme için eklenti bileşenlerinin bir alt kümesi uygulanabilir ve geri kalan için proxy eklentisi kullanılabilir veya tüm eklenti bileşenleri tamamen sıfırdan uygulanabilir.

Proxy eklentisi, uygulamalar için tek bir RRO özelleştirme noktası sağlasa da eklentiyi kullanmayı devre dışı bırakan uygulamalar, doğrudan uygulamayı hedefleyen bir RRO gerektirir.

Eklenti API'lerini uygulama

Eklentinin ana giriş noktası com.android.car.ui.plugin.PluginVersionProviderImpl sınıfıdır. Tüm eklentiler, bu ad ve paket adıyla tam olarak eşleşen bir sınıf içermelidir. Bu sınıfın varsayılan bir oluşturucusu olmalı ve PluginVersionProviderOEMV1 arayüzünü uygulamalıdır.

CarUi eklentileri, eklentiden eski veya yeni uygulamalarla çalışmalıdır. Bunu kolaylaştırmak için tüm eklenti API'leri, sınıf adlarının sonunda V# ile sürüm oluşturulur. Araba kullanıcı arayüzü kitaplığının yeni özellikler içeren yeni bir sürümü yayınlanırsa bu özellikler bileşenin V2 sürümünde yer alır. Araba kullanıcı arayüzü kitaplığı, yeni özelliklerin eski bir eklenti bileşeni kapsamında çalışması için elinden geleni yapar. Örneğin, araç çubuğundaki yeni bir düğme türünü MenuItems simgesine dönüştürerek.

Ancak, Car UI kitaplığının eski bir sürümünü kullanan uygulamalar, daha yeni API'lere göre yazılmış yeni bir eklentiye uyum sağlayamaz. Bu sorunu çözmek için eklentilerin, uygulamalar tarafından desteklenen OEM API sürümüne göre kendilerinin farklı uygulamalarını döndürmesine izin veriyoruz.

PluginVersionProviderOEMV1 içinde bir yöntem vardır:

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

Bu yöntem, eklenti tarafından desteklenen PluginFactoryOEMV# sürümünün en yükseğini uygulayan ancak yine de maxVersion değerinden küçük veya bu değere eşit olan bir nesne döndürür. Bir eklentide bu kadar eski bir PluginFactory uygulaması yoksa null döndürebilir. Bu durumda, CarUi bileşenlerinin statik olarak bağlı uygulaması kullanılır.

Statik Car Ui kitaplığının eski sürümlerine göre derlenen uygulamalarla geriye dönük uyumluluğu korumak için PluginVersionProvider sınıfının eklentinizin uygulamasında maxVersion, 2, 5 ve daha yüksek değerlerin desteklenmesi önerilir. 1, 3 ve 4. sürümler desteklenmez. Daha fazla bilgi için PluginVersionProviderImpl konusuna bakın.

PluginFactory, diğer tüm CarUi bileşenlerini oluşturan arayüzdür. Ayrıca, arayüzlerinin hangi sürümünün kullanılacağını da tanımlar. Eklenti bu bileşenlerden herhangi birini uygulamaya çalışmıyorsa oluşturma işlevinde null döndürebilir (ayrı bir customizesBaseLayout() işlevi olan araç çubuğu hariç).

pluginFactory, CarUi bileşenlerinin hangi sürümlerinin birlikte kullanılabileceğini sınırlar. Örneğin, bileşenlerin çok çeşitli sürümlerinin birlikte çalışacağına dair çok az garanti olacağından, Toolbar'in 100. sürümünü ve RecyclerView'ın 1. sürümünü oluşturabilecek bir pluginFactory asla olmayacaktır. Geliştiricilerin araç çubuğu sürüm 100'ü kullanabilmesi için pluginFactory sürümünün, araç çubuğu sürüm 100'ü oluşturan bir uygulamasını sağlaması gerekir. Bu uygulama, oluşturulabilecek diğer bileşenlerin sürümlerindeki seçenekleri sınırlar. Diğer bileşenlerin sürümleri eşit olmayabilir. Örneğin, pluginFactoryOEMV100, ToolbarControllerOEMV100 ve RecyclerViewOEMV70 oluşturabilir.

Araç Çubuğu

Temel düzen

Araç çubuğu ve "temel düzen" çok yakından ilişkilidir. Bu nedenle, araç çubuğunu oluşturan işlev installBaseLayoutAround olarak adlandırılır. Temel düzen, araç çubuğunun uygulamanın içeriğinin etrafında herhangi bir yere yerleştirilmesine olanak tanıyan bir kavramdır. Bu sayede, uygulamanın üst/alt kısmında, kenarlarında dikey olarak veya hatta tüm uygulamayı çevreleyen dairesel bir araç çubuğu oluşturulabilir. Bu, araç çubuğu/temel düzenin sarmalanması için installBaseLayoutAround öğesine bir görünüm iletilerek gerçekleştirilir.

Eklenti, sağlanan görünümü almalı, üst öğesinden ayırmalı, eklentinin kendi düzenini üst öğenin aynı dizininde ve yeni ayrılan görünümle aynı LayoutParams ile genişletmeli, ardından görünümü yeni genişletilen düzenin bir yerine yeniden eklemelidir. Şişirilmiş düzende, uygulama tarafından istenirse araç çubuğu bulunur.

Uygulama, araç çubuğu olmayan bir temel düzen isteyebilir. Bu durumda, installBaseLayoutAround null değerini döndürmelidir. Çoğu eklenti için bu yeterlidir ancak eklenti yazarı, örneğin uygulamanın kenarına bir süsleme uygulamak isterse bu işlem temel düzenle yapılabilir. Bu süslemeler, özellikle dikdörtgen olmayan ekranlara sahip cihazlarda kullanışlıdır. Uygulamayı dikdörtgen bir alana itebilir ve dikdörtgen olmayan alana temiz geçişler ekleyebilirler.

installBaseLayoutAround da Consumer<InsetsOEMV1> olarak iletilir. Bu tüketici, eklentinin uygulamanın içeriğini kısmen kapladığını (araç çubuğuyla veya başka bir şekilde) uygulamaya bildirmek için kullanılabilir. Uygulama, bu alanda çizim yapmaya devam etmesi gerektiğini ancak kullanıcı etkileşimine açık önemli bileşenleri bu alanın dışında tutması gerektiğini anlar. Bu efekt, araç çubuğunu yarı şeffaf hale getirmek ve listelerin araç çubuğunun altında kaydırılmasını sağlamak için referans tasarımımızda kullanılır. Bu özellik uygulanmasaydı listedeki ilk öğe araç çubuğunun altında kalır ve tıklanamazdı. Bu efekt gerekmiyorsa eklenti, Consumer'ı yoksayabilir.

Araç çubuğunun altında içerik kaydırma Şekil 2. Araç çubuğunun altında içerik kaydırma

Uygulama açısından, eklenti yeni yerleşimler gönderdiğinde bunları InsetsChangedListener uygulayan etkinliklerden veya parçalardan alır. Bir etkinlik veya parça InsetsChangedListener öğesini uygulamıyorsa Car Ui kitaplığı, parçayı içeren Activity veya FragmentActivity öğesine dolgu olarak ekler uygulayarak ekleri varsayılan olarak işler. Kitaplık, varsayılan olarak parçalara ekler uygulamamaktadır. Uygulamada, RecyclerView üzerinde dolgu olarak iç kısımları uygulayan bir uygulama snippet'i örneğini aşağıda bulabilirsiniz:

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

Son olarak, eklentiye, sarmalanması gereken görünümün uygulamanın tamamını mı yoksa yalnızca küçük bir bölümünü mü kapladığını belirtmek için kullanılan bir fullscreen ipucu verilir. Bu, yalnızca ekranın kenarında göründüklerinde anlamlı olan bazı süslemelerin kenar boyunca uygulanmasını önlemek için kullanılabilir. Tam ekran olmayan temel düzenleri kullanan bir örnek uygulama olarak Ayarlar'ı verebiliriz. Bu uygulamada, iki panelli düzenin her bölmesinin kendi araç çubuğu vardır.

installBaseLayoutAround, toolbarEnabled false olduğunda null değerini döndürmesi beklendiğinden, eklentinin temel düzeni özelleştirmek istemediğini belirtmesi için customizesBaseLayout öğesinden false değerini döndürmesi gerekir.

Temel düzende, döner kontrollerin tam olarak desteklenmesi için bir FocusParkingView ve bir FocusArea bulunmalıdır. Döner kontrolü desteklemeyen cihazlarda bu görünümler atlanabilir. FocusParkingView/FocusAreas, statik CarUi kitaplığında uygulanır. Bu nedenle, bağlamlardan görünümler oluşturmak için fabrikalar sağlamak üzere setRotaryFactories kullanılır.

Odaklanma görünümlerini oluşturmak için kullanılan bağlamlar, eklentinin bağlamı değil kaynak bağlam olmalıdır. Kullanıcıya görünür bir odaklanma olmaması gerektiğinde odaklanılan öğe olduğundan, FocusParkingView, ağaçtaki ilk görünüme mümkün olduğunca yakın olmalıdır. FocusArea, döner dokunma bölgesi olduğunu belirtmek için araç çubuğunu temel düzende sarmalamalıdır. FocusArea sağlanmazsa kullanıcı, döner denetleyiciyle araç çubuğundaki düğmelere gidemez.

Araç çubuğu denetleyicisi

Döndürülen gerçek ToolbarController, temel düzenden çok daha kolay uygulanmalıdır. Görevi, ayarlayıcılarına iletilen bilgileri alıp temel düzende göstermektir. Çoğu yöntemle ilgili bilgi için Javadoc'a bakın. Daha karmaşık yöntemlerden bazıları aşağıda ele alınmıştır.

getImeSearchInterface, IME (klavye) penceresinde arama sonuçlarını göstermek için kullanılır. Örneğin, klavye ekranın yalnızca yarısını kaplıyorsa bu, arama sonuçlarını klavyeyle birlikte göstermek/canlandırmak için yararlı olabilir. İşlevlerin çoğu statik CarUi kitaplığında uygulanır. Eklentideki arama arayüzü, statik kitaplığın TextView ve onPrivateIMECommand geri çağırmalarını alması için yalnızca yöntemler sağlar. Bunu desteklemek için eklenti, TextView sınıfını geçersiz kılan ve çağrıyı, sağlanan dinleyiciye arama çubuğunun TextView olarak ileten bir TextView alt sınıfı kullanmalıdır.onPrivateIMECommand

setMenuItems yalnızca MenuItems'ı ekranda gösterir ancak şaşırtıcı derecede sık çağrılır. MenuItems için eklenti API'si değişmez olduğundan bir MenuItem her değiştirildiğinde tamamen yeni bir setMenuItems çağrısı yapılır. Bu durum, kullanıcının bir anahtar MenuItem'i tıklaması ve bu tıklamanın anahtarın açılıp kapanmasına neden olması gibi basit bir olayda meydana gelebilir. Bu nedenle, hem performans hem de animasyon açısından eski ve yeni MenuItems listesi arasındaki farkın hesaplanması ve yalnızca gerçekten değişen görünümlerin güncellenmesi önerilir. MenuItems, bu konuda yardımcı olabilecek bir key alanı sağlar. Anahtar, aynı MenuItem için setMenuItems'ye yapılan farklı çağrılarda aynı olmalıdır.

AppStyledView

AppStyledView, hiç özelleştirilmemiş bir görünümün kapsayıcısıdır. Uygulamanın geri kalanından ayrışmasını sağlayan ve kullanıcıya bunun farklı bir arayüz olduğunu belirten bir kenarlık sağlamak için kullanılabilir. AppStyledView tarafından sarmalanan görünüm, setContent içinde verilir. Uygulamanın isteği doğrultusunda AppStyledView öğesinde geri veya kapat düğmesi de olabilir.

AppStyledView, installBaseLayoutAround gibi görünümlerini görünüm hiyerarşisine hemen eklemez. Bunun yerine, görünümünü getView aracılığıyla statik kitaplığa döndürür. Ekleme işlemi daha sonra yapılır. AppStyledView konum ve boyutu, getDialogWindowLayoutParam uygulanarak da kontrol edilebilir.

Bağlamlar

Eklenti, hem eklenti hem de "kaynak" bağlamları olduğundan bağlamları kullanırken dikkatli olmalıdır. Eklenti bağlamı, getPluginFactory için bağımsız değişken olarak verilir ve eklentinin kaynaklarını içeren tek bağlamdır. Bu, eklentide düzenleri genişletmek için kullanılabilecek tek bağlam olduğu anlamına gelir.

Ancak eklenti bağlamında doğru yapılandırma ayarlanmamış olabilir. Doğru yapılandırmayı elde etmek için bileşen oluşturan yöntemlerde kaynak bağlamları sağlıyoruz. Kaynak bağlam genellikle bir etkinliktir ancak bazı durumlarda bir hizmet veya başka bir Android bileşeni de olabilir. Kaynak bağlamındaki yapılandırmayı eklenti bağlamındaki kaynaklarla kullanmak için createConfigurationContext kullanılarak yeni bir bağlam oluşturulmalıdır. Doğru yapılandırma kullanılmazsa Android katı modu ihlali olur ve şişirilmiş görünümlerin boyutları doğru olmayabilir.

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

Mod değişiklikleri

Bazı eklentiler, bileşenleri için birden fazla modu destekleyebilir. Örneğin, görsel olarak farklı görünen bir spor modu veya eko modu. CarUi'de bu tür işlevler için yerleşik destek yoktur ancak eklentinin bu işlevi tamamen dahili olarak uygulamasına engel olan bir durum yoktur. Eklenti, modlar arasında ne zaman geçiş yapacağını belirlemek için yayınları dinlemek gibi istediği koşulları izleyebilir. Eklenti, modları değiştirmek için yapılandırma değişikliğini tetikleyemez. Ancak her bileşenin görünümünü manuel olarak güncellemek kullanıcı için daha sorunsuz olduğundan ve yapılandırma değişiklikleriyle mümkün olmayan geçişlere izin verdiğinden yapılandırma değişikliklerine güvenmeniz önerilmez.

Jetpack Compose

Eklentiler Jetpack Compose kullanılarak uygulanabilir ancak bu, alfa düzeyinde bir özelliktir ve kararlı olarak kabul edilmemelidir.

Eklentiler, oluşturma özellikli bir yüzey oluşturmak için ComposeView kullanabilir. Bu ComposeView, bileşenlerdeki getView yönteminde uygulamaya döndürülen değerdir.

ComposeView kullanmayla ilgili önemli bir sorun, hiyerarşideki farklı ComposeView'lar arasında paylaşılan global değişkenleri depolamak için düzenin kök görünümünde etiketler ayarlamasıdır. Eklentinin kaynak kimlikleri, uygulamanın kaynak kimliklerinden ayrı olarak adlandırılmadığından hem uygulama hem de eklenti aynı görünümde etiket ayarladığında çakışmalar oluşabilir. Bu genel değişkenleri ComposeViewWithLifecycle konumuna taşıyan özel bir ComposeViewWithLifecycle aşağıda verilmiştir.ComposeView Bu sürümün de kararlı sürüm olarak kabul edilmemesi gerekir.

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