Utilisez les plug-ins de la bibliothèque Car UI pour créer des implémentations complètes de personnalisations de composants dans la bibliothèque Car UI au lieu d'utiliser des superpositions de ressources d'exécution (RRO). Les RRO ne vous permettent de modifier que les ressources XML des composants de la bibliothèque Car UI, ce qui limite l'étendue de la personnalisation.
Créer un plug-in
Un plug-in de bibliothèque Car UI est un APK qui contient des classes implémentant un ensemble d'API de plug-in. Les API de plug-in peuvent être compilées dans un plug-in en tant que bibliothèque statique.
Consultez des exemples dans Soong et Gradle:
Soong
Prenons l'exemple Soong suivant:
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
Consultez ce fichier build.gradle
:
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')
Le plug-in doit comporter un fournisseur de contenu déclaré dans son fichier manifeste avec les attributs suivants:
android:authorities="com.android.car.ui.plugin"
android:enabled="true"
android:exported="true"
android:authorities="com.android.car.ui.plugin"
rend le plug-in détectable par la bibliothèque Car UI. Le fournisseur doit être exporté pour pouvoir être interrogé au moment de l'exécution. De plus, si l'attribut enabled
est défini sur false
, l'implémentation par défaut est utilisée à la place de l'implémentation du plug-in. La classe du fournisseur de contenu n'a pas besoin d'exister. Dans ce cas, veillez à ajouter tools:ignore="MissingClass"
à la définition du fournisseur. Consultez l'exemple d'entrée de fichier manifeste ci-dessous:
<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>
Enfin, par mesure de sécurité, signez votre application.
Plugins en tant que bibliothèque partagée
Contrairement aux bibliothèques statiques Android qui sont compilées directement dans les applications, les bibliothèques partagées Android sont compilées dans un APK autonome référencé par d'autres applications au moment de l'exécution.
Les classes des plug-ins implémentés en tant que bibliothèque partagée Android sont automatiquement ajoutées au chargeur de classe partagé entre les applications. Lorsqu'une application qui utilise la bibliothèque Car UI spécifie une dépendance d'exécution sur la bibliothèque partagée du plug-in, son chargeur de classe peut accéder aux classes de la bibliothèque partagée du plug-in. Les plug-ins implémentés en tant qu'applications Android normales (et non en tant que bibliothèque partagée) peuvent avoir un impact négatif sur les temps de démarrage à froid de l'application.
Implémenter et créer des bibliothèques partagées
Le développement avec des bibliothèques partagées Android est semblable à celui des applications Android normales, à quelques différences près.
- Utilisez la balise
library
sous la baliseapplication
avec le nom du package du plug-in dans le fichier manifeste de l'application de votre plug-in:
<application>
<library android:name="com.chassis.car.ui.plugin" />
...
</application>
- Configurez votre règle de compilation
android_app
Soong (Android.bp
) avec l'indicateur AAPTshared-lib
, qui permet de créer une bibliothèque partagée:
android_app {
...
aaptflags: ["--shared-lib"],
...
}
Dépendances sur les bibliothèques partagées
Pour chaque application du système qui utilise la bibliothèque d'UI pour voitures, incluez la balise uses-library
dans le fichier manifeste de l'application sous la balise application
avec le nom du package du plug-in:
<manifest>
<application
android:name=".MyApp"
...>
<uses-library android:name="com.chassis.car.ui.plugin" android:required="false"/>
...
</application>
</manifest>
Installer un plug-in
Les plug-ins DOIVENT être préinstallés sur la partition système en incluant le module dans PRODUCT_PACKAGES
. Le package préinstallé peut être mis à jour de la même manière que n'importe quelle autre application installée.
Si vous mettez à jour un plug-in existant sur le système, toutes les applications qui l'utilisent se ferment automatiquement. Une fois que l'utilisateur a rouvert la page, les modifications sont appliquées. Si l'application n'était pas en cours d'exécution, le plug-in mis à jour sera disponible la prochaine fois qu'elle sera démarrée.
Lorsque vous installez un plug-in avec Android Studio, vous devez prendre en compte certains éléments supplémentaires. Au moment de la rédaction de cet article, un bug dans le processus d'installation de l'application Android Studio empêche les mises à jour d'un plug-in de prendre effet. Pour résoudre ce problème, sélectionnez l'option Always install with package manager (disables deploy optimizations on Android 11 and later) (Toujours installer avec le gestionnaire de paquets (désactive les optimisations de déploiement sur Android 11 et versions ultérieures)) dans la configuration de compilation du plug-in.
De plus, lors de l'installation du plug-in, Android Studio signale une erreur indiquant qu'il ne trouve pas d'activité principale à lancer. C'est normal, car le plug-in ne comporte aucune activité (à l'exception de l'intent vide utilisé pour résoudre un intent). Pour supprimer l'erreur, définissez l'option Launch (Lancement) sur Nothing (Rien) dans la configuration de la compilation.
Figure 1 : Configuration du plug-in Android Studio
Plug-in Proxy
La personnalisation des applications utilisant la bibliothèque d'UI pour voitures nécessite un RRO qui cible chaque application spécifique à modifier, y compris lorsque les personnalisations sont identiques entre les applications. Cela signifie qu'un RRO par application est requis. Découvrez quelles applications utilisent la bibliothèque d'UI pour voitures.
Le plug-in proxy de la bibliothèque d'interface utilisateur pour voitures est un exemple de bibliothèque partagée de plug-in qui délègue ses implémentations de composants à la version statique de la bibliothèque d'interface utilisateur pour voitures. Ce plug-in peut être ciblé avec un RRO, qui peut être utilisé comme point de personnalisation unique pour les applications qui utilisent la bibliothèque Car UI sans avoir à implémenter de plug-in fonctionnel. Pour en savoir plus sur les RRO, consultez Modifier la valeur des ressources d'une application au moment de l'exécution.
Le plug-in proxy n'est qu'un exemple et un point de départ pour effectuer une personnalisation à l'aide d'un plug-in. Pour la personnalisation au-delà des RRO, vous pouvez implémenter un sous-ensemble de composants de plug-in et utiliser le plug-in proxy pour le reste, ou implémenter tous les composants de plug-in entièrement à partir de zéro.
Bien que le plug-in de proxy fournisse un point de personnalisation unique pour les applications, celles qui désactivent l'utilisation du plug-in auront toujours besoin d'un RRO qui cible directement l'application elle-même.
Implémenter les API du plug-in
Le point d'entrée principal du plug-in est la classe com.android.car.ui.plugin.PluginVersionProviderImpl
. Tous les plug-ins doivent inclure une classe avec ce nom et ce nom de package exacts. Cette classe doit disposer d'un constructeur par défaut et implémenter l'interface PluginVersionProviderOEMV1
.
Les plug-ins CarUi doivent fonctionner avec des applications plus anciennes ou plus récentes que le plug-in. Pour faciliter cette tâche, toutes les API de plug-in sont versionnées avec un V#
à la fin de leur nom de classe. Si une nouvelle version de la bibliothèque d'UI pour voitures est publiée avec de nouvelles fonctionnalités, elles font partie de la version V2
du composant. La bibliothèque Car UI s'efforce de faire fonctionner les nouvelles fonctionnalités dans le champ d'application d'un ancien composant de plug-in.
Par exemple, en convertissant un nouveau type de bouton de la barre d'outils en MenuItems
.
Toutefois, une application avec une ancienne version de la bibliothèque d'UI pour voiture ne peut pas s'adapter à un nouveau plug-in écrit avec des API plus récentes. Pour résoudre ce problème, nous autorisons les plug-ins à renvoyer différentes implémentations d'eux-mêmes en fonction de la version de l'API OEM prise en charge par les applications.
PluginVersionProviderOEMV1
contient une seule méthode:
Object getPluginFactory(int maxVersion, Context context, String packageName);
Cette méthode renvoie un objet qui implémente la version la plus élevée de PluginFactoryOEMV#
compatible avec le plug-in, tout en étant inférieure ou égale à maxVersion
. Si un plug-in ne dispose pas d'une implémentation d'un PluginFactory
aussi ancien, il peut renvoyer null
, auquel cas l'implémentation liée de manière statique des composants CarUi est utilisée.
Pour assurer la rétrocompatibilité avec les applications compilées avec des versions antérieures de la bibliothèque d'interface utilisateur de voiture statique, il est recommandé de prendre en charge les maxVersion
2, 5 et versions ultérieures à partir de l'implémentation de la classe PluginVersionProvider
dans votre plug-in. Les versions 1, 3 et 4 ne sont pas compatibles. Pour en savoir plus, consultez PluginVersionProviderImpl
.
PluginFactory
est l'interface qui crée tous les autres composants CarUi. Il définit également la version de leurs interfaces à utiliser. Si le plug-in ne cherche pas à implémenter l'un de ces composants, il peut renvoyer null
dans sa fonction de création (à l'exception de la barre d'outils, qui dispose d'une fonction customizesBaseLayout()
distincte).
pluginFactory
limite les versions de composants CarUi pouvant être utilisées ensemble. Par exemple, il n'existera jamais de pluginFactory
pouvant créer la version 100 d'un Toolbar
et la version 1 d'un RecyclerView
, car il y aurait peu de garantie qu'une grande variété de versions de composants fonctionnerait ensemble. Pour utiliser la version 100 de la barre d'outils, les développeurs doivent fournir une implémentation d'une version de pluginFactory
qui crée une barre d'outils version 100, ce qui limite ensuite les options sur les versions des autres composants pouvant être créés. Les versions d'autres composants ne sont pas nécessairement identiques. Par exemple, un pluginFactoryOEMV100
peut créer un ToolbarControllerOEMV100
et un RecyclerViewOEMV70
.
Barre d'outils
Mise en page de base
La barre d'outils et la "mise en page de base" sont très étroitement liées. C'est pourquoi la fonction qui crée la barre d'outils s'appelle installBaseLayoutAround
. La mise en page de base est un concept qui permet de positionner la barre d'outils n'importe où autour du contenu de l'application. Vous pouvez ainsi placer une barre d'outils en haut ou en bas de l'application, verticalement sur les côtés, ou même une barre d'outils circulaire englobant l'ensemble de l'application. Pour ce faire, transmettez une vue à installBaseLayoutAround
afin que la barre d'outils/mise en page de base s'enroule.
Le plug-in doit prendre la vue fournie, la détacher de son parent, gonfler sa propre mise en page dans le même indice du parent et avec le même LayoutParams
que la vue qui vient d'être détachée, puis rattacher la vue quelque part dans la mise en page qui vient d'être gonflée. La mise en page gonflée contient la barre d'outils, si l'application le demande.
L'application peut demander une mise en page de base sans barre d'outils. Dans ce cas, installBaseLayoutAround
doit renvoyer la valeur nulle. Pour la plupart des plug-ins, c'est tout ce qui doit se passer, mais si l'auteur du plug-in souhaite appliquer, par exemple, une décoration autour du bord de l'application, cela peut toujours être fait avec une mise en page de base. Ces décorations sont particulièrement utiles pour les appareils dotés d'écrans non rectangulaires, car elles peuvent pousser l'application dans un espace rectangulaire et ajouter des transitions nettes dans l'espace non rectangulaire.
installBaseLayoutAround
reçoit également un Consumer<InsetsOEMV1>
. Ce consommateur peut être utilisé pour indiquer à l'application que le plug-in recouvre partiellement le contenu de l'application (avec la barre d'outils ou autre). L'application saura alors continuer à dessiner dans cet espace, mais elle exclura tous les composants critiques avec lesquels l'utilisateur peut interagir. Cet effet est utilisé dans notre conception de référence pour rendre la barre d'outils semi-transparente et faire défiler les listes en dessous. Si cette fonctionnalité n'était pas implémentée, le premier élément d'une liste serait bloqué sous la barre d'outils et ne serait pas cliquable. Si cet effet n'est pas nécessaire, le plug-in peut ignorer le consommateur.
Figure 2 : Défilement du contenu sous la barre d'outils
Du point de vue de l'application, lorsque le plug-in envoie de nouveaux insets, il les reçoit de toutes les activités ou fragments qui implémentent InsetsChangedListener
. Si une activité ou un fragment n'implémente pas InsetsChangedListener
, la bibliothèque d'interface utilisateur pour voitures gère les encarts par défaut en les appliquant en tant que marge intérieure à Activity
ou FragmentActivity
contenant le fragment. La bibliothèque n'applique pas les marges intérieures par défaut aux fragments. Voici un exemple d'extrait d'implémentation qui applique les marges intérieures en tant que marge intérieure sur un RecyclerView
dans l'application:
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());
}
}
Enfin, le plug-in reçoit un indice fullscreen
, qui permet d'indiquer si la vue à encapsuler occupe l'intégralité de l'application ou seulement une petite section.
Cela permet d'éviter d'appliquer certaines décorations sur le bord qui n'ont de sens que si elles apparaissent sur le bord de l'ensemble de l'écran. L'application Paramètres est un exemple d'application qui utilise des mises en page de base non plein écran, dans lesquelles chaque volet de la mise en page à deux volets possède sa propre barre d'outils.
Étant donné que installBaseLayoutAround
doit renvoyer la valeur nulle lorsque toolbarEnabled
est false
, pour que le plug-in indique qu'il ne souhaite pas personnaliser la mise en page de base, il doit renvoyer false
à partir de customizesBaseLayout
.
La mise en page de base doit contenir un FocusParkingView
et un FocusArea
pour prendre pleinement en charge les commandes rotatives. Ces vues peuvent être omises sur les appareils qui ne sont pas compatibles avec les commandes rotatives. Les FocusParkingView/FocusAreas
sont implémentés dans la bibliothèque statique CarUi. Par conséquent, un setRotaryFactories
est utilisé pour fournir des usines permettant de créer des vues à partir de contextes.
Les contextes utilisés pour créer des vues de concentration doivent être le contexte source, et non le contexte du plug-in. Le FocusParkingView
doit être aussi proche que possible de la première vue de l'arborescence, car c'est ce qui est mis en surbrillance lorsqu'aucun focus ne doit être visible par l'utilisateur. FocusArea
doit encapsuler la barre d'outils dans la mise en page de base pour indiquer qu'il s'agit d'une zone de pression rotative. Si FocusArea
n'est pas fourni, l'utilisateur ne peut pas accéder à aucun bouton de la barre d'outils avec le contrôleur rotatif.
Contrôleur de la barre d'outils
L'ToolbarController
renvoyée doit être beaucoup plus simple à implémenter que la mise en page de base. Son rôle est de récupérer les informations transmises à ses sétteurs et de les afficher dans la mise en page de base. Consultez la documentation Javadoc pour en savoir plus sur la plupart des méthodes. Certaines des méthodes les plus complexes sont décrites ci-dessous.
getImeSearchInterface
permet d'afficher les résultats de recherche dans la fenêtre de l'IME (clavier). Cela peut être utile pour afficher/animer les résultats de recherche à côté du clavier, par exemple si le clavier ne prend que la moitié de l'écran. La plupart des fonctionnalités sont implémentées dans la bibliothèque statique CarUi. L'interface de recherche du plug-in ne fournit que des méthodes permettant à la bibliothèque statique d'obtenir les rappels TextView
et onPrivateIMECommand
. Pour ce faire, le plug-in doit utiliser une sous-classe TextView
qui remplace onPrivateIMECommand
et transmet l'appel à l'écouteur fourni en tant que TextView
de sa barre de recherche.
setMenuItems
affiche simplement des éléments de menu à l'écran, mais il sera appelé de façon surprenante souvent. Étant donné que l'API du plug-in pour les MenuItems est immuable, chaque fois qu'un MenuItem est modifié, un nouvel appel setMenuItems
est effectué. Cela peut se produire pour quelque chose d'aussi simple qu'un utilisateur qui a cliqué sur un bouton bascule MenuItem, et ce clic a activé le bouton. Pour des raisons de performances et d'animation, nous vous recommandons donc de calculer la différence entre l'ancienne et la nouvelle liste MenuItems, et de ne mettre à jour que les vues qui ont réellement changé. Les MenuItems fournissent un champ key
qui peut vous aider, car la clé doit être la même pour les différents appels de setMenuItems
pour le même MenuItem.
AppStyledView
AppStyledView
est un conteneur pour une vue qui n'est pas du tout personnalisée. Il peut être utilisé pour ajouter une bordure autour de cette vue afin de la faire ressortir du reste de l'application et d'indiquer à l'utilisateur qu'il s'agit d'un type d'interface différent. La vue encapsulée par AppStyledView est indiquée dans setContent
. AppStyledView
peut également comporter un bouton "Retour" ou "Fermer", comme demandé par l'application.
AppStyledView
n'insère pas immédiatement ses vues dans la hiérarchie des vues comme installBaseLayoutAround
, mais renvoie simplement sa vue à la bibliothèque statique via getView
, qui effectue ensuite l'insertion. La position et la taille de l'AppStyledView
peuvent également être contrôlées en implémentant getDialogWindowLayoutParam
.
Contextes
Le plug-in doit faire preuve de prudence lors de l'utilisation des contextes, car il existe à la fois des contextes plug-in et des contextes "source". Le contexte du plug-in est fourni en tant qu'argument à getPluginFactory
et est le seul contexte contenant les ressources du plug-in. Cela signifie qu'il s'agit du seul contexte pouvant être utilisé pour gonfler les mises en page dans le plug-in.
Toutefois, il est possible que la configuration du contexte du plug-in ne soit pas correcte. Pour obtenir la configuration appropriée, nous fournissons des contextes sources dans les méthodes qui créent des composants. Le contexte source est généralement une activité, mais dans certains cas, il peut également s'agir d'un service ou d'un autre composant Android. Pour utiliser la configuration du contexte source avec les ressources du contexte du plug-in, un nouveau contexte doit être créé à l'aide de createConfigurationContext
. Si la configuration appropriée n'est pas utilisée, une violation du mode strict Android se produira et les vues gonflées risquent de ne pas avoir les dimensions appropriées.
Context layoutInflationContext = pluginContext.createConfigurationContext(
sourceContext.getResources().getConfiguration());
Changements de mode
Certains plug-ins peuvent prendre en charge plusieurs modes pour leurs composants, comme un mode sport ou un mode éco qui se distinguent visuellement. Cette fonctionnalité n'est pas prise en charge en interne dans CarUi, mais rien n'empêche le plug-in de l'implémenter entièrement en interne. Le plug-in peut surveiller toutes les conditions qu'il souhaite déterminer pour changer de mode, par exemple pour écouter les diffusions. Le plug-in ne peut pas déclencher de modification de configuration pour changer de mode. Toutefois, il est déconseillé de s'appuyer sur des modifications de configuration, car la mise à jour manuelle de l'apparence de chaque composant est plus fluide pour l'utilisateur et permet également des transitions qui ne sont pas possibles avec des modifications de configuration.
Jetpack Compose
Les plug-ins peuvent être implémentés à l'aide de Jetpack Compose, mais il s'agit d'une fonctionnalité de niveau alpha et ne doit pas être considérée comme stable.
Les plug-ins peuvent utiliser ComposeView
pour créer une surface compatible avec Compose à afficher. Ce ComposeView
serait ce qui est renvoyé à l'application à partir de la méthode getView
dans les composants.
L'utilisation de ComposeView
pose un problème majeur, car elle définit des balises sur la vue racine de la mise en page afin de stocker des variables globales partagées entre différentes ComposeViews dans la hiérarchie. Étant donné que les ID de ressources du plug-in ne sont pas définis dans un espace de noms distinct de celui de l'application, cela peut entraîner des conflits lorsque l'application et le plug-in définissent des balises sur la même vue. Un ComposeViewWithLifecycle
personnalisé qui déplace ces variables globales vers le bas vers le ComposeView
est fourni ci-dessous. Encore une fois, cette version ne doit pas être considérée comme stable.
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)
// }
}