Effectuer une migration vers Jetpack Compose

1. Introduction

Compose et le système de vues peuvent fonctionner ensemble.

Dans cet atelier de programmation, vous allez migrer vers Compose certains éléments de l'écran de détails de la plante de Sunflower. Nous avons créé une copie du projet pour vous permettre d'expérimenter la migration d'une application réaliste vers Compose.

À la fin de l'atelier, vous pourrez poursuivre la migration et convertir les autres écrans de Sunflower si vous le souhaitez.

Pour obtenir de l'aide tout au long de cet atelier de programmation, reportez-vous au code suivant :

Objectifs de l'atelier

Cet atelier de programmation traite des points suivants :

  • Les différents parcours de migration que vous pouvez suivre
  • Comment migrer par incréments une application vers Compose
  • Comment ajouter Compose à un écran préexistant créé le système de vues sous Android
  • Comment utiliser une vue Android dans Compose
  • Comment transposer votre thème du système de vues vers Compose
  • Comment tester un écran avec le système de vues et le code Compose

Conditions préalables

  • Connaître la syntaxe du langage Kotlin, y compris les lambdas
  • Connaître les bases de Compose

Ce dont vous avez besoin

2. Préparer la migration

Vous et votre équipe pouvez choisir comment migrer vers Compose. Il existe de nombreuses façons d'intégrer Jetpack Compose dans une application Android existante. Les deux exemples suivants sont des stratégies de migration courantes :

  • Développer un nouvel écran entièrement dans Compose
  • Utiliser un écran existant et en migrer progressivement les composants

Compose sur les nouveaux écrans

Lorsqu'il s'agit de refactoriser une application pour l'adapter à une nouvelle technologie, une approche courante consiste à l'adopter dans les nouvelles fonctionnalités créées pour l'application. Dans ce cas, vous créerez de nouveaux écrans. Si vous devez développer un nouvel écran d'interface utilisateur pour votre application, envisagez d'utiliser Compose tout en maintenant le reste de l'application dans le système de vues.

Dans ce second cas, vous établirez une interopérabilité avec Compose en périphérie des fonctionnalités migrées.

Combiner Compose et le système de vues

Certains éléments d'un même écran peuvent être migrés vers Compose tandis que les autres sont maintenus dans le système de vue. Par exemple, vous pouvez migrer une RecyclerView sans transférer le reste de l'écran.

À l'inverse, vous pouvez utiliser Compose comme structure de mise en page pour des vues existantes comme MapView ou AdView, qui ne seraient pas disponibles dans Compose.

Migration complète

Vous pouvez migrer des fragments ou des écrans entiers vers Compose, l'un après l'autre. C'est une approche simple, mais peu précise.

Et dans cet atelier ?

Dans cet atelier de programmation, vous procéderez à la migration par incréments de l'écran des détails de la plante de Sunflower, en combinant Compose et le système de vues. Au terme de l'atelier, vous en saurez suffisamment pour poursuivre la migration, si vous le souhaitez.

3. Configuration

Obtenir le code

Obtenez le code de l'atelier de programmation sur GitHub :

$ git clone https://github.com/googlecodelabs/android-compose-codelabs

Vous pouvez également télécharger le dépôt sous forme de fichier ZIP :

Ouvrir Android Studio

Cet atelier de programmation nécessite Android Studio Bumblebee.

Exécuter l'exemple d'application

Le dépôt que vous venez de télécharger contient du code pour tous les ateliers de programmation traitant de Compose. Pour cet atelier, ouvrez le projet MigrationCodelab dans Android Studio.

Dans cet atelier, vous allez migrer l'écran des détails de la plante de Sunflower vers Compose. Pour ouvrir l'écran des détails, appuyez sur l'une des plantes disponibles sur l'écran de liste.

bb6fcf50b2899894.png

Configuration du projet

Le projet comporte plusieurs branches git :

  • main est la branche que vous avez extraite ou téléchargée. Il s'agit du point de départ de l'atelier de programmation.
  • end contient la solution à cet atelier de programmation.

Nous vous recommandons de commencer par le code de la branche main, puis de suivre l'atelier étape par étape, à votre propre rythme.

Au cours de cet atelier de programmation, vous découvrirez des extraits de code que vous devrez ajouter au projet. À certains endroits, vous devrez également supprimer le code qui est explicitement mentionné dans les commentaires sur les extraits de code.

Pour obtenir la branche end à l'aide de git, exécutez la commande suivante :

$ git clone -b end https://github.com/googlecodelabs/android-compose-codelabs

Vous pouvez également télécharger le code de la solution en cliquant sur le bouton suivant :

Questions fréquentes

4. Compose dans Sunflower

Compose a déjà été ajouté au code que vous avez téléchargé à partir de la branche main. Voyons toutefois les éléments nécessaires à son fonctionnement.

Lorsque vous ouvrez le fichier app/build.gradle (ou build.gradle (Module: compose-migration.app)), vous noterez que les dépendances de Compose sont importées, ce qui permet à Android Studio d'utiliser Compose avec l'option buildFeatures { compose true }.

app/build.gradle

android {
    ...
    kotlinOptions {
        jvmTarget = '1.8'
        useIR = true
    }
    buildFeatures {
        ...
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion rootProject.composeVersion
    }
}

dependencies {
    ...
    // Compose
    implementation "androidx.compose.runtime:runtime:$rootProject.composeVersion"
    implementation "androidx.compose.ui:ui:$rootProject.composeVersion"
    implementation "androidx.compose.foundation:foundation:$rootProject.composeVersion"
    implementation "androidx.compose.foundation:foundation-layout:$rootProject.composeVersion"
    implementation "androidx.compose.material:material:$rootProject.composeVersion"
    implementation "androidx.compose.runtime:runtime-livedata:$rootProject.composeVersion"
    implementation "androidx.compose.ui:ui-tooling:$rootProject.composeVersion"
    implementation "com.google.android.material:compose-theme-adapter:$rootProject.composeVersion"
    ...
}

La version de ces dépendances est définie dans le fichier racine build.gradle.

5. Compose, nous voilà !

Parmi les éléments de l'écran des détails, nous allons migrer la description de la plante vers Compose, sans modifier la structure générale de l'écran. Dans le cas présent, vous suivrez la stratégie de migration Combiner Compose et le système de vues, qui est mentionnée dans la section Préparer votre migration.

Compose a besoin d'une activité ou d'un fragment hôte pour afficher l'interface utilisateur. Comme tous les écrans de Sunflower utilisent des fragments, vous emploierez ComposeView. Cette vue Android peut héberger du contenu UI de Compose via sa méthode setContent.

Supprimer le code XML

Commençons par la migration. Ouvrez fragment_plant_detail.xml et procédez comme suit :

  1. Passer en vue Code
  2. Supprimez le code ConstraintLayout et les TextViews imbriquées dans la NestedScrollView. L'atelier de programmation compare le code XML et s'y réfère lors de la migration d'éléments individuels. Commenter le code s'avérera utile.
  3. Ajoutez une ComposeView avec l'ID de vue compose_view pour héberger le code Compose

fragment_plant_detail.xml

<androidx.core.widget.NestedScrollView
    android:id="@+id/plant_detail_scrollview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:paddingBottom="@dimen/fab_bottom_padding"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

    // Step 2) Comment out ConstraintLayout and its children
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="@dimen/margin_normal">

        <TextView
            android:id="@+id/plant_detail_name"
        ...

    </androidx.constraintlayout.widget.ConstraintLayout>
    // End Step 2) Comment out until here

    // Step 3) Add a ComposeView to host Compose code
    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.core.widget.NestedScrollView>

Ajouter le code Compose

Vous pouvez désormais commencer à migrer l'écran des détails de la plante vers Compose.

Tout au long de l'atelier de programmation, vous allez ajouter du code Compose au fichier PlantDetailDescription.kt, qui se trouve dans le dossier plantdetail. Ouvrez-le. Vous découvrirez le texte "Hello Compose!" dans un espace réservé à l'avance pour notre projet.

plantdetail/PlantDetailDescription.kt

@Composable
fun PlantDetailDescription() {
    Text("Hello Compose")
}

Affichons ce texte sur l'écran en appelant ce composable à partir de la ComposeView que nous avons ajoutée à l'étape précédente. Ouvrez plantdetail/PlantDetailFragment.kt.

Comme l'écran utilise une liaison de données, vous pouvez accéder directement à la composeView et appeler setContent pour afficher le code Compose sur l'écran. Appelez la composition PlantDetailDescription dans MaterialTheme (Sunflower utilise le Material Design).

plantdetail/PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    ...
    override fun onCreateView(...): View? {
        val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
            inflater, R.layout.fragment_plant_detail, container, false
        ).apply {
            ...
            composeView.setContent {
                // You're in Compose world!
                MaterialTheme {
                    PlantDetailDescription()
                }
            }
        }
        ...
    }
}

Si vous exécutez l'application, "Hello Compose!" s'affiche à l'écran.

66f3525ecf6669e0.png

6. Créer un composable à partir du XML

Commençons par migrer le nom de la plante. Plus précisément, il s'agira de migrer la TextView avec l'ID @+id/plant_detail_name, que vous avez supprimée de fragment_plant_detail.xml. Voici le code XML correspondant :

<TextView
    android:id="@+id/plant_detail_name"
    ...
    android:layout_marginStart="@dimen/margin_small"
    android:layout_marginEnd="@dimen/margin_small"
    android:gravity="center_horizontal"
    android:text="@{viewModel.plant.name}"
    android:textAppearance="?attr/textAppearanceHeadline5"
    ... />

Notez le style textAppearanceHeadline5, la marge horizontale de 8.dp et le centrage sur l'axe horizontal de l'écran. Toutefois, le titre à afficher est observé depuis une LiveData exposée par PlantDetailViewModel, qui provient de la couche du dépôt.

L'observation de LiveData sera abordée plus tard. Pour le moment, supposons que nous disposons du nom et le transmettons en tant que paramètre à un nouveau composable PlantName, que nous créons dans le fichier PlantDetailDescription.kt. Ce composable sera appelé ultérieurement, à partir du composable PlantDetailDescription.

PlantDetailDescription.kt

@Composable
private fun PlantName(name: String) {
    Text(
        text = name,
        style = MaterialTheme.typography.h5,
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = dimensionResource(R.dimen.margin_small))
            .wrapContentWidth(Alignment.CenterHorizontally)
    )
}

@Preview
@Composable
private fun PlantNamePreview() {
    MaterialTheme {
        PlantName("Apple")
    }
}

Avec l'aperçu :

d09fe886b98bde91.png

Spécifications :

  • Le style de Text est MaterialTheme.typography.h5, mappé au textAppearanceHeadline5 du code XML.
  • Les modificateurs décorent l'élément Text pour refléter sa version XML :
  • Le modificateur fillMaxWidth correspond à android:layout_width="match_parent" dans le code XML.
  • Le padding horizontal de margin_small est une valeur issue du système de vues basée sur la fonction d'assistance dimensionResource.
  • wrapContentWidth permet d'aligner horizontalement l'élément Text.

7. ViewModels et LiveData

Transposons maintenant le titre à l'écran. Vous devrez charger les données à l'aide du PlantDetailViewModel. Pour ce faire, Compose dispose d'intégrations pour ViewModel et LiveData.

ViewModels

Étant donné qu'une instance de PlantDetailViewModel est utilisée dans le fragment, nous pourrions nous contenter de la transmettre à PlantDetailDescription en tant que paramètre.

Ouvrez le fichier PlantDetailDescription.kt et ajoutez le paramètre PlantDetailViewModel à PlantDetailDescription :

PlantDetailDescription.kt

@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
    ...
}

À présent, transmettez l'instance de ViewModel lorsque ce composable est appelé à partir du fragment :

PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    ...
    override fun onCreateView(...): View? {
        ...
        composeView.setContent {
            MaterialTheme {
                PlantDetailDescription(plantDetailViewModel)
            }
        }
    }
}

LiveData

Ainsi, vous avez déjà accès au champ LiveData<Plant> du PlantDetailViewModel pour obtenir le nom de la plante.

Pour observer vos LiveData à partir d'un composable, utilisez la fonction LiveData.observeAsState().

Comme les valeurs émises par vos LiveData peuvent être nulles, vous devez encapsuler leur utilisation avec un contrôle des valeurs nulles. Pour cette raison, et pour faciliter la réutilisation, il est judicieux de diviser la consommation des LiveData et d'écouter différents composables. En ce sens, créez un nouveau composable appelé PlantDetailContent, qui affichera les informations de Plant.

Compte tenu de ce qui précède, voici à quoi ressemble le fichier PlantDetailDescription.kt après avoir ajouté l'observation des LiveData.

PlantDetailDescription.kt

@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
    // Observes values coming from the VM's LiveData<Plant> field
    val plant by plantDetailViewModel.plant.observeAsState()

    // If plant is not null, display the content
    plant?.let {
        PlantDetailContent(it)
    }
}

@Composable
fun PlantDetailContent(plant: Plant) {
    PlantName(plant.name)
}

@Preview
@Composable
private fun PlantDetailContentPreview() {
    val plant = Plant("id", "Apple", "description", 3, 30, "")
    MaterialTheme {
        PlantDetailContent(plant)
    }
}

L'aperçu est identique à PlantNamePreview, car PlantDetailContent n'appelle que PlantName pour le moment :

3e47e682cf518c71.png

Vous avez à présent configuré le ViewModel afin d'afficher un nom de plante dans Compose. Dans les sections suivantes, vous allez créer les autres composables et les relier au ViewModel de la même manière.

8. Plus de migrations de code XML

Il sera désormais plus facile de compléter notre interface utilisateur avec les consignes d'arrosage et la description des plantes. Vous pouvez déjà migrer le reste de l'écran en adoptant la même approche d'observation du code XML que précédemment.

Le code XML correspondant aux consignes d'arrosage, que vous avez supprimé de fragment_plant_detail.xml, est constitué de deux TextViews associées aux ID plant_watering_header et plant_watering.

<TextView
    android:id="@+id/plant_watering_header"
    ...
    android:layout_marginStart="@dimen/margin_small"
    android:layout_marginTop="@dimen/margin_normal"
    android:layout_marginEnd="@dimen/margin_small"
    android:gravity="center_horizontal"
    android:text="@string/watering_needs_prefix"
    android:textColor="?attr/colorAccent"
    android:textStyle="bold"
    ... />

<TextView
    android:id="@+id/plant_watering"
    ...
    android:layout_marginStart="@dimen/margin_small"
    android:layout_marginEnd="@dimen/margin_small"
    android:gravity="center_horizontal"
    app:wateringText="@{viewModel.plant.wateringInterval}"
    .../>

Procédez comme précédemment et créez un composable appelé PlantWatering. Ajoutez les éléments Text pour afficher les consignes d'arrosage à l'écran :

PlantDetailDescription.kt

@Composable
private fun PlantWatering(wateringInterval: Int) {
    Column(Modifier.fillMaxWidth()) {
        // Same modifier used by both Texts
        val centerWithPaddingModifier = Modifier
            .padding(horizontal = dimensionResource(R.dimen.margin_small))
            .align(Alignment.CenterHorizontally)

        val normalPadding = dimensionResource(R.dimen.margin_normal)

        Text(
            text = stringResource(R.string.watering_needs_prefix),
            color = MaterialTheme.colors.primaryVariant,
            fontWeight = FontWeight.Bold,
            modifier = centerWithPaddingModifier.padding(top = normalPadding)
        )

        val wateringIntervalText = LocalContext.current.resources.getQuantityString(
            R.plurals.watering_needs_suffix, wateringInterval, wateringInterval
        )
        Text(
            text = wateringIntervalText,
            modifier = centerWithPaddingModifier.padding(bottom = normalPadding)
        )
    }
}

@Preview
@Composable
private fun PlantWateringPreview() {
    MaterialTheme {
        PlantWatering(7)
    }
}

Avec l'aperçu :

6f6c17085801a518.png

À noter :

  • Comme la marge intérieure horizontale et la décoration d'alignement sont partagées avec les composables Text, vous pouvez réutiliser le modificateur en l'attribuant à une variable locale (par exemple, centerWithPaddingModifier). Les modificateurs sont des objets Kotlin standards, que vous maîtrisez déjà.
  • L'élément MaterialTheme de Compose ne correspond pas exactement au colorAccent utilisé dans plant_watering_header. Nous utiliserons MaterialTheme.colors.primaryVariant pour le moment. Vous améliorerez cet aspect dans la section "Interopérabilité des thèmes".

Nous allons connecter tous les éléments et appeler PlantWatering à partir de PlantDetailContent. Le code XML ConstraintLayout que nous avons supprimé au début spécifiait une marge de 16.dp, que nous devons inclure dans notre code Compose.

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="@dimen/margin_normal">

Dans PlantDetailContent, créez une Column pour afficher le nom et les consignes d'arrosage ensemble, et pour intégrer cette marge intérieure. Ajoutez également une Surface pour obtenir les couleurs de texte et d'arrière-plan appropriées.

PlantDetailDescription.kt

@Composable
fun PlantDetailContent(plant: Plant) {
    Surface {
        Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
            PlantName(plant.name)
            PlantWatering(plant.wateringInterval)
        }
    }
}

Si vous actualisez l'aperçu, le résultat devrait être le suivant :

56626a7118ce075c.png

9. Les vues dans le code Compose

Nous allons maintenant migrer la description de la plante. Le code XML dans fragment_plant_detail.xml comportait une TextView spécifiant app:renderHtml="@{viewModel.plant.description}" pour indiquer quel texte afficher à l'écran. renderHtml est un adaptateur de liaison. Vous le trouverez dans le fichier PlantDetailBindingAdapters.kt. La mise en œuvre utilise HtmlCompat.fromHtml pour placer le texte sur la TextView.

Toutefois, Compose n'est pas compatible avec les classes Spanned et ne permet pas d'afficher du texte au format HTML pour le moment. Nous devons donc utiliser une TextView du système de vues dans le code Compose pour contourner cette limitation.

Comme Compose n'est pas encore en mesure d'afficher le code HTML, vous allez générer automatiquement un fichier TextView à cet effet, à l'aide de l'API AndroidView.

AndroidView accepte une View en tant que paramètre et vous rappelle lorsque la vue est gonflée.

Commençons par créer notre nouveau composable, PlantDescription. Ce composable appelle AndroidView avec la TextView que nous venons d'enregistrer dans un lambda. Dans le rappel factory, initialisez une TextView réagissant aux interactions HTML à l'aide du Context donné. Et, dans le rappel update, définissez le texte avec une description au format HTML enregistrée.

PlantDetailDescription.kt

@Composable
private fun PlantDescription(description: String) {
    // Remembers the HTML formatted description. Re-executes on a new description
    val htmlDescription = remember(description) {
        HtmlCompat.fromHtml(description, HtmlCompat.FROM_HTML_MODE_COMPACT)
    }

    // Displays the TextView on the screen and updates with the HTML description when inflated
    // Updates to htmlDescription will make AndroidView recompose and update the text
    AndroidView(
        factory = { context ->
            TextView(context).apply {
                movementMethod = LinkMovementMethod.getInstance()
            }
        },
        update = {
            it.text = htmlDescription
        }
    )
}

@Preview
@Composable
private fun PlantDescriptionPreview() {
    MaterialTheme {
        PlantDescription("HTML<br><br>description")
    }
}

Aperçu :

deea1d191e9087b4.png

Notez que htmlDescription conserve la description HTML d'un élément description transmis en tant que paramètre. Si le paramètre description est modifié, le code htmlDescription dans remember s'exécute à nouveau.

De même, le rappel de mise à jour AndroidView sera recomposé si htmlDescription change. Toute lecture d'un état dans le rappel entraîne une recomposition.

Ajoutons PlantDescription au composable PlantDetailContent et modifions le code de prévisualisation pour afficher également une description HTML :

PlantDetailDescription.kt

@Composable
fun PlantDetailContent(plant: Plant) {
    Surface {
        Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
            PlantName(plant.name)
            PlantWatering(plant.wateringInterval)
            PlantDescription(plant.description)
        }
    }
}

@Preview
@Composable
private fun PlantDetailContentPreview() {
    val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
    MaterialTheme {
        PlantDetailContent(plant)
    }
}

Avec l'aperçu :

7843a8d6c781c244.png

À ce stade, vous avez migré l'ensemble du contenu du fichier ConstraintLayout d'origine vers Compose. Vous pouvez exécuter l'application pour vérifier qu'elle fonctionne comme prévu.

c7021c18eb8b4d4e.gif

10. ViewCompositionStrategy

Par défaut, Compose supprime la composition dès lors que la ComposeView est dissociée d'une fenêtre. Ce n'est pas souhaitable lorsque ComposeView est utilisé dans des fragments, et ce pour plusieurs raisons :

  • Pour conserver l'état, la composition doit suivre le cycle de vie de la vue du fragment pour les types UI View de Compose.
  • Pour maintenir les éléments d'interface utilisateur de Compose à l'écran lors des transitions ou des changements de fenêtre. Pendant les transitions, la ComposeView doit rester visible, même après sa dissociation de la fenêtre.

Vous pouvez appeler manuellement la méthode AbstractComposeView.disposeComposition pour supprimer la composition. Vous pouvez également supprimer automatiquement les compositions qui ne sont plus nécessaires en définissant une autre stratégie, ou en créant la vôtre en appelant la méthode setViewCompositionStrategy.

Utilisez la stratégie DisposeOnViewTreeLifecycleDestroyed pour supprimer la composition lorsque le LifecycleOwner du fragment est détruit.

Comme PlantDetailFragment comporte des transitions d'entrée et de sortie (reportez-vous à nav_garden.xml pour en savoir plus), et comme nous utiliserons des types View dans Compose par la suite, nous devons nous assurer que la ComposeView applique la stratégie DisposeOnViewTreeLifecycleDestroyed. Notez que les bonnes pratiques suggèrent de toujours employer cette stratégie lorsque vous utilisez ComposeView dans des fragments.

plantdetail/PlantDetailFragment.kt

import androidx.compose.ui.platform.ViewCompositionStrategy
...

class PlantDetailFragment : Fragment() {
    ...
    override fun onCreateView(...): View? {
        val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
            inflater, R.layout.fragment_plant_detail, container, false
        ).apply {
            ...
            composeView.apply {
                // Dispose the Composition when the view's LifecycleOwner
                // is destroyed
                setViewCompositionStrategy(
                    ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
                )
                setContent {
                    MaterialTheme {
                        PlantDetailDescription(plantDetailViewModel)
                    }
                }
            }
        }
        ...
    }
}

11. Interopérabilité des thèmes

Nous avons migré le contenu textuel des détails concernant les plantes vers Compose. Cependant, vous avez peut-être remarqué que Compose ne reflète pas correctement les couleurs du thème. Au lieu d'apparaître en vert, le nom de la plante est en violet.

À ce stade de la migration, vous pouvez faire en sorte que Compose hérite des thèmes disponibles dans le système de vues plutôt que de récréer votre propre thème Material dans Compose. Les thèmes Material sont parfaitement compatibles avec tous les composants Material Design utilisés par Compose.

Pour réutiliser le thème MDC (Material Design Components) du système de vues dans Compose, vous pouvez utiliser l'adaptateur compose-theme-adapter. La fonction MdcTheme lit automatiquement le thème MDC du contexte hôte et le transmet pour vous à MaterialTheme, pour le mode clair et le mode sombre. Même si vous ne spécifiez que les couleurs dans le cadre de cet atelier de programmation, la bibliothèque lit également les formes et la typographie du système de vues.

La bibliothèque est déjà incluse dans le fichier app/build.gradle, comme suit :

...
dependencies {
    ...
    implementation "com.google.android.material:compose-theme-adapter:$rootProject.composeVersion"
    ...
}

Pour l'utiliser, remplacez les utilisations de MaterialTheme pour MdcTheme. Par exemple, dans PlantDetailFragment :

PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    ...
    composeView.apply {
        ...
        setContent {
            MdcTheme {
                PlantDetailDescription(plantDetailViewModel)
            }
        }
    }
}

Et tous les composables d'aperçu dans le fichier PlantDetailDescription.kt :

PlantDetailDescription.kt

@Preview
@Composable
private fun PlantDetailContentPreview() {
    val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
    MdcTheme {
        PlantDetailContent(plant)
    }
}

@Preview
@Composable
private fun PlantNamePreview() {
    MdcTheme {
        PlantName("Apple")
    }
}

@Preview
@Composable
private fun PlantWateringPreview() {
    MdcTheme {
        PlantWatering(7)
    }
}

@Preview
@Composable
private fun PlantDescriptionPreview() {
    MdcTheme {
        PlantDescription("HTML<br><br>description")
    }
}

Comme vous pouvez le voir dans l'aperçu, MdcTheme récupère les couleurs du thème dans le fichier styles.xml.

886d7eaea611f4eb.png

Vous pouvez également prévisualiser l'interface utilisateur en mode sombre en créant une fonction et en transmettant Configuration.UI_MODE_NIGHT_YES au uiMode de l'aperçu :

import android.content.res.Configuration
...

@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PlantDetailContentDarkPreview() {
    val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
    MdcTheme {
        PlantDetailContent(plant)
    }
}

Avec l'aperçu :

cfe11c109ff19eeb.png

Si vous exécutez l'application, celle-ci se comporte exactement comme avant la migration, que ce soit avec le thème clair ou le thème sombre :

c99216fc77699dd7.gif

12. Test

Après avoir migré certaines parties de l'écran des détails de la plante vers Compose, il est crucial d'effectuer des tests pour vous assurer que tout fonctionne.

Dans Sunflower, le fichier PlantDetailFragmentTest situé dans le dossier androidTest teste certaines fonctionnalités de l'application. Ouvrez ce fichier et examinez le code actuel :

  • testPlantName vérifie le nom de la plante affichée à l'écran
  • testShareTextIntent vérifie que l'intent approprié se déclenche une fois que l'utilisateur appuie sur le bouton de partage

Lorsqu'une activité ou un fragment utilise Compose, au lieu de ActivityScenarioRule, vous devez utiliser createAndroidComposeRule, qui intègre une ActivityScenarioRule avec une ComposeTestRule pour vous permettre de tester le code Compose.

Dans PlantDetailFragmentTest, remplacez l'utilisation de ActivityScenarioRule par createAndroidComposeRule. Lorsque la règle d'activité est nécessaire pour configurer le test, utilisez l'attribut activityRule de createAndroidComposeRule, comme suit :

@RunWith(AndroidJUnit4::class)
class PlantDetailFragmentTest {

    @Rule
    @JvmField
    val composeTestRule = createAndroidComposeRule<GardenActivity>()

    ...

    @Before
    fun jumpToPlantDetailFragment() {
        populateDatabase()

        composeTestRule.activityRule.scenario.onActivity { gardenActivity ->
            activity = gardenActivity

            val bundle = Bundle().apply { putString("plantId", "malus-pumila") }
            findNavController(activity, R.id.nav_host).navigate(R.id.plant_detail_fragment, bundle)
        }
    }

    ...
}

Si vous exécutez les tests, testPlantName échouera. testPlantName vérifie qu'un élément TextView s'affiche à l'écran. Cependant, vous avez migré cette partie de l'interface utilisateur vers Compose. Vous devez donc utiliser des assertions Compose à la place :

@Test
fun testPlantName() {
    composeTestRule.onNodeWithText("Apple").assertIsDisplayed()
}

Si vous exécutez à nouveau les tests, tous les indicateurs devraient être au vert.

dd59138fac1740e4.png

13. Félicitations

Bravo ! Vous êtes arrivé au terme de cet atelier de programmation.

La branche compose du projet GitHub Sunflower d'origine migre complètement l'écran des détails de la plante vers Compose. Outre ce que vous avez fait dans cet atelier de programmation, elle simule également le comportement de CollapsingToolbarLayout, ce qui implique :

  • de charger des images avec Compose ;
  • des animations ;
  • une meilleure gestion des dimensions ;
  • et plus encore !

Et maintenant ?

Consultez les autres ateliers de programmation du parcours Compose :

Complément d'informations