Araba kullanıcı arayüzü eklentileri

Araba kullanıcı arayüzü kitaplığında, çalışma zamanında kaynak yer paylaşımlarını (RRO'lar) kullanmak yerine bileşen özelleştirmelerinin eksiksiz uygulamalarını oluşturmak için Araba kullanıcı arayüzü 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 öğelerin kapsamını sınırlandırır.

Eklenti oluşturma

Araba kullanıcı arayüzü kitaplığı eklentisi, bir dizi eklenti API'sini uygulayan sınıflar içeren bir APK'dir. 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

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

Eklentide, manifest dosyasında aşağıdaki özelliklere sahip bir içerik sağlayıcı tanımlanmış olmalıdır:

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

android:authorities="com.android.car.ui.plugin", eklentinin Araba kullanıcı arayüzü kitaplığında bulunabilir olmasını sağlar. Çalıştırma sırasında sorgulanabilir olması için sağlayıcının 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ında 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ında bağımlılığı belirtirse sınıf yükleyicisi, eklenti paylaşılan kitaplığının sınıflarına erişebilir. Paylaşılan kitaplık yerine normal Android uygulamaları olarak uygulanan eklentiler, uygulamanın soğuk başlatma sürelerini olumsuz yönde etkileyebilir.

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

Android paylaşılan kitaplıklarıyla geliştirme yapmak, normal Android uygulamalarına benzer ancak aralarında birkaç temel fark vardır.

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

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

Sistemde Car UI kitaplığını kullanan her uygulama için uygulama manifest dosyasına application etiketinin altına eklenti paket adıyla birlikte 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

Eklentiler, modülü PRODUCT_PACKAGES'e ekleyerek sistem bölümüne ÖNCEDEN YÜKLENMESİ GEREKİR. Önceden yüklenmiş paket, yüklü diğer uygulamalara benzer şekilde güncellenebilir.

Sistemde 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şiklikleri görür. Uygulama çalışmıyorsa bir sonraki başlatmada güncellenmiş eklenti yüklenir.

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 sürecinde, eklenti güncellemelerinin geçerli olmamasına neden olan bir hata mevcuttur. Bu sorun, eklentinin derleme yapılandırmasında 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, Android Studio eklentiyi yüklerken başlatılacak bir ana etkinlik bulamadığıyla ilgili bir hata bildirir. Bu, eklentide etkinlik olmadığı için (bir amacı çözmek için kullanılan boş intent hariç) beklenen bir durumdur. Hatayı ortadan kaldırmak 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ı Şekil 1. Eklenti Android Studio yapılandırması

Proxy eklentisi

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

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 delege eden örnek bir eklenti paylaşılan kitaplığıdır. Bu eklenti, işlevsel bir eklenti uygulamak zorunda kalmadan Araba kullanıcı arayüzü kitaplığını kullanan uygulamalar için tek bir özelleştirme noktası olarak kullanılabilecek bir RRO ile hedeflenmelidir. RRO'lar hakkında daha fazla bilgi için 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 yapmak için yalnızca bir örnek ve başlangıç noktasıdır. RRO'ların ötesinde özelleştirme için, eklenti bileşenlerinin bir alt kümesini uygulayabilir ve geri kalanı için proxy eklentisini kullanabilir veya tüm eklenti bileşenlerini sıfırdan uygulayabilirsiniz.

Proxy eklentisi, uygulamalar için tek bir RRO özelleştirme noktası sunsa da eklentiyi kullanmayı devre dışı bırakan uygulamalar için doğrudan uygulamayı hedefleyen bir RRO'ya yine de ihtiyaç duyulur.

Eklenti API'lerini uygulama

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

CarUi eklentileri, eklentiden daha eski veya daha yeni uygulamalarla çalışmalıdır. Bunu kolaylaştırmak için tüm eklenti API'leri, sınıf adlarının sonuna V# eklenerek sürümlendirilir. Araba kullanıcı arayüzü kitaplığının yeni özellikleri içeren yeni bir sürümü yayınlanırsa bu özellikler bileşenin V2 sürümüne dahil edilir. Araba kullanıcı arayüzü kitaplığı, yeni özelliklerin eski bir eklenti bileşeni kapsamında çalışmasını sağlamak için elinden geleni yapar. Örneğin, araç çubuğundaki yeni bir düğme türünü MenuItems olarak dönüştürebilirsiniz.

Ancak, Araba kullanıcı arayüzü kitaplığının eski bir sürümüne sahip olan uygulamalar, daha yeni API'ler için 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 kendi farklı uygulamalarını döndürmesine izin veriyoruz.

PluginVersionProviderOEMV1 içinde bir yöntem bulunur:

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

Bu yöntem, PluginFactoryOEMV# için eklenti tarafından desteklenen en yüksek sürümü uygulayan ancak maxVersion'ten küçük veya maxVersion'e eşit olan bir nesne döndürür. Bir eklentide bu kadar eski bir PluginFactory uygulaması yoksa null döndürülebilir. 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, eklentinizin PluginVersionProvider sınıfı uygulamasından 2, 5 ve sonraki sürümlerin maxVersion'lerini desteklemeniz önerilir. 1, 3 ve 4 sürümleri desteklenmez. Daha fazla bilgi için PluginVersionProviderImpl bölümüne 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 uygulamak istemiyorsa oluşturma işlevinde null döndürebilir (ayrı bir customizesBaseLayout() işlevine sahip araç çubuğu hariç).

pluginFactory, CarUi bileşenlerinin hangi sürümlerinin birlikte kullanılabileceğini sınırlar. Örneğin, Toolbar'un 100. sürümünü ve RecyclerView'nin 1. sürümünü oluşturabilecek bir pluginFactory hiçbir zaman olmayacak. Bunun nedeni, bileşenlerin çok çeşitli sürümlerinin birlikte çalışacağı konusunda çok az garantinin olmasıdır. 100 numaralı araç çubuğu sürümünü kullanmak için geliştiricilerin, pluginFactory sürümünün 100 numaralı araç çubuğu sürümünü oluşturan bir uygulamasını sağlaması gerekir. Bu uygulama, oluşturulabilecek diğer bileşen sürümlerindeki seçenekleri sınırlandırır. Diğer bileşenlerin sürümleri eşit olmayabilir. Örneğin, bir pluginFactoryOEMV100, bir ToolbarControllerOEMV100 ve bir RecyclerViewOEMV70 oluşturabilir.

Araç Çubuğu

Temel düzen

Araç çubuğu ve "temel düzen" birbirine çok yakın olduğundan araç çubuğunu oluşturan işleve installBaseLayoutAround adı verilir. Temel düzen, araç çubuğunun uygulama 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, dikey olarak yanlarda veya hatta tüm uygulamayı çevreleyen dairesel bir araç çubuğu kullanılabilir. Bu, araç çubuğunun/temel düzenin etrafı sarması için installBaseLayoutAround'ye bir görünüm geçirilerek yapılır.

Eklenti, sağlanan görünümü alıp üst öğesinden ayırmalı, eklentinin kendi düzenini üst öğenin aynı dizininde ve yeni ayrılan görünümle aynı LayoutParams ile şişirmeli, ardından görünümü yeni şişirilen düzenin bir yerine yeniden eklemelidir. Uygulama tarafından istenirse şişirilmiş düzende araç çubuğu yer alır.

Uygulama, araç çubuğu olmayan bir temel düzen isteyebilir. Bu durumda installBaseLayoutAround null değerini döndürmelidir. Çoğu eklenti için yapılması gereken tek şey budur. Ancak eklenti yazarı, örneğin uygulamanın etrafına bir dekorasyon uygulamak isterse bu, temel bir düzenle de yapılabilir. Bu süslemeler, uygulamayı dikdörtgen bir alana itekledikleri ve dikdörtgen olmayan alana düzgün geçişler ekledikleri için özellikle dikdörtgen olmayan ekranlara sahip cihazlar için kullanışlıdır.

installBaseLayoutAround, Consumer<InsetsOEMV1> parametresi de iletilir. Bu tüketici, eklentinin uygulamanın içeriğini kısmen (araç çubuğuyla veya başka bir şekilde) kapladığını uygulamaya bildirmek için kullanılabilir. Bu durumda uygulama, bu alanda çizmeye devam etmeyi ancak kullanıcının etkileşimde bulunabileceği kritik bileşenleri bu alandan uzak tutmayı bilir. Bu efekt, referans tasarımımızda araç çubuğunu yarı saydam hale getirmek ve listelerin altında kaydırılmasını sağlamak için kullanılır. Bu özellik uygulanmazsa listedeki ilk öğe araç çubuğunun altına yapışır ve tıklanamaz. Bu etki gerekli değilse eklenti, tüketiciyi yoksayabilir.

İçeriklerin araç çubuğunun altına kayması Şekil 2. İçeriklerin araç çubuğunun altına kayması

Uygulama açısından, eklenti yeni ek gönderdiğinde bunları InsetsChangedListener uygulayan tüm etkinliklerden veya parçalardan alır. Bir etkinlik veya parça InsetsChangedListener'ü uygulamazsa Car Ui kitaplığı, parçayı içeren Activity veya FragmentActivity'ye dolgu olarak uygulayarak varsayılan olarak iç içe yerleştirilmeleri işler. Kitaplık, varsayılan olarak iç içe yerleştirilmeleri parçalara uygulamaz. Uygulamadaki RecyclerView öğesine dolgu olarak iç içe yerleştirilmiş öğeleri uygulayan bir uygulama örneği snippet'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 bir fullscreen ipucu verilir. Bu ipucu, 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ılır. Bu, kenara yalnızca ekranın tamamının kenarında göründüğünde anlamlı olan bazı süslemelerin uygulanmasını önlemek için kullanılabilir. Tam ekran olmayan temel düzenler kullanan örnek bir uygulama, çift bölmeli düzenin her bölmesinin kendi araç çubuğuna sahip olduğu Ayarlar'dır.

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

Döner kontrolleri tam olarak desteklemek için temel düzende bir FocusParkingView ve bir FocusArea bulunmalıdır. Bu görünümler, döner ekranı desteklemeyen cihazlarda atlanabilir. FocusParkingView/FocusAreas, statik CarUi kitaplığında uygulandığından, bağlamlardan görünüm oluşturmak için fabrikalar sağlamak üzere bir setRotaryFactories kullanılır.

Odak görünümleri oluşturmak için kullanılan bağlamlar, eklentinin bağlamı değil kaynak bağlam olmalıdır. Kullanıcı tarafından görülebilen bir odak olmadığında odak noktası olduğu için FocusParkingView, ağaçtaki ilk görünüme mümkün olduğunca yakın olmalıdır. FocusArea, dönen bir dokunma bölgesi olduğunu belirtmek için araç çubuğunu temel düzene sarmalaması gerekir. FocusArea sağlanmazsa kullanıcı, döner kontrol cihazıyla araç çubuğundaki düğmelere gidemez.

Araç çubuğu denetleyicisi

Döndürülen gerçek ToolbarController, temel düzene kıyasla çok daha kolay uygulanabilir olmalıdır. İşi, ayarlayıcılarına iletilen bilgileri alıp temel düzende görüntülemektir. Çoğu yöntemle ilgili bilgi için Javadoc'a bakın. Daha karmaşık yöntemlerden bazıları aşağıda açıklanmıştır.

getImeSearchInterface, arama sonuçlarını IME (klavye) penceresinde göstermek için kullanılır. Bu, arama sonuçlarını klavyenin yanında görüntülemek/animasyonlu olarak göstermek için yararlı olabilir (ör. klavye ekranın yalnızca yarısını kaplıyorsa). İşlevlerin çoğu statik CarUi kitaplığında uygulanır. Eklentideki arama arayüzü, statik kitaplığın TextView ve onPrivateIMECommand geri çağırma işlevlerini almasına yönelik yöntemler sağlar. Bunu desteklemek için eklenti, onPrivateIMECommand'u geçersiz kılan ve arama çubuğunun TextView olarak çağrıyı sağlanan dinleyiciye ileten bir TextView alt sınıfı kullanmalıdır.

setMenuItems, ekranda MenuItem öğelerini gösterir ancak şaşırtıcı bir şekilde sık çağrılır. MenuItem'ler için eklenti API'si değiştirilemez olduğundan, bir MenuItem değiştirildiğinde tamamen yeni bir setMenuItems çağrısı gerçekleşir. Bu durum, kullanıcının bir açma/kapatma MenuItem'ini tıklaması ve bu tıklamanın açma/kapatma düğmesinin açılmasına neden olması gibi önemsiz bir şeyden kaynaklanabilir. Bu nedenle, hem performans hem de animasyon açısından eski ve yeni MenuItems listesi arasındaki farkı hesaplamanız ve yalnızca gerçekten değişen görünümleri güncellemeniz önerilir. MenuItems, aynı MenuItem için setMenuItems'a yapılan farklı çağrılarda anahtar aynı olacağından bu konuda yardımcı olabilecek bir key alanı sağlar.

AppStyledView

AppStyledView, hiç özelleştirilmemiş bir görünümün kapsayıcısıdır. Bu görünümün etrafına, uygulamanın geri kalanından öne çıkmasını sağlayan bir kenarlık eklemek ve kullanıcıya bunun farklı bir arayüz türü olduğunu belirtmek için kullanılabilir. AppStyledView tarafından sarmalanmış görünüm setContent içinde verilmiştir. AppStyledView, uygulamanın istediği şekilde bir geri veya kapat düğmesi de içerebilir.

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. Daha sonra ekleme işlemini getView gerçekleştirir. AppStyledView öğesinin konumu ve boyutu, getDialogWindowLayoutParam uygulanarak da kontrol edilebilir.

Bağlamlar

Hem eklenti hem de "kaynak" bağlamları olduğundan eklenti, bağlamları kullanırken dikkatli olmalıdır. Eklenti bağlamı, getPluginFactory bağımsız değişkeni olarak verilir ve eklentinin kaynaklarını içeren tek bağlamdır. Bu, eklentide düzenleri şişirmek 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ğlarız. Kaynak bağlamı genellikle bir etkinliktir ancak bazı durumlarda 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ı mod ihlali olur ve şişirilmiş görüntülemelerin boyutları doğru olmayabilir.

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

Mod değişiklikleri

Bazı eklentiler, bileşenleri için birden fazla mod destekleyebilir. Örneğin, görsel olarak farklı görünen bir spor modu veya eko modu. CarUi'da bu tür işlevler için yerleşik destek yoktur ancak eklentinin bunu tamamen dahili olarak uygulamasını engelleyen bir şey yoktur. Eklenti, modu ne zaman değiştireceğini belirlemek için istediği koşulları (ör. yayınları dinleme) izleyebilir. Bu eklenti, modları değiştirmek için yapılandırma değişikliği tetikleyemez. Ancak her bir bileşenin görünümünü manuel olarak güncellemek kullanıcı için daha kolay olduğu ve yapılandırma değişiklikleriyle mümkün olmayan geçişlere olanak tanıdığı için yapılandırma değişikliklerine güvenilmesi önerilmez.

Jetpack Compose

Jetpack Compose kullanılarak eklentiler uygulanabilir ancak bu alfa düzeyinde bir özelliktir ve istikrarlı kabul edilmemelidir.

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

ComposeView kullanmanın en önemli sorunlarından biri, hiyerarşideki farklı ComposeView'lar arasında paylaşılan global değişkenleri depolamak için düzendeki kök görünümde etiketler ayarlamasıdır. Eklentinin kaynak kimlikleri, uygulamanın kaynak kimliklerinden ayrı bir ad alanında olmadığından hem uygulama hem de eklenti aynı görünümde etiket belirlediğinde bu durum çakışmalara neden olabilir. Aşağıda, bu genel değişkenleri ComposeView bölümüne taşıyan özel bir ComposeViewWithLifecycle verilmiştir. Bu, kararlı bir sürüm olarak kabul edilmemelidir.

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