Ç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ındakilibrary
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.
Ş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.
Ş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)
// }
}