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 :
- Créer de nouveaux écrans avec Compose
- 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.
- 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 :
- É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.
- É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 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.
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 :
- Passer en vue Code
- Supprimez le code
ConstraintLayout
et les quatreTextView
s imbriquées dans laNestedScrollView
. 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. - Ajoutez une
ComposeView
avec l'ID de vuecompose_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.
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 :
Où :
- Le style de
Text
estMaterialTheme.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 valeurmatch_parent
de l'attributlayout_width
dans le code XML. - Le modificateur
padding
permet d'appliquer la valeur de marge intérieure horizontalemargin_small
. Cela correspond aux déclarationsmarginStart
etmarginEnd
en XML. La valeurmargin_small
est également la ressource de dimension existante récupérée à l'aide de la fonction d'assistancedimensionResource
. - Le modificateur
wrapContentWidth
permet d'aligner le texte afin de le centrer horizontalement. Cette méthode est semblable à l'attributgravity
decenter_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
:
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 :
À 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 aucolorAccent
utilisé dansplant_watering_header
. Nous utiliseronsMaterialTheme.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 deExperimentalComposeUiApi
. 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 :
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 :
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 :
À 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.
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.
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 :
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 :
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'écrantestShareTextIntent
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.
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 :
- Principes de base de Compose
- Mises en page avec Compose
- Personnalisation des thèmes avec Compose
- Gérer les états dans Compose