Araç kullanıcı arayüzü eklentileri

Çalışma zamanı kaynak katmanlarını (RRO'lar) kullanmak yerine Araç Kullanıcı Arayüzü kitaplığında bileşen özelleştirmelerinin eksiksiz uygulamalarını oluşturmak için Araç Kullanıcı Arayüzü kitaplığı eklentilerini kullanın. RRO'lar yalnızca Araç Kullanıcı Arayüzü kitaplığı bileşenlerinin XML kaynaklarını değiştirmenize olanak tanır ve bu da özelleştirebileceğiniz öğelerin kapsamını sınırlar.

Eklenti oluştur

Araç Kullanıcı Arayüzü kitaplığı eklentisi, bir dizi Eklenti API'sini uygulayan sınıfları içeren bir APK'dır. Eklenti API'leri statik bir kitaplık olarak bir eklentide derlenebilir.

Soong ve Gradle'daki örneklere bakın:

Soong

Şu Soong örneğini düşünün:

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",
}

kepçe

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

Eklentinin manifest dosyasında aşağıdaki özelliklere sahip bir içerik sağlayıcısı belirtilmesi gerekir:

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

android:authorities="com.android.car.ui.plugin" eklentinin Araç Kullanıcı Arayüzü kitaplığı tarafından keşfedilmesini sağlar. Sağlayıcının çalışma zamanında sorgulanabilmesi için dışa aktarılması gerekir. Ayrıca, enabled öznitelik false olarak ayarlanırsa, eklenti uygulaması yerine varsayılan uygulama kullanılacaktı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 bildirim 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 bir güvenlik önlemi olarak uygulamanızı imzalayın .

Paylaşılan bir kitaplık olarak eklentiler

Doğrudan uygulamalarda 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 halinde derlenir.

Android paylaşımlı kitaplığı olarak uygulanan eklentilerin sınıfları, uygulamalar arasında paylaşılan sınıf yükleyiciye otomatik olarak eklenir. Araç Kullanıcı Arayüzü kitaplığını kullanan bir uygulama, eklenti paylaşılan kitaplığına bir çalışma zamanı bağımlılığı belirttiğinde, bu uygulamanın 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 yönde etkileyebilir.

Paylaşılan kütüphaneleri uygulayın ve oluşturun

Android paylaşımlı kitaplıklarıyla geliştirme yapmak, birkaç önemli fark dışında normal Android uygulamalarına çok benzer.

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

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

Sistemdeki Araç Kullanıcı Arayüzü kitaplığını kullanan her uygulama için, uygulama etiketinin altındaki application bildirimine 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>

Bir eklenti yükleyin

Eklentiler, modül PRODUCT_PACKAGES dahil edilerek sistem bölümüne önceden kurulmalıdır ZORUNLU. Önceden yüklenmiş paket, diğer yüklü 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şikliklere sahip olurlar. Uygulama çalışmıyorsa, bir sonraki başlatılışında güncellenmiş eklentiye sahip olur.

Android Studio ile bir eklenti yüklerken dikkate alınması gereken bazı ek hususlar vardır. Bu yazının yazıldığı sırada, Android Studio uygulaması yükleme sürecinde, eklentiye yönelik güncellemelerin etkili olmamasına neden olan bir hata mevcuttu. Bu, 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 seçilerek düzeltilebilir.

Ayrıca eklentiyi yüklerken Android Studio başlatılacak ana aktivite bulamadığını belirten bir hata bildiriyor. Eklentinin herhangi bir etkinliği olmadığından (bir amacı çözmek için kullanılan boş amaç dışında) bu beklenen bir durumdur. Hatayı ortadan kaldırmak için yapı 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

Uygulamaların Araç Kullanıcı Arayüzü kitaplığını kullanarak özelleştirilmesi, özelleştirmelerin uygulamalar arasında aynı olduğu durumlar da dahil olmak üzere, değiştirilecek her bir özel uygulamayı hedefleyen bir RRO gerektirir. Bu, uygulama başına bir RRO'nun gerekli olduğu anlamına gelir. Hangi uygulamaların Araç Kullanıcı Arayüzü kitaplığını kullandığını görün.

Car UI kitaplığı proxy eklentisi, bileşen uygulamalarını Araba UI kitaplığının statik sürümüne devreden örnek bir eklenti paylaşılan kitaplığıdır. Bu eklenti, işlevsel bir eklenti uygulamaya gerek kalmadan Araç Kullanıcı Arayüzü 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 bkz. Çalışma zamanında bir uygulamanın kaynaklarının değerini değiştirme .

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 tamamen sıfırdan uygulayabilirsiniz.

Proxy eklentisi uygulamalar için tek bir RRO özelleştirme noktası sağlasa da, eklentiyi kullanmayı tercih etmeyen uygulamalar yine de doğrudan uygulamanın kendisini hedefleyen bir RRO'ya ihtiyaç duyacaktır.

Eklenti API'lerini uygulayın

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 yapıcısı 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 sonunda bir V# ile sürümlendirilir. Araç Kullanıcı Arayüzü kitaplığının yeni özelliklerle birlikte yeni bir sürümü yayınlanırsa, bunlar bileşenin V2 sürümünün bir parçasıdır. Araç 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 dönüştürerek.

Ancak Car UI kitaplığının eski bir sürümüne sahip bir uygulama, daha yeni API'lere karşı yazılmış yeni bir eklentiye uyum sağlayamaz. Bu sorunu çözmek için eklentilerin, uygulamalar tarafından desteklenen OEM API sürümüne bağlı olarak 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 en yüksek PluginFactoryOEMV# sürümünü uygulayan, yine de maxVersion küçük veya ona eşit olan bir nesneyi döndürür. Bir eklentinin o kadar eski bir PluginFactory uygulaması yoksa null değerini döndürebilir; bu durumda CarUi bileşenlerinin statik olarak bağlantılı uygulaması kullanılır.

Statik Araç Kullanıcı Arayüzü 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ı içinden maxVersion 2, 5 ve daha yüksek sürümlerin desteklenmesi önerilir. Sürüm 1, 3 ve 4 desteklenmemektedir. Daha fazla bilgi için bkz. PluginVersionProviderImpl .

PluginFactory , diğer tüm CarUi bileşenlerini oluşturan arayüzdür. Ayrıca arayüzlerinin hangi sürümünün kullanılması gerektiğini de tanımlar. Eklenti bu bileşenlerden herhangi birini uygulamaya çalışmazsa, oluşturma işlevinde null değerini 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, çok çeşitli bileşen sürümlerinin birlikte çalışacağına dair çok az garanti olacağından, bir Toolbar 100 sürümünü ve ayrıca RecyclerView 1. sürümünü oluşturabilen bir pluginFactory hiçbir zaman olmayacaktır. Araç çubuğu sürüm 100'ü kullanmak için geliştiricilerin, bir araç çubuğu sürümü 100 oluşturan ve daha sonra oluşturulabilecek diğer bileşenlerin sürümlerindeki seçenekleri sınırlayan bir pluginFactory sürümünün uygulamasını sağlamaları beklenir. 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" çok yakından ilişkilidir, dolayısıyla araç çubuğunu oluşturan işleve installBaseLayoutAround adı verilir. Temel düzen, araç çubuğunun uygulamanın içeriği çevresinde herhangi bir yere konumlandırılmasına, uygulamanın üst/alt kısmında, yanlar boyunca dikey olarak bir araç çubuğuna veya hatta tüm uygulamayı çevreleyen dairesel bir araç çubuğuna izin verecek şekilde konumlandırılmasına olanak tanıyan bir konsepttir. Bu, araç çubuğu/taban düzeninin sarılması için installBaseLayoutAround bir görünüm iletilerek gerçekleştirilir.

Eklenti, sağlanan görünümü almalı, onu üst öğesinden ayırmalı, eklentinin kendi düzenini üst öğenin aynı dizininde ve henüz ayrılan görünümle aynı LayoutParams ile şişirmeli ve ardından görünümü, düzenin içinde bir yere yeniden eklemelidir. sadece şişirildi. Uygulama tarafından istenirse şişirilmiş düzen araç çubuğunu içerecektir.

Uygulama, araç çubuğu olmadan bir temel düzen talep edebilir. Eğer öyleyse, installBaseLayoutAround null değerini döndürmelidir. Çoğu eklenti için olması gereken tek şey budur, ancak eklenti yazarı örneğin uygulamanın kenarına bir dekorasyon uygulamak isterse, bu yine de bir temel düzen ile yapılabilir. Bu dekorasyonlar, uygulamayı dikdörtgen bir alana itebildikleri ve dikdörtgen olmayan alana temiz geçişler ekleyebildikleri için özellikle dikdörtgen olmayan ekranlara sahip cihazlar için kullanışlıdır.

installBaseLayoutAround ayrıca bir Consumer<InsetsOEMV1> iletilir. Bu tüketici, eklentinin uygulamanın içeriğini kısmen kapsadığını (araç çubuğuyla veya başka bir şekilde) uygulamaya iletmek için kullanılabilir. Uygulama daha sonra bu alanda çizim yapmaya devam etmesi gerektiğini bilecek, ancak kullanıcıyla etkileşime girebilecek kritik bileşenleri bunun dışında tutacaktır. Bu efekt, referans tasarımımızda araç çubuğunu yarı şeffaf hale getirmek ve listelerin altında kaydırılmasını sağlamak için kullanılır. Bu özellik uygulanmasaydı, listedeki ilk öğe araç çubuğunun altında kalır ve tıklanamazdı. Bu etkiye ihtiyaç duyulmuyorsa eklenti Tüketiciyi yok sayabilir.

Araç çubuğunun altında içerik kaydırılıyor Şekil 2. Araç çubuğunun altında kayan içerik

Uygulama açısından bakıldığında, eklenti yeni ekler gönderdiğinde, bunları InsetsChangedListener uygulayan tüm etkinliklerden veya parçalardan alacaktır. Bir etkinlik veya parça InsetsChangedListener uygulamıyorsa, Car Ui kitaplığı, ekleri parçayı içeren Activity veya FragmentActivity dolgu olarak uygulayarak ekleri varsayılan olarak işleyecektir. Kitaplık, ekleri varsayılan olarak parçalara uygulamaz. Aşağıda, uygulamadaki bir RecyclerView ekleri dolgu olarak uygulayan bir uygulamanın örnek bir pasajı verilmiştir:

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, sarılması gereken görünümün uygulamanın tamamını mı yoksa sadece küçük bir bölümünü mü kapladığını belirtmek için kullanılan bir fullscreen ipucu verilir. Bu, yalnızca tüm ekranın kenarı boyunca 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 ö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 null değeri döndürmesi beklendiğinden, eklentinin temel düzeni özelleştirmek istemediğini belirtmesi için, customizesBaseLayout öğesinden false döndürmesi gerekir.

Temel düzen, döner kontrolleri tam olarak desteklemek için bir FocusParkingView ve bir FocusArea içermelidir. Döner desteği desteklemeyen cihazlarda bu görünümler atlanabilir. FocusParkingView/FocusAreas statik CarUi kitaplığında uygulanır, dolayısıyla fabrikaların bağlamlardan görünümler oluşturmasını sağlamak için 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. FocusParkingView ağaçtaki ilk görünüme mümkün olduğunca en yakın görünüm olmalıdır; çünkü kullanıcıya görünür bir odak olmaması gerektiğinde odaklanılan görünüm budur. FocusArea , bir döner itme bölgesi olduğunu belirtmek için araç çubuğunu temel düzende sarması gerekir. FocusArea sağlanmazsa kullanıcı, döner denetleyiciyi kullanarak araç çubuğundaki herhangi bir düğmeye gidemez.

Araç çubuğu denetleyicisi

Döndürülen gerçek ToolbarController uygulanması, temel düzenden çok daha kolay olmalıdır. Görevi, ayarlayıcılarına iletilen bilgileri almak ve bunu temel düzende görüntülemektir. Çoğu yöntem hakkında bilgi için Javadoc'a bakın. Daha karmaşık yöntemlerden bazıları aşağıda tartışılmaktadır.

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

setMenuItems ekranda MenuItems'ı görüntüler ancak şaşırtıcı derecede sık çağrılacaktır. MenuItems 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şecektir. Bu, kullanıcının MenuItem anahtarını tıklatması ve bu tıklamanın anahtarın değişmesine neden olması kadar önemsiz bir şey için bile gerçekleşebilir. Hem performans hem de animasyon nedenleriyle, eski ve yeni Menü Öğeleri listesi arasındaki farkın hesaplanması ve yalnızca gerçekten değişen görünümlerin güncellenmesi önerilir. Aynı MenuItem için setMenuItems yapılan farklı çağrılarda anahtarın aynı olması gerektiğinden, MenuItems bu konuda yardımcı olabilecek bir key alanı sağlar.

AppStyledView

AppStyledView , hiç özelleştirilmemiş bir görünüm için bir kapsayıcıdır. Bu görünümün etrafında, uygulamanın geri kalanından öne çıkmasını sağlayacak bir kenarlık sağlamak ve kullanıcıya bunun farklı türde bir arayüz olduğunu belirtmek için kullanılabilir. AppStyledView tarafından sarılmış görünüm setContent verilmiştir. AppStyledView ayrıca uygulamanın talep ettiği şekilde bir geri veya kapatma düğmesine de sahip olabilir.

AppStyledView görünümlerini installBaseLayoutAround yaptığı gibi hemen görünüm hiyerarşisine eklemez, bunun yerine görünümünü getView aracılığıyla statik kitaplığa döndürür ve o da ekleme işlemini gerçekleştirir. AppStyledView konumu ve boyutu, getDialogWindowLayoutParam uygulanarak da kontrol edilebilir.

Bağlamlar

Hem eklenti hem de "kaynak" bağlamları olduğundan, Bağlamları kullanırken eklentinin dikkatli olması gerekir. Eklenti bağlamı getPluginFactory bir argüman olarak verilmiştir ve eklentinin kaynaklarını içeren tek bağlamdır. Bu, eklentideki düzenleri şişirmek için kullanılabilecek tek bağlamın bu olduğu anlamına gelir.

Ancak eklenti içeriğinde doğru yapılandırma ayarlanmamış olabilir. Doğru konfigürasyonu 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ğlamdaki 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 meydana gelir ve şişirilmiş görünümler doğru boyutlara sahip olmayabilir.

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

Mod değişiklikleri

Bazı eklentiler, görsel olarak farklı görünen spor modu veya eko modu gibi bileşenleri için birden fazla modu destekleyebilir. CarUi'de bu tür işlevsellik için yerleşik bir destek yoktur, ancak eklentinin bunu tamamen dahili olarak uygulamasını engelleyen hiçbir şey yoktur. Eklenti, yayınları dinlemek gibi modların ne zaman değiştirileceğini anlamak için istediği koşulları izleyebilir. Eklenti, modları değiştirmek için bir konfigürasyon değişikliğini tetikleyemez, ancak her bileşenin görünümünün manuel olarak güncellenmesi kullanıcı için daha sorunsuz olduğundan ve ayrıca konfigürasyon değişiklikleriyle mümkün olmayan geçişlere izin verdiğinden, konfigürasyon değişikliklerine güvenilmesi önerilmez.

Jetpack Oluşturma

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

Eklentiler, oluşturulacak Compose özellikli bir yüzey oluşturmak için ComposeView kullanabilir. Bu ComposeView bileşenlerdeki getView yönteminden uygulamaya döndürülen şey olacaktır.

ComposeView kullanmanın önemli sorunlarından biri, hiyerarşideki farklı ComposeView'lar arasında paylaşılan genel değişkenleri depolamak için düzendeki kök görünümde etiketler ayarlamasıdır. Eklentinin kaynak kimlikleri uygulamanınkinden ayrı ad alanına sahip olmadığından, hem uygulama hem de eklenti aynı görünümde etiket ayarladığında bu durum çakışmalara neden olabilir. Bu genel değişkenleri ComposeView taşıyan özel bir ComposeViewWithLifecycle aşağıda verilmiştir. Tekrar ediyorum, bu durum istikrarlı 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)
//  }
}