Redimensionner les applications Android

1. Introduction

L'écosystème d'appareils Android évolue en permanence. Des anciens mobiles à clavier intégré à la multitude moderne de téléphones à clapet et pliables, de tablettes et autres fenêtres redimensionnables en format libre, les applications Android n'ont jamais fonctionné sur autant d'appareils différents.

C'est une excellente nouvelle pour les développeurs. En revanche, certaines optimisations sont nécessaires pour répondre aux attentes des utilisateurs et leur garantir une expérience optimale sur différentes tailles d'écran. Plutôt que de cibler individuellement chaque nouveau modèle, vous pouvez employer une UI responsive/adaptative reposant une architecture résiliente pour donner à votre application une apparence soignée sur tous les appareils de vos utilisateurs actuels et futurs, quelle que soit leur taille et leur format !

Un excellent moyen de mettre à l'épreuve votre UI responsive/adaptative et de vous assurer qu'elle fonctionnera sur tous les appareils consiste à introduire des environnements Android redimensionnables au format libre. Cet atelier de programmation vous aidera à maîtriser les implications du redimensionnement et à mettre en œuvre les bonnes pratiques pour que votre application se redimensionne de manière fiable et efficace.

Objectifs de l'atelier

Explorer les implications du redimensionnement au format libre et optimiser une application Android afin d'implémenter les bonnes pratiques pour le redimensionnement. Caractéristiques clés de l'application :

Fichier manifeste compatible

  • Lever les restrictions qui empêchent une application d'être redimensionnée librement

Maintien de l'état lors du redimensionnement

  • Maintenir l'état de l'UI lors du redimensionnement en utilisant rememberSaveable
  • Éviter tout duplication inutile des opérations en arrière-plan servant à initialiser l'UI

Ce dont vous avez besoin

  1. Vous disposez des connaissances nécessaires pour créer des applications Android de base
  2. Vous maîtrisez ViewModel et State dans Compose
  3. Vous disposez d'un appareil de test permettant le redimensionnement de fenêtres au format libre, tel que l'un des suivants :

Si vous rencontrez des problèmes (bugs de code, erreurs grammaticales, formulation peu claire, etc.) au cours de cet atelier de programmation, veuillez les signaler via le lien Signaler une erreur situé dans l'angle inférieur gauche de l'atelier de programmation.

2. Premiers pas

Clonez le dépôt depuis GitHub

git clone https://github.com/android/large-screen-codelabs/

…ou téléchargez un fichier ZIP du dépôt et extrayez-le.

Importer un projet

  • Ouvrez Android Studio.
  • Sélectionnez Importer un projet ou Fichier > Nouveau > Importer un projet.
  • Accédez à l'emplacement où vous avez cloné ou extrait le projet.
  • Ouvrez le dossier resizing (redimensionnement).
  • Ouvrez le projet dans le dossier start (démarrage), qui contient le code de démarrage.

Essayer l'application

  • Créez et exécutez l'application.
  • Essayez de redimensionner l'appli.

Qu'en pensez-vous ?

L'expérience dépend du degré de prise en charge sur votre appareil de test, mais vous avez sans doute remarqué qu'elle n'était pas idéale. L'application ne peut pas être redimensionnée et reste bloquée au format initial. Pourquoi ?

Restrictions du fichier manifeste

Si vous examinez le fichier AndroidManifest.xml, vous découvrirez certaines restrictions qui empêchent l'application de se comporter correctement dans un environnement redimensionnable au format libre.

AndroidManifest.xml

            android:maxAspectRatio="1.4"
            android:resizeableActivity="false"
            android:screenOrientation="portrait">

Supprimez ces lignes problématiques de votre fichier manifeste, recompilez l'application et faites un nouvel essai sur votre appareil de test. Vous remarquerez que l'application peut désormais être redimensionnée librement. La suppression de ce type de restrictions est une étape importante pour optimiser votre application pour le redimensionnement de fenêtres au format libre.

3. Changements de configuration pour le redimensionnement

Lorsque la fenêtre de votre application est redimensionnée, sa Configuration est actualisée. Cette actualisation a des implications pour votre application, que vous devez comprendre et anticiper pour offrir une expérience optimale à vos utilisateurs. Le changement de hauteur et de largeur de la fenêtre est le plus évident, mais pas le seul : le format et l'orientation sont également affectés.

Observer les changements de configuration

Pour observer ces changements lorsqu'ils se produisent dans une application basée sur le système de vues Android, vous pouvez remplacer View.onConfigurationChanged. Dans Jetpack Compose, nous avons accès à LocalConfiguration.current, qui est mis à jour automatiquement chaque fois que View.onConfigurationChanged est appelé.

Pour observer ces changements de configuration dans votre application exemple, ajoutez-y un composable qui affiche les valeurs de LocalConfiguration.current ou créez un nouvel exemple de projet avec un composable du même effet. Voici un exemple d'UI permettant d'observer ces changements :

val configuration = LocalConfiguration.current
val isPortrait = configuration.orientation ==
    Configuration.ORIENTATION_PORTRAIT
val screenLayoutSize =
        when (configuration.screenLayout and
                Configuration.SCREENLAYOUT_SIZE_MASK) {
            SCREENLAYOUT_SIZE_SMALL -> "SCREENLAYOUT_SIZE_SMALL"
            SCREENLAYOUT_SIZE_NORMAL -> "SCREENLAYOUT_SIZE_NORMAL"
            SCREENLAYOUT_SIZE_LARGE -> "SCREENLAYOUT_SIZE_LARGE"
            SCREENLAYOUT_SIZE_XLARGE -> "SCREENLAYOUT_SIZE_XLARGE"
            else -> "undefined value"
        }
Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier.fillMaxWidth()
) {
    Text("screenWidthDp: ${configuration.screenWidthDp}")
    Text("screenHeightDp: ${configuration.screenHeightDp}")
    Text("smallestScreenWidthDp: ${configuration.smallestScreenWidthDp}")
    Text("orientation: ${if (isPortrait) "portrait" else "landscape"}")
    Text("screenLayout SIZE: $screenLayoutSize")
}

Vous trouverez un exemple d'implémentation dans le dossier observing-configuration-changes de votre projet. Ajoutez-le à l'UI, exécutez votre application sur l'appareil de test et observez comment l'UI reflète les changements de configuration.

Les informations de configuration sont affichées dans l'interface et actualisées en temps réel lorsque l'application est redimensionnée

Les changements apportés à la configuration de votre application vous permettent de simuler une transition rapide d'un extrême à l'autre, comme le passage d'un petit écran mobile partagé au plein écran sur une tablette ou un ordinateur. C'est une bonne solution pour tester non seulement la mise en page de votre application sur différents écrans, mais aussi sa capacité à gérer ces événements de changement rapide de configuration.

4. Journaliser les événements de cycle de vie d'activité

Le redimensionnement de fenêtres au format libre implique également divers changements affectant les cycles de vie Activity de votre application. Pour observer ces changements en temps réel, ajoutez un observateur de cycle de vie à votre méthode onCreate et journalisez chaque nouvel évènement de cycle de vie en forçant onStateChanged.

lifecycle.addObserver(object : LifecycleEventObserver {
        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        Log.d("resizing-codelab-lifecycle", "$event was called")
    }
})

Une fois la journalisation établie, exécutez à nouveau l'application sur votre appareil de test et examinez Logcat lorsque vous réduisez puis ramenez l'application au premier plan.

Notez que votre application est suspendue lorsqu'elle est réduite, puis relancée lorsqu'elle revient au premier plan. Nous verrons les implications de cet aspect lorsque nous aborderons la continuité, dans la prochaine section de cet atelier de programmation.

L'outil Logcat affichant les méthodes de cycle de vie d'activité invoquées lors du redimensionnement

Jetez un coup d'œil à Logcat pour voir quels rappels de cycle de vie d'activité sont appelés lorsque vous redimensionnez l'application de la plus petite à la plus grande taille possible.

Le comportement observé peut varier selon l'appareil de test utilisé. Vous avez certainement remarqué que votre activité est supprimée puis recréée dès que la taille de votre fenêtre d'application change de manière significative, mais pas lors de modifications plus mineures. Cela s'explique par le fait qu'à partir du niveau d'API 24, seuls les changements de taille importants entraînent la recréation d'Activity.

Vous avez vu certains des changements de configuration courants qui sont susceptibles de survenir dans un environnement de fenêtres au format libre, mais il existe d'autres cas dont vous devez tenir compte. Par exemple, si vous connectez un écran externe à votre appareil de test, vous pouvez observer que votre Activity est supprimée puis recréée afin de refléter des changements de configuration tels que la densité d'affichage.

Pour vous épargner une partie de la complexité associée à ces changements de configuration, vous pouvez utiliser des API de plus haut niveau comme WindowSizeClass afin d'implémenter votre UI adaptative. (Voir Compatibilité avec différentes tailles d'écran.)

5. Continuité : maintenir l'état interne du composable lors du redimensionnement

Dans la section précédente, vous avez vu certains changements de configuration attendus dans un environnement avec redimensionnement de fenêtres au format libre. Dans cette section, vous allez assurer la continuité de l'état de l'UI de votre application lors de ces changements.

Commencez par développer la fonction composable NavigationDrawerHeader (située dans ReplyHomeScreen.kt) afin d'afficher l'adresse e-mail lorsque l'on clique dessus.

@Composable
private fun NavigationDrawerHeader(
    modifier: Modifier = Modifier
) {
    var showDetails by remember { mutableStateOf(false) }
    Column(
        modifier = modifier.clickable {
                showDetails = !showDetails
            }
    ) {


        Row(
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            ReplyLogo(
                modifier = Modifier
                    .size(dimensionResource(R.dimen.reply_logo_size))
            )
            ReplyProfileImage(
                drawableResource = LocalAccountsDataProvider
                    .userAccount.avatar,
                description = stringResource(id = R.string.profile),
                modifier = Modifier
                    .size(dimensionResource(R.dimen.profile_image_size))
            )
        }
        AnimatedVisibility (showDetails) {
            Text(
                text = stringResource(id = LocalAccountsDataProvider
                        .userAccount.email),
                style = MaterialTheme.typography.labelMedium,
                modifier = Modifier
                    .padding(
                        start = dimensionResource(
                            R.dimen.drawer_padding_header),
                        end = dimensionResource(
                            R.dimen.drawer_padding_header),
                        bottom = dimensionResource(
                            R.dimen.drawer_padding_header)
                ),


            )
        }
    }
}

Après avoir ajouté l'en-tête extensible à votre application :

  1. exécutez l'application sur votre appareil de test ;
  2. appuyez sur l'en-tête pour le développer ;
  3. essayez de redimensionner la fenêtre.

Notez que l'en-tête perd son état lorsque sa taille change de façon significative.

L'en-tête du panneau de navigation de l'application se développe lorsque l'on appuie dessus, mais il se réduit lorsque l'application est redimensionnée

L'état de l'UI est perdu parce que remember permet de conserver l'état lors d'une recomposition, mais pas lors de la recréation d'une activité ou d'un processus. Une pratique courante consiste à utiliser le hissage d'état (hoisting) afin de transférer l'état vers l'appelant d'un composable et de rendre les composables sans état, ce qui permet de contourner entièrement le problème. Cela dit, vous pouvez utiliser remember dans certains cas, pour conserver l'état d'un élément d'interface utilisateur à l'intérieur de fonctions composables.

Pour résoudre ces problèmes, remplacez remember par rememberSaveable. Cette approche fonctionne parce que rememberSaveable enregistre et restaure la valeur mémorisée dans savedInstanceState. Transformez remember en rememberSaveable, exécutez votre application sur l'appareil de test et essayez à nouveau de la redimensionner. Vous remarquerez que l'état de l'en-tête extensible est préservé comme prévu lors du redimensionnement.

6. Éviter la duplication inutile des opérations en arrière-plan

Vous avez vu comment utiliser rememberSaveable pour préserver l'état interne de l'UI des composables lors de changements configuration potentiellement fréquents avec le redimensionnement de fenêtres au format libre. Cependant, votre application peut fréquemment devoir hisser l'état et la logique de l'UI hors de composables. L'une des meilleures façons de préserver l'état lors du redimensionnement consiste à en transférer la propriété à un ViewModel. Lorsque vous hissez l'état dans un ViewModel, vous risquez de rencontrer des problèmes avec des tâches de longue durée en arrière-plan, comme l'accès intensif au système de fichier ou les appels réseau nécessaires pour initialiser l'écran.

Pour consulter un exemple des types de problèmes que vous êtes susceptibles de rencontrer, ajoutez une instruction de journalisation à la méthode initializeUIState dans ReplyViewModel.

fun initializeUIState() {
    Log.d("resizing-codelab", "initializeUIState() called in the viewmodel")
    val mailboxes: Map<MailboxType, List<Email>> =
        LocalEmailsDataProvider.allEmails.groupBy { it.mailbox }
    _uiState.value =
        ReplyUiState(
            mailboxes = mailboxes,
            currentSelectedEmail = mailboxes[MailboxType.Inbox]?.get(0)
                ?: LocalEmailsDataProvider.defaultEmail
        )
}

Ensuite, exécutez l'application sur votre appareil de test et redimensionnez sa fenêtre plusieurs fois.

Vous remarquerez dans Logcat que la méthode d'initialisation de votre application s'est exécutée plusieurs fois. Cela peut poser problème pour les tâches que vous ne voulez exécuter qu'une seule fois afin d'initialiser votre UI. Les appels réseau supplémentaires, les entrées/sorties de fichiers et autres tâches peuvent nuire aux performances de l'appareil et provoquer d'autres problèmes indésirables.

Pour éliminer les opérations en arrière-plan inutiles, supprimez l'appel de initializeUIState() dans la méthode onCreate() de votre activité. Au lieu de cela, initialisez les données dans la méthode init du ViewModel. En procédant ainsi, vous vous assurez que la méthode d'initialisation ne s'exécute qu'une seule fois, lorsque ReplyViewModel est instancié pour la première fois :

init {
    initializeUIState()
}

Exécutez à nouveau l'application. Vous constaterez que la tâche inutile simulant l'initialisation n'est exécutée qu'une seule fois, même si vous redimensionnez la fenêtre de l'application à plusieurs reprises. C'est possible parce que les ViewModels survivent au cycle de vie d'Activity. En exécutant le code d'initialisation une seule fois, lors de la création du ViewModel, nous le séparons des éventuelles recréations d'Activity, ce qui évite une charge inutile. Si vous deviez passer par des appels serveur coûteux ou des opérations d'entrées/sorties de fichiers massives pour initialiser votre UI, cela représenterait une économie de ressources considérable et améliorerait l'expérience de vos utilisateurs.

7. FÉLICITATIONS !

Bravo ! Beau travail ! Vous venez de mettre en œuvre certaines des bonnes pratiques pour permettre aux applications Android de se redimensionner correctement dans ChromeOS et dans d'autres environnements multi-écrans et multifenêtres.

Exemple de code source

Clonez le dépôt depuis GitHub

git clone https://github.com/android/large-screen-codelabs/

… ou téléchargez un fichier ZIP du dépôt et extrayez-le.