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éé à l'aide de vues
  • Comment utiliser une vue depuis Compose
  • Comment créer un thème dans Compose
  • Comment tester un écran mixte écrit à la fois dans les vues et dans 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. Stratégie de migration

L'interopérabilité avec les vues a été conçue dès le départ dans Jetpack Compose. Pour migrer vers Compose, nous vous recommandons une migration incrémentielle durant laquelle Compose et les vues coexistent dans votre codebase jusqu'à ce que votre application soit entièrement dans Compose.

La stratégie de migration recommandée est la suivante :

  1. Créer de nouveaux écrans avec Compose
  2. Lorsque vous créez des fonctionnalités, identifiez les éléments réutilisables et commencez à créer une bibliothèque de composants d'interface utilisateur courants.
  3. Remplacez les fonctionnalités existantes un écran à la fois.

Créer de nouveaux écrans avec Compose

L'utilisation de Compose pour créer de nouvelles fonctionnalités englobant un écran entier est le meilleur moyen de favoriser votre adoption de Compose. Cette stratégie vous permet d'ajouter des fonctionnalités et de profiter des avantages de Compose, tout en continuant à répondre aux besoins de votre entreprise.

Une nouvelle fonctionnalité peut couvrir un écran entier, auquel cas l'intégralité de l'écran devrait se trouver dans Compose. Si vous utilisez la navigation basée sur des fragments, cela signifie que vous devez créer un fragment et que son contenu est dans Compose.

Vous pouvez également intégrer de nouvelles fonctionnalités à un écran existant. Dans ce cas, les vues et Compose coexistent sur le même écran. Supposons que la fonctionnalité que vous ajoutez soit un nouveau type de vue dans un objet RecyclerView. Dans ce cas, le nouveau type de vue serait dans Compose, tandis que les autres éléments resteraient les mêmes.

Créer une bibliothèque de composants d'interface utilisateur courants

Lorsque vous créez des fonctionnalités avec Compose, vous vous apercevez rapidement que vous créez aussi une bibliothèque de composants. Vous devez identifier les composants réutilisables pour favoriser leur réutilisation dans votre application afin que les composants partagés disposent d'une source unique fiable. Les fonctionnalités que vous créez pourront ensuite dépendre de cette bibliothèque.

Remplacer des fonctionnalités existantes avec Compose

En plus de créer des fonctionnalités, vous devrez migrer progressivement les fonctionnalités existantes de votre application vers Compose. C'est à vous de choisir la marche à suivre :

  1. Écrans simples : écrans simples dans votre application, avec peu d'éléments d'interface utilisateur et peu d'éléments dynamiques (écran d'accueil, écran de confirmation ou écran de paramètres, par exemple). Il s'agit de bons candidats pour migrer vers Compose, car il suffit de quelques lignes de code.
  2. Écrans mixtes vues et Compose : les écrans qui contiennent déjà un peu de code Compose sont d'autres bons candidat, car vous pouvez continuer à migrer les éléments de cet écran petit à petit. Si un écran ne contient qu'une seule sous-arborescence dans Compose, vous pouvez continuer à migrer les autres parties de l'arborescence jusqu'à ce que l'intégralité de l'interface utilisateur soit dans Compose. Il s'agit d'une approche de migration ascendante.

Approche ascendante de la migration d'une interface mixte vues et Compose vers Compose

Approche utilisée dans cet atelier de programmation

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/android/codelab-android-compose

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

Exécuter l'application exemple

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 :

  • La branche main est le 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 une commande cd dans le répertoire du projet MigrationCodelab, suivie de la commande ci-dessous :

$ git checkout end

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.

Si vous ouvrez le fichier build.gradle au niveau de l'application, vous noterez que les dépendances de Compose sont importées, ce qui permet à Android Studio d'utiliser Compose avec l'indicateur buildFeatures { compose true }.

app/build.gradle

android {
    //...
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        //...
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion '1.3.2'
    }
}

dependencies {
    //...
    // Compose
    def composeBom = platform('androidx.compose:compose-bom:2024.09.02')
    implementation(composeBom)
    androidTestImplementation(composeBom)

    implementation "androidx.compose.runtime:runtime"
    implementation "androidx.compose.ui:ui"
    implementation "androidx.compose.foundation:foundation"
    implementation "androidx.compose.foundation:foundation-layout"
    implementation "androidx.compose.material3:material3"
    implementation "androidx.compose.runtime:runtime-livedata"
    implementation "androidx.compose.ui:ui-tooling"
    //...
}

La version de ces dépendances est définie dans le fichier build.gradle au niveau du projet.

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.

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 quatre 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. La mise en commentaire du 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.

PlantDetailDescription.kt

@Composable
fun PlantDetailDescription() {
    Surface {
        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 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 le composable PlantDetailDescription dans MaterialTheme (Sunflower utilise le Material Design).

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.headlineSmall,
        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

Où :

  • Le style de Text est MaterialTheme.typography.headlineSmall, ce qui est semblable à textAppearanceHeadline5 dans le code XML.
  • Les modificateurs décorent l'élément Text pour refléter sa version XML :
  • Le modificateur fillMaxWidth est utilisé pour occuper la largeur maximale disponible. Il correspond à la valeur match_parent de l'attribut layout_width dans le code XML.
  • Le modificateur padding permet d'appliquer la valeur de marge intérieure horizontale margin_small. Cela correspond aux déclarations marginStart et marginEnd en XML. La valeur margin_small est également la ressource de dimension existante récupérée à l'aide de la fonction d'assistance dimensionResource.
  • Le modificateur wrapContentWidth permet d'aligner le texte afin de le centrer horizontalement. Cette méthode est semblable à l'attribut gravity de center_horizontal en XML.

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().

Étant donné que les valeurs émises par les LiveData peuvent être null, vous devez encapsuler leur utilisation dans une vérification des valeurs null. Pour cette raison, et pour faciliter la réutilisation, il est judicieux de diviser la consommation des LiveData et d'écouter différents composables. Nous allons donc créer un composable appelé PlantDetailContent, qui affichera les informations de Plant.

Avec ces mises à jour, le fichier PlantDetailDescription.kt devrait maintenant se présenter comme suit :

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)
    }
}

PlantNamePreview devrait refléter notre modification sans avoir à effectuer une mise à jour directe, car PlantDetailContent n'appelle que PlantName :

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 est 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 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 des composables Text pour afficher les consignes d'arrosage à l'écran :

PlantDetailDescription.kt

@OptIn(ExperimentalComposeUiApi::class)
@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.colorScheme.primaryContainer,
            fontWeight = FontWeight.Bold,
            modifier = centerWithPaddingModifier.padding(top = normalPadding)
        )

        val wateringIntervalText = pluralStringResource(
            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.colorScheme.primaryContainer pour le moment. Vous améliorerez cet aspect dans la section sur l'interopérabilité des thèmes.
  • Dans Compose 1. 2.1, l'utilisation de pluralStringResource nécessite l'activation de ExperimentalComposeUiApi. Dans une prochaine version de Compose, vous n'aurez peut-être plus besoin de cette activation.

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 vous permet de construire un élément View dans son lambda factory. Il fournit également un lambda update qui est appelé lorsque la vue est gonflée et lors des recompositions ultérieures.

Commençons par créer notre nouveau composable, PlantDescription. Ce composable appelle AndroidView, qui construit une TextView dans son lambda factory. Dans le lambda factory, initialisez un objet TextView qui affiche le texte au format HTML, puis définissez movementMethod sur une instance de LinkMovementMethod. Enfin, dans le lambda update, définissez le texte de TextView sur htmlDescription.

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.

Par conséquent, le rappel de mise à jour d'AndroidView est recomposé si l'élément htmlDescription change. Toute lecture d'un état dans le lambda update 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

Compose supprime la composition chaque fois 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 deux raisons :

  • Pour conserver l'état, la composition doit suivre le cycle de vie de la vue du fragment pour les types de View de l'UI Compose.
  • Lors des transitions, l'élément ComposeView sous-jacent est en état de dissociation. Toutefois, les éléments de l'interface utilisateur de Compose restent visibles pendant ces transitions.

Pour modifier ce comportement, appelez setViewCompositionStrategy avec la ViewCompositionStrategy approprié afin qu'il suive le cycle de vie de la vue du fragment. Plus précisément, vous devrez utiliser 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 ComposeView applique la stratégie DisposeOnViewTreeLifecycleDestroyed. Il est toutefois recommandé de toujours définir cette stratégie lorsque vous utilisez ComposeView dans des fragments.

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. Thématisation Material

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.

Pour utiliser les couleurs de thème appropriées, vous devez personnaliser MaterialTheme en définissant votre propre thème et en indiquant les couleurs de celui-ci.

Personnaliser MaterialTheme

Pour créer votre propre thème, ouvrez le fichier Theme.kt sous le package theme. Theme.kt définit un composable appelé SunflowerTheme qui accepte un lambda de contenu et le transmet à MaterialTheme.

Il ne fait rien d'intéressant pour l'instant. Vous le personnaliserez à l'étape suivante.

Theme.kt

import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable

@Composable
fun SunflowerTheme(
    content: @Composable () -> Unit
) {
    MaterialTheme(content = content)
}

MaterialTheme vous permet de personnaliser ses couleurs, sa typographie et ses formes. Pour l'instant, continuez et personnalisez les couleurs en utilisant les mêmes dans le thème de la vue Sunflower. SunflowerTheme peut également accepter un paramètre booléen appelé darkTheme, qui sera défini par défaut sur true si le système est en mode sombre. Sinon, il sera défini sur false. Ce paramètre permet de transmettre les valeurs de couleur appropriées à MaterialTheme pour qu'elles correspondent au thème système actuellement défini.

Theme.kt

@Composable
fun SunflowerTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val lightColors  = lightColorScheme(
        primary = colorResource(id = R.color.sunflower_green_500),
        primaryContainer = colorResource(id = R.color.sunflower_green_700),
        secondary = colorResource(id = R.color.sunflower_yellow_500),
        background = colorResource(id = R.color.sunflower_green_500),
        onPrimary = colorResource(id = R.color.sunflower_black),
        onSecondary = colorResource(id = R.color.sunflower_black),
    )
    val darkColors  = darkColorScheme(
        primary = colorResource(id = R.color.sunflower_green_100),
        primaryContainer = colorResource(id = R.color.sunflower_green_200),
        secondary = colorResource(id = R.color.sunflower_yellow_300),
        onPrimary = colorResource(id = R.color.sunflower_black),
        onSecondary = colorResource(id = R.color.sunflower_black),
        onBackground = colorResource(id = R.color.sunflower_black),
        surface = colorResource(id = R.color.sunflower_green_100_8pc_over_surface),
        onSurface = colorResource(id = R.color.sunflower_white),
    )
    val colors = if (darkTheme) darkColors else lightColors
    MaterialTheme(
        colorScheme = colors,
        content = content
    )
}

Pour l'utiliser, remplacez MaterialTheme par SunflowerTheme. Par exemple, dans PlantDetailFragment :

PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    ...
    composeView.apply {
        ...
        setContent {
            SunflowerTheme {
                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, "")
    SunflowerTheme {
        PlantDetailContent(plant)
    }
}

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

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

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

Comme vous pouvez le voir dans l'aperçu, les couleurs doivent désormais correspondre à celles du thème Sunflower.

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, "")
    SunflowerTheme {
        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 :

438d2dd9f8acac39.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