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ındalibrary
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şaretishared-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.
Ş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.
Ş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)
// }
}