Créer une application avec une mise en page adaptative

1. Introduction

Dans l'atelier de programmation précédent, vous avez commencé à transformer l'application Reply pour la rendre adaptative en utilisant des classes de taille de fenêtre et en implémentant la navigation dynamique. Ces fonctionnalités sont essentielles et constituent la première étape dans le processus de création d'applications pour toutes les tailles d'écran. Si vous n'avez pas suivi l'atelier de programmation Créer une application adaptative avec la navigation dynamique, nous vous recommandons vivement de le faire.

Dans cet atelier de programmation, vous allez vous appuyer sur un concept que vous avez étudié pour implémenter une mise en page adaptative dans votre application. La mise en page que vous allez implémenter fait partie des mises en page standards, c'est-à-dire un ensemble de formats couramment utilisés pour les grands écrans. Vous découvrirez également d'autres outils et techniques de test qui vous aideront à créer rapidement des applications robustes.

Conditions préalables

  • Vous avez terminé l'atelier de programmation Créer une application adaptative avec la navigation dynamique.
  • Vous maîtrisez la programmation Kotlin, y compris les classes, les fonctions et les conditions.
  • Vous maîtrisez les classes ViewModel.
  • Vous maîtrisez les fonctions Composable.
  • Vous savez comment créer des mises en page avec Jetpack Compose.
  • Vous savez comment exécuter des applications sur un appareil ou un émulateur.
  • Vous savez comment utiliser l'API WindowSizeClass.

Points abordés

  • Comment créer une mise en page adaptative de modèle sous forme de liste avec Jetpack Compose
  • Comment créer des aperçus pour différentes tailles d'écran
  • Comment tester le code pour plusieurs tailles d'écran

Objectifs de l'atelier

  • Vous allez continuer à mettre à jour l'application Reply pour qu'elle s'adapte à toutes les tailles d'écran.

La version finale de l'application ressemblera à ceci :

Ce dont vous avez besoin

  • Un ordinateur avec accès à Internet, un navigateur Web et Android Studio
  • Un accès à GitHub

Télécharger le code de démarrage

Pour commencer, téléchargez le code de démarrage :

Vous pouvez également cloner le dépôt GitHub du code :

$ git clone
https://github.com/google-developer-training/basic-android-kotlin-compose-training-reply-app.git
$ cd basic-android-kotlin-compose-training-reply-app
$ git checkout nav-update

Vous pouvez parcourir le code dans le dépôt GitHub Reply.

2. Aperçus pour différentes tailles d'écran

Créer des aperçus pour différentes tailles d'écran

Dans l'atelier de programmation Créer une application adaptative avec la navigation dynamique, vous avez appris à utiliser des composables d'aperçu pour faciliter le processus de développement. Dans le cas d'une application adaptative, il est recommandé de créer plusieurs aperçus pour afficher l'application sur différentes tailles d'écran. Cela vous permet de voir vos modifications sur toutes les tailles d'écran en même temps. Les aperçus servent également de documentation pour que les autres développeurs qui examinent votre code puissent vérifier la compatibilité de votre application avec différentes tailles d'écran.

Auparavant, il n'y avait qu'un seul aperçu compatible avec l'écran de format compact. Vous en ajouterez d'autres par la suite.

Pour ajouter des aperçus pour les écrans de taille moyenne et étendus, procédez comme suit :

  1. Ajoutez un aperçu pour les écrans de taille moyenne en définissant une valeur widthDp moyenne dans le paramètre d'annotation Preview et en spécifiant la valeur WindowWidthSizeClass.Medium en tant que paramètre pour le composable ReplyApp.

MainActivity.kt

...
@Preview(showBackground = true, widthDp = 700)
@Composable
fun ReplyAppMediumPreview() {
    ReplyTheme {
        Surface {
            ReplyApp(windowSize = WindowWidthSizeClass.Medium)
        }
    }
}
...
  1. Ajoutez un autre aperçu pour les écrans étendus en définissant une valeur widthDp élevée dans le paramètre d'annotation Preview et en spécifiant la valeur WindowWidthSizeClass.Expanded en tant que paramètre pour le composable ReplyApp.

MainActivity.kt

...
@Preview(showBackground = true, widthDp = 1000)
@Composable
fun ReplyAppExpandedPreview() {
    ReplyTheme {
        Surface {
            ReplyApp(windowSize = WindowWidthSizeClass.Expanded)
        }
    }
}
...
  1. Générez l'aperçu pour afficher ce qui suit :

a60105aedfbd8172.png

2e9e6b444a676f3a.png

3. Implémenter la mise en page de contenu adaptative

Présentation de la vue détaillée

Vous remarquerez peut-être que sur les écrans étendus, le contenu semble étiré et qu'il n'exploite pas correctement l'espace disponible à l'écran.

46cbd7374bfcc4a9.png

Vous pouvez améliorer cette mise en page en appliquant l'une des mises en page standards. Il s'agit de compositions d'écran de grande taille qui servent de points de départ pour la conception et l'implémentation. Trois mises en page sont disponibles pour vous aider à organiser les éléments courants d'une application, d'une vue de liste, d'un panneau d'assistance et d'un flux. Chaque mise en page prend en compte les cas d'utilisation et les composants courants pour répondre aux attentes et aux besoins des utilisateurs concernant la façon dont l'application s'adapte aux tailles d'écran et aux points d'arrêt.

Pour l'application Reply, nous allons implémenter la vue détaillée, car elle est idéale pour parcourir des contenus et afficher rapidement des détails. Dans la mise en page de vue détaillée, vous allez créer un autre volet à côté de l'écran de la liste de diffusion pour afficher les détails des e-mails. Cette mise en page vous permet d'utiliser l'écran disponible pour présenter davantage d'informations aux utilisateurs et améliorer la productivité de votre application.

Implémenter la vue détaillée

Pour implémenter une vue détaillée pour les écrans étendus, procédez comme suit :

  1. Pour représenter différents types de mise en page de contenu, créez une classe Enum pour différents types de contenu sur WindowStateUtils.kt. Utilisez la valeur LIST_AND_DETAIL lorsque l'écran étendu est utilisé et LIST_ONLY dans le cas contraire.

WindowStateUtils.kt

...
enum class ReplyContentType {
    LIST_ONLY, LIST_AND_DETAIL
}
...
  1. Déclarez la variable contentType sur ReplyApp.kt et affectez la variable contentType appropriée pour différentes tailles de fenêtre afin de déterminer la sélection du type de contenu approprié en fonction de la taille de l'écran.

ReplyApp.kt

...
import com.example.reply.ui.utils.ReplyContentType
...

    val navigationType: ReplyNavigationType
    val contentType: ReplyContentType

    when (windowSize) {
        WindowWidthSizeClass.Compact -> {
            ...
            contentType = ReplyContentType.LIST_ONLY
        }
        WindowWidthSizeClass.Medium -> {
            ...
            contentType = ReplyContentType.LIST_ONLY
        }
        WindowWidthSizeClass.Expanded -> {
            ...
            contentType = ReplyContentType.LIST_AND_DETAIL
        }
        else -> {
            ...
            contentType = ReplyContentType.LIST_ONLY
        }
    }
...

Vous pouvez ensuite utiliser la valeur contentType pour créer un embranchement différent pour les mises en page dans le composable ReplyAppContent.

  1. Dans ReplyHomeScreen.kt, ajoutez contentType en tant que paramètre au composable ReplyHomeScreen.

ReplyHomeScreen.kt

...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReplyHomeScreen(
    navigationType: ReplyNavigationType,
    contentType: ReplyContentType,
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit,
    onEmailCardPressed: (Email) -> Unit,
    onDetailScreenBackPressed: () -> Unit,
    modifier: Modifier = Modifier
) {
...
  1. Transmettez la valeur contentType au composable ReplyHomeScreen.

ReplyApp.kt

...
    ReplyHomeScreen(
        navigationType = navigationType,
        contentType = contentType,
        replyUiState = replyUiState,
        onTabPressed = { mailboxType: MailboxType ->
            viewModel.updateCurrentMailbox(mailboxType = mailboxType)
            viewModel.resetHomeScreenStates()
        },
        onEmailCardPressed = { email: Email ->
            viewModel.updateDetailsScreenStates(
                email = email
            )
        },
        onDetailScreenBackPressed = {
            viewModel.resetHomeScreenStates()
        },
        modifier = modifier
    )

...
  1. Ajoutez contentType en tant que paramètre pour le composable ReplyAppContent.

ReplyHomeScreen.kt

...
@Composable
private fun ReplyAppContent(
    navigationType: ReplyNavigationType,
    contentType: ReplyContentType,
    replyUiState: ReplyUiState,
    onTabPressed: ((MailboxType) -> Unit),
    onEmailCardPressed: (Email) -> Unit,
    navigationItemContentList: List<NavigationItemContent>,
    modifier: Modifier = Modifier
) {
...
  1. Transmettez la valeur contentType aux deux composables ReplyAppContent.

ReplyHomeScreen.kt

...
            ReplyAppContent(
                navigationType = navigationType,
                contentType = contentType,
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
        }
    } else {
        if (replyUiState.isShowingHomepage) {
            ReplyAppContent(
                navigationType = navigationType,
                contentType = contentType,
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
        } else {
            ReplyDetailsScreen(
                replyUiState = replyUiState,
                isFullScreen = true,
                onBackButtonClicked = onDetailScreenBackPressed,
                modifier = modifier
            )
        }
    }
...

Nous allons afficher soit la liste complète et l'écran détaillé lorsque contentType est LIST_AND_DETAIL, soit le contenu de l'e-mail sous forme de liste uniquement lorsque contentType est LIST_ONLY.

  1. Dans ReplyHomeScreen.kt, ajoutez une instruction if/else sur le composable ReplyAppContent pour afficher le composable ReplyListAndDetailContent lorsque la valeur contentType est LIST_AND_DETAIL et pour afficher le composable ReplyListOnlyContent sur la branche else.

ReplyHomeScreen.kt

...
        Column(
            modifier = modifier
                .fillMaxSize()
                .background(MaterialTheme.colorScheme.inverseOnSurface)
        ) {
            if (contentType == ReplyContentType.LIST_AND_DETAIL) {
                ReplyListAndDetailContent(
                    replyUiState = replyUiState,
                    onEmailCardPressed = onEmailCardPressed,
                    modifier = Modifier.weight(1f)
                )
            } else {
                ReplyListOnlyContent(
                    replyUiState = replyUiState,
                    onEmailCardPressed = onEmailCardPressed,
                    modifier = Modifier.weight(1f)
                        .padding(
                            horizontal = dimensionResource(R.dimen.email_list_only_horizontal_padding)
                        )
                )
            }
            AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
                ReplyBottomNavigationBar(
                    currentTab = replyUiState.currentMailbox,
                    onTabPressed = onTabPressed,
                    navigationItemContentList = navigationItemContentList
                )
            }
        }
...
  1. Supprimez la condition replyUiState.isShowingHomepage pour afficher un panneau de navigation permanent, car l'utilisateur n'a pas besoin d'accéder à la vue détaillée s'il utilise la vue étendue.

ReplyHomeScreen.kt

...
    if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER) {
        PermanentNavigationDrawer(
            drawerContent = {
                PermanentDrawerSheet(Modifier.width(dimensionResource(R.dimen.drawer_width))) {
                    NavigationDrawerContent(
                        selectedDestination = replyUiState.currentMailbox,
                        onTabPressed = onTabPressed,
                        navigationItemContentList = navigationItemContentList,
                        modifier = Modifier
                            .wrapContentWidth()
                            .fillMaxHeight()
                            .background(MaterialTheme.colorScheme.inverseOnSurface)
                            .padding(dimensionResource(R.dimen.drawer_padding_content))
                    )
                }
            }
        ) {

...
  1. Exécutez votre application en mode Tablette pour afficher l'écran ci-dessous :

3a545c9f30ccae08.png

Améliorer les éléments d'interface utilisateur pour la vue détaillée

Votre application affiche actuellement un volet de détails sur l'écran d'accueil pour les écrans étendus.

6292c7a61485e112.png

Cependant, l'écran contient des éléments superflus, tels que le bouton "Retour", l'en-tête et des marges intérieures supplémentaires, car il a été conçu pour un écran de détails autonome. Vous pourrez améliorer cela par la suite en effectuant un simple ajustement.

Pour améliorer l'écran de détails de la vue étendue, procédez comme suit :

  1. Dans ReplyDetailsScreen.kt, ajoutez une variable isFullScreen en tant que paramètre Boolean au composable ReplyDetailsScreen.

Cela vous permet de différencier le composable lorsqu'il est utilisé de manière autonome et lorsqu'il est utilisé dans l'écran d'accueil.

ReplyDetailsScreen.kt

...
@Composable
fun ReplyDetailsScreen(
    replyUiState: ReplyUiState,
    onBackPressed: () -> Unit,
    modifier: Modifier = Modifier,
    isFullScreen: Boolean = false
) {
...
  1. Dans le composable ReplyDetailsScreen, encapsulez le composable ReplyDetailsScreenTopBar avec une instruction if pour qu'il ne s'affiche que lorsque l'application est en mode plein écran.

ReplyDetailsScreen.kt

...
    LazyColumn(
        modifier = modifier
            .fillMaxSize()
            .background(color = MaterialTheme.colorScheme.inverseOnSurface)
            .padding(top = dimensionResource(R.dimen.detail_card_list_padding_top))
    ) {
        item {
            if (isFullScreen) {
                ReplyDetailsScreenTopBar(
                    onBackPressed,
                    replyUiState,
                    Modifier
                        .fillMaxWidth()
                        .padding(bottom = dimensionResource(R.dimen.detail_topbar_padding_bottom))
                    )
                )
            }

...

Vous pouvez à présent ajouter une marge intérieure. La marge intérieure requise pour le composable ReplyEmailDetailsCard varie selon que vous l'utilisez ou non en plein écran. Lorsque vous utilisez ReplyEmailDetailsCard avec d'autres composables sur l'écran étendu, une marge intérieure supplémentaire provenant d'autres composables est ajoutée.

  1. Transmettez la valeur isFullScreen au composable ReplyEmailDetailsCard. Transmettez un modificateur avec une marge intérieure horizontale de R.dimen.detail_card_outer_padding_horizontal en cas d'affichage en plein écran. Dans le cas contraire, transmettez un modificateur avec une marge intérieure de fin de R.dimen.detail_card_outer_padding_horizontal.

ReplyDetailsScreen.kt

...
        item {
            if (isFullScreen) {
                ReplyDetailsScreenTopBar(
                    onBackPressed,
                    replyUiState,
                    Modifier
                        .fillMaxWidth()
                        .padding(bottom = dimensionResource(R.dimen.detail_topbar_padding_bottom))
                    )
                )
            }
            ReplyEmailDetailsCard(
                email = replyUiState.currentSelectedEmail,
                mailboxType = replyUiState.currentMailbox,
                isFullScreen = isFullScreen,
                modifier = if (isFullScreen) {
                    Modifier.padding(horizontal = dimensionResource(R.dimen.detail_card_outer_padding_horizontal))
                } else {
                    Modifier.padding(end = dimensionResource(R.dimen.detail_card_outer_padding_horizontal))
                }
            )
        }
...
  1. Ajoutez une valeur isFullScreen en tant que paramètre au composable ReplyEmailDetailsCard.

ReplyDetailsScreen.kt

...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ReplyEmailDetailsCard(
    email: Email,
    mailboxType: MailboxType,
    modifier: Modifier = Modifier,
    isFullScreen: Boolean = false
) {
...
  1. Dans le composable ReplyEmailDetailsCard, n'affichez le texte d'objet de l'e-mail que lorsque l'application n'est pas en mode plein écran, car la mise en page en plein écran affiche déjà l'objet de l'e-mail comme en-tête. Si elle est affichée en plein écran, ajoutez un espace vide d'une hauteur de R.dimen.detail_content_padding_top.

ReplyDetailsScreen.kt

...
Column(
    modifier = Modifier
        .fillMaxWidth()
        .padding(dimensionResource(R.dimen.detail_card_inner_padding))
) {
    DetailsScreenHeader(
        email,
        Modifier.fillMaxWidth()
    )
    if (isFullScreen) {
        Spacer(modifier = Modifier.height(dimensionResource(R.dimen.detail_content_padding_top)))
    } else {
        Text(
            text = stringResource(email.subject),
            style = MaterialTheme.typography.bodyMedium,
            color = MaterialTheme.colorScheme.outline,
            modifier = Modifier.padding(
                top = dimensionResource(R.dimen.detail_content_padding_top),
                bottom = dimensionResource(R.dimen.detail_expanded_subject_body_spacing)
            ),
        )
    }
    Text(
        text = stringResource(email.body),
        style = MaterialTheme.typography.bodyLarge,
        color = MaterialTheme.colorScheme.onSurfaceVariant,
    )
    DetailsScreenButtonBar(mailboxType, displayToast)
}

...
  1. Dans ReplyHomeScreen.kt, à l'intérieur du composable ReplyHomeScreen, transmettez une valeur true pour le paramètre isFullScreen lorsque vous créez le composable ReplyDetailsScreen en tant qu'élément autonome.

ReplyHomeScreen.kt

...
        } else {
            ReplyDetailsScreen(
                replyUiState = replyUiState,
                isFullScreen = true,
                onBackPressed = onDetailScreenBackPressed,
                modifier = modifier
            )
        }
...
  1. Exécutez l'application en mode Tablette pour afficher la mise en page suivante :

a3c92a6b810cb9d1.png

Ajuster le comportement de retour pour la vue détaillée

Avec les écrans étendus, il n'est pas nécessaire d'accéder au composable ReplyDetailsScreen. Vous souhaitez plutôt que l'application se ferme lorsque l'utilisateur sélectionne le bouton "Retour". Il convient donc d'ajuster le gestionnaire de retour.

Modifiez le gestionnaire de retour en transmettant la fonction activity.finish() en tant que paramètre onBackPressed du composable ReplyDetailsScreen à l'intérieur du composable ReplyListAndDetailContent.

ReplyHomeContent.kt

...
import android.app.Activity
import androidx.compose.ui.platform.LocalContext
...
        val activity = LocalContext.current as Activity
        ReplyDetailsScreen(
            replyUiState = replyUiState,
            modifier = Modifier.weight(1f),
            onBackPressed = { activity.finish() }
        )
...

4. Vérifier la compatibilité avec différentes tailles d'écran

Consignes relatives à la qualité des applications sur grand écran

Pour garantir une expérience optimale et cohérente aux utilisateurs d'Android, vous devez créer et tester vos applications en accordant une attention toute particulière à la qualité. Consultez les Consignes fondamentales relatives à la qualité des applications pour savoir comment améliorer la qualité de votre application.

Pour créer une application de qualité pour tous les facteurs de forme, consultez les Consignes relatives à la qualité des applications sur grand écran. Votre application doit également répondre aux exigences de niveau 3 : applications compatibles avec un grand écran.

Évaluer manuellement l'aptitude de votre application à s'afficher sur un grand écran

Vous trouverez, dans les consignes relatives à la qualité des applications, les recommandations et procédures de test nécessaires pour vérifier la qualité de votre application. Penchons-nous sur un exemple de test correspondant à l'application Reply.

Description de la qualité des applications sur grand écran sur le plan de la configuration et de la continuité.

Conformément aux consignes relatives à la qualité des applications ci-dessus, l'application doit conserver ou restaurer son état après un changement de configuration. Des instructions sont également disponibles pour tester les applications, comme illustré ci-dessous :

Étapes du test de qualité des applications sur grand écran (configuration et continuité).

Pour tester manuellement la continuité et la configuration de l'application Reply, procédez comme suit :

  1. Exécutez l'application Reply sur un appareil de taille moyenne ou, si vous utilisez l'émulateur redimensionnable, en mode pliable déplié.
  2. Assurez-vous que la rotation automatique est activée sur l'émulateur.

9f10bf0c49070e8d.png

  1. Faites défiler la liste de diffusion vers le bas.

9c069c37c5591316.png

  1. Cliquez sur une fiche d'e-mail. Par exemple, ouvrez l'e-mail envoyé par Stef.

40ded263c2342432.png

  1. Faites pivoter l'appareil pour vérifier que l'e-mail choisi correspond toujours à celui sélectionné en mode Portrait. Dans cet exemple, l'e-mail envoyé par Stef est toujours affiché.

98d9e0b7ff3c4081.png

  1. Faites à nouveau pivoter l'appareil en mode Portrait pour vérifier que l'application affiche toujours le même e-mail.

40ded263c2342432.png

5. Ajouter un test automatisé pour les applications adaptatives

Configurer le test pour la taille d'écran compacte

Dans l'atelier de programmation Tester l'application Cupcake, vous avez appris à créer des tests d'interface utilisateur. Voyons maintenant comment créer des tests spécifiques pour différentes tailles d'écran.

Dans l'application Reply, vous utilisez des éléments de navigation différents en fonction des tailles d'écran. Par exemple, lorsque l'utilisateur affiche l'écran étendu, vous vous attendez à voir un panneau de navigation permanent. Il est utile de créer des tests pour vérifier l'existence de divers éléments de navigation, comme la barre de navigation inférieure, le rail de navigation et le panneau de navigation pour différentes tailles d'écran.

Pour créer un test dans le but de vérifier l'existence d'un élément de navigation inférieure dans un écran de format compact, procédez comme suit :

  1. Dans le répertoire de test, créez une classe Kotlin nommée ReplyAppTest.kt.
  2. Dans la classe ReplyAppTest, créez une règle de test en utilisant createAndroidComposeRule et en transmettant ComponentActivity comme paramètre de type. ComponentActivity est utilisé pour accéder à une activité vide au lieu de MainActivity.

ReplyAppTest.kt

...
class ReplyAppTest {

    @get:Rule
    val composeTestRule = createAndroidComposeRule<ComponentActivity>()
...

Pour différencier les éléments de navigation dans les écrans, ajoutez testTag dans le composable ReplyBottomNavigationBar.

  1. Définissez une ressource de chaîne pour Navigation Bottom (Navigation inférieure).

strings.xml

...
<resources>
...
    <string name="navigation_bottom">Navigation Bottom</string>
...
</resources>
  1. Ajoutez le nom de chaîne en tant qu'argument testTag pour la méthode testTag du Modifier dans le composable ReplyBottomNavigationBar.

ReplyHomeScreen.kt

...
val bottomNavigationContentDescription = stringResource(R.string.navigation_bottom)
ReplyBottomNavigationBar(
    ...
    modifier = Modifier
        .fillMaxWidth
        .testTag(bottomNavigationContentDescription)
)
...
  1. Dans la classe ReplyAppTest, créez une fonction de test pour tester un écran de format compact. Définissez le contenu de composeTestRule avec le composable ReplyApp, puis transmettez WindowWidthSizeClass.Compact en tant qu'argument windowSize.

ReplyAppTest.kt

...
    @Test
    fun compactDevice_verifyUsingBottomNavigation() {
        // Set up compact window
        composeTestRule.setContent {
            ReplyApp(
                windowSize = WindowWidthSizeClass.Compact
            )
        }
    }
  1. Confirmez l'existence de l'élément de navigation inférieure avec la balise de test. Appelez la fonction d'extension onNodeWithTagForStringId sur composeTestRule, transmettez la chaîne de navigation inférieure et appelez la méthode assertExists().

ReplyAppTest.kt

...
    @Test
    fun compactDevice_verifyUsingBottomNavigation() {
        // Set up compact window
        composeTestRule.setContent {
            ReplyApp(
                windowSize = WindowWidthSizeClass.Compact
            )
        }
        // Bottom navigation is displayed
        composeTestRule.onNodeWithTagForStringId(
            R.string.navigation_bottom
        ).assertExists()
    }
  1. Exécutez le test et assurez-vous qu'il réussit.

Configurer le test pour les écrans de taille moyenne et étendus

Maintenant que vous avez créé un test pour l'écran de format compact, nous allons créer les tests correspondants pour les écrans de taille moyenne et étendus.

Pour créer des tests dans le but de vérifier l'existence d'un rail de navigation et d'un panneau de navigation permanent pour les écrans de taille moyenne et étendus, procédez comme suit :

  1. Définissez une ressource de chaîne pour le rail de navigation qui sera utilisée, par la suite, comme balise de test.

strings.xml

...
<resources>
...
    <string name="navigation_rail">Navigation Rail</string>
...
</resources>
  1. Transmettez la chaîne en tant que balise de test via Modifier dans le composable PermanentNavigationDrawer.

ReplyHomeScreen.kt

...
    val navigationDrawerContentDescription = stringResource(R.string.navigation_drawer)
        PermanentNavigationDrawer(
...
modifier = Modifier.testTag(navigationDrawerContentDescription)
)
...
  1. Transmettez la chaîne en tant que balise de test via Modifier dans le composable ReplyNavigationRail.

ReplyHomeScreen.kt

...
val navigationRailContentDescription = stringResource(R.string.navigation_rail)
ReplyNavigationRail(
    ...
    modifier = Modifier
        .testTag(navigationRailContentDescription)
)
...
  1. Ajoutez un test pour vérifier qu'il existe un élément de rail de navigation dans les écrans de taille moyenne.

ReplyAppTest.kt

...
@Test
fun mediumDevice_verifyUsingNavigationRail() {
    // Set up medium window
    composeTestRule.setContent {
        ReplyApp(
            windowSize = WindowWidthSizeClass.Medium
        )
    }
    // Navigation rail is displayed
    composeTestRule.onNodeWithTagForStringId(
        R.string.navigation_rail
    ).assertExists()
}
  1. Ajoutez un test pour vérifier qu'il existe un élément de panneau de navigation dans les écrans étendus.

ReplyAppTest.kt

...
@Test
fun expandedDevice_verifyUsingNavigationDrawer() {
    // Set up expanded window
    composeTestRule.setContent {
        ReplyApp(
            windowSize = WindowWidthSizeClass.Expanded
        )
    }
    // Navigation drawer is displayed
    composeTestRule.onNodeWithTagForStringId(
        R.string.navigation_drawer
    ).assertExists()
}
  1. Utilisez un émulateur de tablette ou un émulateur redimensionnable en mode Tablette pour exécuter le test.
  2. Exécutez tous les tests et vérifiez qu'ils réussissent.

Tester un changement de configuration sur un écran de format compact

Un changement de configuration est un événement courant dans le cycle de vie d'une application. Cela se produit, par exemple, lorsque vous passez du mode Portrait au mode Paysage. Dans ce cas, il est important de vérifier que votre application conserve son état. Vous allez ensuite créer des tests qui simulent un changement de configuration afin de vérifier que votre application conserve son état sur un écran de format compact.

Pour tester un changement de configuration sur l'écran de format compact :

  1. Dans le répertoire de test, créez une classe Kotlin nommée ReplyAppStateRestorationTest.kt.
  2. Dans la classe ReplyAppStateRestorationTest, créez une règle de test en utilisant createAndroidComposeRule et en transmettant ComponentActivity comme paramètre de type.

ReplyAppStateRestorationTest.kt

...
class ReplyAppStateRestorationTest {

    /**
     * Note: To access to an empty activity, the code uses ComponentActivity instead of
     * MainActivity.
     */
    @get:Rule
    val composeTestRule = createAndroidComposeRule<ComponentActivity>()
}
...
  1. Créez une fonction de test pour vérifier qu'un e-mail est toujours sélectionné dans l'écran de format compact après un changement de configuration.

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {

}
...

Pour tester un changement de configuration, vous devez utiliser StateRestorationTester.

  1. Configurez stateRestorationTester en transmettant composeTestRule comme argument à StateRestorationTester.
  2. Utilisez setContent() avec le composable ReplyApp et transmettez WindowWidthSizeClass.Compact en tant qu'argument windowSize.

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup compact window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }

}
...
  1. Vérifiez qu'un troisième e-mail s'affiche dans l'application. Utilisez la méthode assertIsDisplayed() sur composeTestRule pour rechercher le texte du troisième e-mail.

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup compact window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }

    // Given third email is displayed
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertIsDisplayed()
}
...
  1. Accédez à l'écran des détails de l'e-mail en cliquant sur son objet. Utilisez la méthode performClick() pour naviguer.

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup compact window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }

    // Given third email is displayed
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertIsDisplayed()

    // Open detailed page
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
    ).performClick()
}
...
  1. Vérifiez que le troisième e-mail est bien affiché dans l'écran des détails. Vérifiez l'existence du bouton "Retour" pour confirmer la présence de l'application dans l'écran de détails et vérifiez que le texte du troisième e-mail est bien affiché.

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    ...
    // Open detailed page
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
    ).performClick()

    // Verify that it shows the detailed screen for the correct email
    composeTestRule.onNodeWithContentDescriptionForStringId(
        R.string.navigation_back
    ).assertExists()
    composeTestRule.onNodeWithText(
}
...
  1. Simulez un changement de configuration à l'aide de stateRestorationTester.emulateSavedInstanceStateRestore().

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    ...
    // Verify that it shows the detailed screen for the correct email
    composeTestRule.onNodeWithContentDescriptionForStringId(
        R.string.navigation_back
    ).assertExists()
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertExists()

    // Simulate a config change
    stateRestorationTester.emulateSavedInstanceStateRestore()
}
...
  1. Vérifiez à nouveau que le troisième e-mail est bien affiché dans l'écran des détails. Vérifiez l'existence du bouton "Retour" pour confirmer la présence de l'application dans l'écran de détails et vérifiez que le texte du troisième e-mail est bien affiché.

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup compact window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }

    // Given third email is displayed
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertIsDisplayed()

    // Open detailed page
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
    ).performClick()

    // Verify that it shows the detailed screen for the correct email
    composeTestRule.onNodeWithContentDescriptionForStringId(
        R.string.navigation_back
    ).assertExists()
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertExists()

    // Simulate a config change
    stateRestorationTester.emulateSavedInstanceStateRestore()

    // Verify that it still shows the detailed screen for the same email
    composeTestRule.onNodeWithContentDescriptionForStringId(
        R.string.navigation_back
    ).assertExists()
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertExists()
}

...
  1. Exécutez le test avec un émulateur de téléphone ou un émulateur redimensionnable en mode Téléphone.
  2. Vérifiez que le test réussit.

Tester un changement de configuration sur l'écran étendu

Pour tester un changement de configuration sur l'écran étendu en simulant le changement et en transmettant la classe WindowWidthSizeClass appropriée, procédez comme suit :

  1. Créez une fonction de test pour vérifier qu'un e-mail est toujours sélectionné dans l'écran de détails après un changement de configuration.

ReplyAppStateRestorationTest.kt

...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {

}
...

Pour tester un changement de configuration, vous devez utiliser StateRestorationTester.

  1. Configurez stateRestorationTester en transmettant composeTestRule comme argument à StateRestorationTester.
  2. Utilisez setContent() avec le composable ReplyApp et transmettez WindowWidthSizeClass.Expanded en tant qu'argument windowSize.

ReplyAppStateRestorationTest.kt

...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup expanded window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }
}
...
  1. Vérifiez qu'un troisième e-mail s'affiche dans l'application. Utilisez la méthode assertIsDisplayed() sur composeTestRule pour rechercher le texte du troisième e-mail.

ReplyAppStateRestorationTest.kt

...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup expanded window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }

    // Given third email is displayed
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertIsDisplayed()
}
...
  1. Sélectionnez le troisième e-mail sur l'écran de détails. Utilisez la méthode performClick() pour sélectionner l'e-mail.

ReplyAppStateRestorationTest.kt

...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup expanded window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }

    // Given third email is displayed
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertIsDisplayed()

    // Select third email
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
    ).performClick()
    ...
}

...
  1. Vérifiez que le troisième e-mail est bien affiché sur l'écran de détails en utilisant testTag sur cet écran et en recherchant le texte sur ses éléments enfants. Avec cette méthode, vous êtes sûr de trouver le texte dans la section des détails et non dans la liste de diffusion.

ReplyAppStateRestorationTest.kt

...

@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
    ...
    // Select third email
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
    ).performClick()

    // Verify that third email is displayed on the details screen
    composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
        .assertAny(hasAnyDescendant(hasText(
            composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
        )
...
}

...
  1. Simulez un changement de configuration à l'aide de stateRestorationTester.emulateSavedInstanceStateRestore().

ReplyAppStateRestorationTest.kt

...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
    ...
    // Verify that third email is displayed on the details screen
    composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
        .assertAny(hasAnyDescendant(hasText(
            composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
        )

    // Simulate a config change
    stateRestorationTester.emulateSavedInstanceStateRestore()
    ...
}
...
  1. Vérifiez à nouveau que le troisième e-mail est bien affiché sur l'écran de détails à la suite d'un changement de configuration.

ReplyAppStateRestorationTest.kt

...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup expanded window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }

    // Given third email is displayed
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertIsDisplayed()

    // Select third email
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
    ).performClick()

    // Verify that third email is displayed on the details screen
    composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
        .assertAny(hasAnyDescendant(hasText(
            composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
        )

    // Simulate a config change
    stateRestorationTester.emulateSavedInstanceStateRestore()

    // Verify that third email is still displayed on the details screen
    composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
        .assertAny(hasAnyDescendant(hasText(
            composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
        )
}
...
  1. Exécutez le test avec un émulateur de tablette ou un émulateur redimensionnable en mode Tablette.
  2. Vérifiez que le test réussit.

Utiliser des annotations pour regrouper les tests pour différentes tailles d'écran

Comme vous avez pu le constater dans les sections précédentes, certains tests ont échoué lorsqu'ils étaient exécutés sur des appareils ayant une taille d'écran incompatible. Bien qu'il soit possible d'exécuter les tests un par un à l'aide d'un appareil approprié, il se peut que cette méthode ne soit pas adaptée s'il y a de nombreux scénarios de test.

Une solution consiste à créer des annotations indiquant les tailles d'écran sur lesquelles le test peut s'exécuter et à configurer le test annoté pour les appareils appropriés.

Pour exécuter un test basé sur la taille d'écran, procédez comme suit :

  1. Dans le répertoire de test, créez le fichier TestAnnotations.kt, qui contient trois classes d'annotation : TestCompactWidth, TestMediumWidth et TestExpandedWidth.

TestAnnotations.kt

...
annotation class TestCompactWidth
annotation class TestMediumWidth
annotation class TestExpandedWidth
...
  1. Utilisez les annotations sur les fonctions de test pour les écrans de format compact en plaçant l'annotation TestCompactWidth après l'annotation de test dans ReplyAppTest et ReplyAppStateRestorationTest.

ReplyAppTest.kt

...
    @Test
    @TestCompactWidth
    fun compactDevice_verifyUsingBottomNavigation() {
...

ReplyAppStateRestorationTest.kt

...
    @Test
    @TestCompactWidth
    fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {

...
  1. Utilisez les annotations sur les fonctions de test pour les écrans de taille moyenne en plaçant l'annotation TestMediumWidth après l'annotation de test dans ReplyAppTest.

ReplyAppTest.kt

...
    @Test
    @TestMediumWidth
    fun mediumDevice_verifyUsingNavigationRail() {
...
  1. Utilisez les annotations sur les fonctions de test pour les écrans étendus en plaçant l'annotation TestExpandedWidth après l'annotation de test dans ReplyAppTest et ReplyAppStateRestorationTest.

ReplyAppTest.kt

...
    @Test
    @TestExpandedWidth
    fun expandedDevice_verifyUsingNavigationDrawer() {
...

ReplyAppStateRestorationTest.kt

...
    @Test
    @TestExpandedWidth
    fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
...

Pour que le test réussisse, configurez-le de telle sorte qu'il exécute uniquement les tests annotés avec TestCompactWidth.

  1. Dans le lanceur de test d'Android Studio, cliquez sur l'icône en forme de clé à molette pour configurer le test.
  2. Renommez le test Compact Test (Test compact), puis choisissez d'exécuter le test All in Package (Tous dans le package).

a277b820697a102f.png

  1. Cliquez sur les points de suspension () à droite du champ Instrumentation arguments (Arguments d'instrumentation).
  2. Cliquez sur le bouton Plus (+), puis ajoutez les paramètres supplémentaires : annotation avec la valeur com.example.reply.test.TestCompactWidth.

944778497cfa41d4.png

  1. Exécutez les tests avec un émulateur compact.
  2. Vérifiez que seuls les tests compacts ont été exécutés.

626e73e8325acb0c.png

  1. Répétez la procédure pour les écrans de taille moyenne et étendus.

6. Télécharger le code de solution

Pour télécharger le code de l'atelier de programmation terminé, utilisez la commande Git suivante :

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-reply-app.git

Vous pouvez également télécharger le dépôt sous forme de fichier ZIP, le décompresser et l'ouvrir dans Android Studio.

Si vous souhaitez voir le code de solution, affichez-le sur GitHub.

7. Conclusion

Félicitations ! Vous avez adapté l'application Reply à toutes les tailles d'écran en implémentant une mise en page adaptative. Vous avez également appris à utiliser des aperçus pour accélérer le développement et à préserver la qualité de votre application à l'aide de différentes méthodes de test.

N'oubliez pas de partager le fruit de vos efforts sur les réseaux sociaux avec le hashtag #AndroidBasics.

En savoir plus