Plug-ins d'interface utilisateur pour voitures

Utilisez les plug-ins de la bibliothèque Car UI pour créer des implémentations complètes des personnalisations de composants dans la bibliothèque Car UI au lieu d'utiliser des superpositions de ressources d'exécution (RRO). Les RRO vous permettent de modifier uniquement les ressources XML des composants de la bibliothèque Car UI, ce qui limite l'étendue de ce que vous pouvez personnaliser.

Créer un plug-in

Un plug-in de la 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 avoir un fournisseur de contenu déclaré dans son fichier manifeste et possédant les attributs suivants :

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

android:authorities="com.android.car.ui.plugin" permet à la bibliothèque Car UI de découvrir le plug-in. 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 sera 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 auquel d'autres applications font référence 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 des applications.

Implémenter et créer des bibliothèques partagées

Le développement avec des bibliothèques partagées Android est très similaire à celui des applications Android normales, à quelques différences près.

  • Utilisez la balise library sous la balise application 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 Soong android_app (Android.bp) avec le flag AAPT shared-lib, qui est utilisé pour compiler une bibliothèque partagée :
android_app {
  ...
  aaptflags: ["--shared-lib"],
  ...
}

Dépendances des bibliothèques partagées

Pour chaque application du système qui utilise la bibliothèque Car UI, 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 rouverte par l'utilisateur, la page affiche les modifications mises à jour. Si l'application n'était pas en cours d'exécution, le plug-in mis à jour sera disponible au prochain démarrage.

Lorsque vous installez un plug-in avec Android Studio, vous devez tenir compte de certains éléments supplémentaires. Au moment de la rédaction de cet article, un bug dans le processus d'installation d'applications 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 package (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 éliminer l'erreur, définissez l'option Launch (Lancement) sur Nothing (Rien) dans la configuration de compilation.

Configuration du plug-in Android Studio Figure 1. Configuration du plug-in Android Studio

Plug-in de proxy

La personnalisation des applications à l'aide de la bibliothèque Car UI nécessite un RRO ciblant chaque application spécifique à modifier, y compris lorsque les personnalisations sont identiques d'une application à l'autre. Cela signifie qu'un RRO par application est requis. Découvrez les applications qui utilisent la bibliothèque Car UI.

Le plug-in proxy de la bibliothèque Car UI est un exemple de bibliothèque de plug-ins partagée qui délègue ses implémentations de composants à la version statique de la bibliothèque Car UI. Ce plug-in peut être ciblé avec un RRO, qui peut être utilisé comme point unique de personnalisation pour les applications qui utilisent la bibliothèque Car UI sans avoir besoin d'implémenter un 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 une personnalisation allant au-delà des RRO, il est possible d'implémenter un sous-ensemble de composants de plug-in et d'utiliser le plug-in proxy pour le reste, ou d'implémenter tous les composants de plug-in entièrement à partir de zéro.

Bien que le plug-in proxy fournisse un point unique de personnalisation RRO pour les applications, les applications qui choisissent de ne pas utiliser le 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 portant exactement ce nom et ce nom de package. 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 cela, 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 Car UI est publiée avec de nouvelles fonctionnalités, elles font partie de la version V2 du composant. La bibliothèque Car UI fait de son mieux pour que les nouvelles fonctionnalités fonctionnent 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 Car UI 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 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# prise en charge par 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 à liaison statique des composants CarUi est utilisée.

Pour maintenir la rétrocompatibilité avec les applications compilées avec d'anciennes versions de la bibliothèque Car Ui statique, il est recommandé de prendre en charge les maxVersion de 2, 5 et plus dans l'implémentation de la classe PluginVersionProvider de 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 des composants CarUi qui peuvent être utilisées ensemble. Par exemple, il n'y aura jamais de pluginFactory capable de créer la version 100 d'un Toolbar et la version 1 d'un RecyclerView, car il y aurait peu de chances qu'une grande variété de versions de composants fonctionnent ensemble. Pour utiliser la barre d'outils version 100, 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 qui peuvent être créés. Les versions des autres composants peuvent ne pas être égales. 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, pour permettre 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, il suffit de transmettre une vue à installBaseLayoutAround pour que la barre d'outils/mise en page de base s'enroule autour.

Le plug-in doit prendre la vue fournie, la détacher de son parent, gonfler sa propre mise en page au même index que le parent et avec le même LayoutParams que la vue qui vient d'être détachée, puis rattacher la vue quelque part à l'intérieur de la mise en page qui vient d'être gonflée. La mise en page développée contiendra la barre d'outils, si l'application l'a demandée.

L'application peut demander une mise en page de base sans barre d'outils. Si c'est le cas, installBaseLayoutAround doit renvoyer la valeur "null". 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 fluides 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 couvre partiellement le contenu de l'application (avec la barre d'outils ou autrement). L'application saura alors qu'elle doit continuer à dessiner dans cet espace, mais en gardant tous les composants critiques avec lesquels l'utilisateur peut interagir en dehors de celui-ci. Cet effet est utilisé dans notre conception de référence pour rendre la barre d'outils semi-transparente et permettre aux listes de défiler 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.

Défilement du contenu sous la barre d&#39;outils Figure 2. Défilement du contenu sous la barre d'outils

Du point de vue de l'application, lorsque le plug-in envoie de nouvelles encarts, 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 Car Ui gère les encarts par défaut en les appliquant en tant que marge intérieure au Activity ou FragmentActivity contenant le fragment. La bibliothèque n'applique pas les encarts par défaut aux fragments. Voici un exemple d'extrait d'implémentation qui applique les encarts 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 est utilisé pour indiquer si la vue à encapsuler occupe toute l'application ou seulement une petite section. Cela peut être utilisé pour éviter d'appliquer certaines décorations le long du bord qui n'ont de sens que si elles apparaissent le long du bord de l'écran entier. L'application Paramètres est un exemple d'application qui utilise des mises en page de base non plein écran. Dans celle-ci, chaque volet de la mise en page à deux volets possède sa propre barre d'outils.

Étant donné que installBaseLayoutAround est censé renvoyer la valeur "null" lorsque toolbarEnabled est false, le plug-in doit renvoyer false à partir de customizesBaseLayout pour indiquer qu'il ne souhaite pas personnaliser la mise en page de base.

La mise en page de base doit contenir un FocusParkingView et un FocusArea pour prendre entièrement en charge les commandes rotatives. Ces vues peuvent être omises sur les appareils qui ne sont pas compatibles avec la rotation. Les FocusParkingView/FocusAreas sont implémentés dans la bibliothèque CarUi statique. Un setRotaryFactories est donc utilisé pour fournir des fabriques permettant de créer les vues à partir des contextes.

Les contextes utilisés pour créer des vues Focus doivent être le contexte source, et non le contexte du plug-in. Le FocusParkingView doit être le plus proche possible de la première vue dans l'arborescence, car c'est ce qui est sélectionné lorsqu'aucun focus ne doit être visible pour l'utilisateur. Le FocusArea doit envelopper la barre d'outils dans la mise en page de base pour indiquer qu'il s'agit d'une zone de sélection par rotation. Si FocusArea n'est pas fourni, l'utilisateur ne peut pas accéder aux boutons de la barre d'outils avec le sélecteur rotatif.

Contrôleur de barre d'outils

L'ToolbarController réel renvoyé devrait être beaucoup plus simple à implémenter que la mise en page de base. Son rôle est de prendre les informations transmises à ses setters et de les afficher dans la mise en page de base. Pour en savoir plus sur la plupart des méthodes, consultez la documentation Javadoc. Certaines des méthodes les plus complexes sont décrites ci-dessous.

getImeSearchInterface est utilisé pour afficher les résultats de recherche dans la fenêtre IME (clavier). Cela peut être utile pour afficher/animer les résultats de recherche à côté du clavier, par exemple si le clavier n'occupe que la moitié de l'écran. La plupart des fonctionnalités sont implémentées dans la bibliothèque CarUi statique. L'interface de recherche du plug-in fournit simplement 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 au listener fourni en tant que TextView de sa barre de recherche.

setMenuItems affiche simplement les MenuItems à l'écran, mais il sera appelé étonnamment souvent. Étant donné que l'API de plug-in pour les MenuItems est immuable, chaque fois qu'un MenuItem est modifié, un tout nouvel appel setMenuItems se produit. Cela peut se produire pour quelque chose d'aussi banal qu'un utilisateur qui a cliqué sur un MenuItem de bouton bascule, ce qui a entraîné l'activation ou la désactivation du bouton bascule. Pour des raisons de performances et d'animation, il est donc recommandé de calculer la différence entre l'ancienne et la nouvelle liste MenuItems, et de n'actualiser 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 différents appels à setMenuItems pour le même MenuItem.

AppStyledView

AppStyledView est un conteneur pour une vue qui n'est pas personnalisée. Il peut être utilisé pour fournir une bordure autour de cette vue qui la distingue du reste de l'application et indique à l'utilisateur qu'il s'agit d'un autre type d'interface. La vue encapsulée par AppStyledView est indiquée dans setContent. Le AppStyledView peut également comporter un bouton "Retour" ou "Fermer" à la demande de l'application.

Contrairement à installBaseLayoutAround, AppStyledView n'insère pas immédiatement ses vues dans la hiérarchie des vues. Il se contente de renvoyer sa vue à la bibliothèque statique via getView, qui effectue ensuite l'insertion. La position et la taille de AppStyledView peuvent également être contrôlées en implémentant getDialogWindowLayoutParam.

Contextes

Le plug-in doit faire attention lorsqu'il utilise des contextes, car il existe des contextes de plug-in et de "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 développer des mises en page dans le plug-in.

Toutefois, il est possible que le contexte du plug-in ne soit pas correctement configuré. Pour obtenir la configuration correcte, nous fournissons des contextes sources dans les méthodes qui créent des composants. Le contexte source est généralement une activité, mais il peut également s'agir d'un service ou d'un autre composant Android dans certains cas. 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 correcte n'est pas utilisée, une violation du mode strict Android se produira et les vues développées risquent de ne pas avoir les dimensions correctes.

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

Changements de mode

Certains plug-ins peuvent prendre en charge plusieurs modes pour leurs composants, tels qu'un mode sport ou un mode éco qui ont une apparence visuelle distincte. CarUi ne prend pas en charge cette fonctionnalité, mais rien n'empêche le plug-in de l'implémenter entièrement en interne. Le plug-in peut surveiller les conditions de son choix pour déterminer quand changer de mode, par exemple en écoutant les diffusions. Le plug-in ne peut pas déclencher de modification de configuration pour changer de mode, mais il n'est pas recommandé de s'appuyer sur les modifications de configuration de toute façon, 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 les 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 qui ne doit pas être considérée comme stable.

Les plug-ins peuvent utiliser ComposeView pour créer une surface compatible avec Compose dans laquelle effectuer le rendu. Ce ComposeView correspond à ce qui est renvoyé à l'application à partir de la méthode getView dans les composants.

L'un des principaux problèmes liés à l'utilisation de ComposeView est qu'il définit des tags sur la vue racine de la mise en page afin de stocker des variables globales partagées entre différentes ComposeViews de la hiérarchie. Étant donné que les ID de ressources du plug-in ne sont pas séparés de ceux de l'application par un espace de noms, cela peut entraîner des conflits lorsque l'application et le plug-in définissent des tags sur la même vue. Un ComposeViewWithLifecycle personnalisé qui déplace ces variables globales 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)
//  }
}