Bien que la migration des vues vers Compose soit purement liée à l'interface utilisateur, de nombreux éléments doivent être pris en compte pour effectuer une migration incrémentielle et sûre. Cette page présente quelques points importants à retenir lors de la migration d'une application basée sur les vues vers Compose.
Migration du thème de votre application
Material Design est le système de conception recommandé pour la thématisation des applications Android.
Pour les applications basées sur des vues, trois versions de Material sont disponibles :
- Material Design 1 avec la bibliothèque AppCompat (c.-à-d.
Theme.AppCompat.*
) - Material Design 2 avec la bibliothèque MDC-Android (c.-à-d.
Theme.MaterialComponents.*
) - Material Design 3 avec la bibliothèque MDC-Android (c.-à-d.
Theme.Material3.*
)
Pour les applications Compose, deux versions de Material sont disponibles :
- Material Design 2 avec la bibliothèque Compose Material (c.-à-d.
androidx.compose.material.MaterialTheme
) - Material Design 3 avec la bibliothèque Compose Material 3 (c.-à-d.
androidx.compose.material3.MaterialTheme
)
Nous vous recommandons d'utiliser la dernière version (Material 3) si le système de conception de votre application le permet. Des guides de migration sont disponibles pour les vues et Compose :
- De Material 1 à Material 2 dans les vues
- De Material 2 à Material 3 dans les vues
- De Material 2 à Material 3 dans Compose
Lorsque vous créez des écrans dans Compose, quelle que soit la version de Material Design que vous utilisez, veillez à appliquer un MaterialTheme
avant tous les composables qui émettent des UI depuis les bibliothèques Material de Compose. Les composants Material (Button
, Text
, etc.) dépendent de la mise en place d'un MaterialTheme
, sans lequel leur comportement n'est pas défini.
Tous les exemples avec Jetpack Compose utilisent un thème Compose personnalisé basé sur un MaterialTheme
.
Pour en savoir plus, consultez les sections Concevoir des systèmes dans Compose et Migrer des thèmes XML vers Compose.
Navigation
Si vous utilisez le composant Navigation dans votre application, consultez les pages Naviguer avec Compose : interopérabilité et Migrer Jetpack Navigation vers Navigation Compose pour en savoir plus.
Tester votre interface utilisateur mixte Compose/Vues
Après avoir migré certaines parties de votre application vers Compose, vous devez effectuer des tests pour vous assurer que vous n'avez rien endommagé.
Lorsqu'une activité ou un fragment utilise Compose, vous devez utiliser createAndroidComposeRule
au lieu d'ActivityScenarioRule
. createAndroidComposeRule
intègre ActivityScenarioRule
avec une ComposeTestRule
qui vous permet de tester Compose et d'afficher le code dans le même temps.
class MyActivityTest { @Rule @JvmField val composeTestRule = createAndroidComposeRule<MyActivity>() @Test fun testGreeting() { val greeting = InstrumentationRegistry.getInstrumentation() .targetContext.resources.getString(R.string.greeting) composeTestRule.onNodeWithText(greeting).assertIsDisplayed() } }
Pour en savoir plus sur les tests, consultez Tester votre mise en page Compose. Pour en savoir plus sur l'interopérabilité avec les frameworks de test de l'interface utilisateur, consultez les pages Interopérabilité avec Espresso et Interopérabilité avec UiAutomator.
Intégrer Compose dans l'architecture existante de votre application
Les schémas d'architecture de flux de données unidirectionnel (UDF) fonctionnent parfaitement avec Compose. Si l'application utilise d'autres types de schémas d'architecture à la place, par exemple un modèle MVP (Model View Presenter), nous vous recommandons de migrer cette partie de l'interface utilisateur vers l'UDF avant ou pendant l'adoption de Compose.
Utiliser un ViewModel
dans Compose
Si vous utilisez la bibliothèque Architecture Components ViewModel
, vous pouvez accéder à un ViewModel
à partir de n'importe quel composable en appelant la fonction viewModel()
, comme expliqué dans la section Compose et autres bibliothèques.
Lorsque vous adoptez Compose, veillez à utiliser le même type ViewModel
dans différents composables, car les éléments ViewModel
suivent les champs d'application du cycle de vie View. Le champ d'application correspondra à l'activité hôte, au fragment ou au graphique de navigation si la bibliothèque Navigation est utilisée.
Par exemple, si les composables sont hébergés dans une activité, viewModel()
renvoie toujours la même instance qui ne sera effacée qu'une fois l'activité terminée.
Dans l'exemple suivant, le même utilisateur ("user1") sera accueilli deux fois, car la même instance GreetingViewModel
est réutilisée dans tous les composables de l'activité hôte. La première instance ViewModel
créée est réutilisée dans d'autres composables.
class GreetingActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { Column { GreetingScreen("user1") GreetingScreen("user2") } } } } } @Composable fun GreetingScreen( userId: String, viewModel: GreetingViewModel = viewModel( factory = GreetingViewModelFactory(userId) ) ) { val messageUser by viewModel.message.observeAsState("") Text(messageUser) } class GreetingViewModel(private val userId: String) : ViewModel() { private val _message = MutableLiveData("Hi $userId") val message: LiveData<String> = _message } class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { return GreetingViewModel(userId) as T } }
Comme les graphiques de navigation couvrent également les éléments ViewModel
, les composables qui sont une destination dans un graphique de navigation ont une instance différente de ViewModel
.
Dans ce cas, le champ d'application de ViewModel
est limité au cycle de vie de la destination et est effacé lorsque la destination est supprimée de la pile "Retour". Dans l'exemple suivant, lorsque l'utilisateur accède à l'écran Profil, une instance de GreetingViewModel
est créée.
@Composable fun MyApp() { NavHost(rememberNavController(), startDestination = "profile/{userId}") { /* ... */ composable("profile/{userId}") { backStackEntry -> GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "") } } }
Source de référence de l'état
Lorsque vous adoptez Compose dans une partie de l'interface utilisateur, il est possible qu'il doive partager des données avec le code du système de vues. Dans la mesure du possible, nous vous recommandons d'encapsuler cet état partagé dans une autre classe qui respecte les bonnes pratiques d'UDF utilisées par les deux plates-formes, par exemple dans un ViewModel
qui expose un flux de données partagées afin d'émettre des mises à jour de données.
Toutefois, ce n'est pas toujours possible si les données à partager sont mutables ou si elles sont étroitement liées à un élément d'interface utilisateur. Dans ce cas, l'un des systèmes doit être la source de référence et ce système doit partager toutes les mises à jour de données avec l'autre système. En règle générale, la source de référence doit appartenir à l'élément le plus proche de la racine de la hiérarchie de l'interface utilisateur.
Compose comme source de référence
Utilisez le composable SideEffect
pour publier l'état de Compose dans du code non spécifique à Compose. Dans ce cas, la source de référence est conservée dans un composable qui envoie des mises à jour d'état.
Par exemple, votre bibliothèque d'analyse peut vous permettre de segmenter votre population d'utilisateurs en associant des métadonnées personnalisées (propriétés utilisateur dans cet exemple) à tous les événements d'analyse suivants. Pour communiquer le type d'utilisateur de l'utilisateur actuel à votre bibliothèque d'analyse, utilisez SideEffect
afin de mettre à jour sa valeur.
@Composable fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics { val analytics: FirebaseAnalytics = remember { FirebaseAnalytics() } // On every successful composition, update FirebaseAnalytics with // the userType from the current User, ensuring that future analytics // events have this metadata attached SideEffect { analytics.setUserProperty("userType", user.userType) } return analytics }
Pour en savoir plus, consultez Effets secondaires dans Compose.
Système de vues comme source de référence
Si le système de vues est propriétaire de l'état et le partage avec Compose, nous vous recommandons de l'encapsuler dans des objets mutableStateOf
afin qu'il soit thread-safe pour Compose. Si vous utilisez cette approche, les fonctions modulables sont simplifiées, car elles ne disposent plus de la source de référence. Toutefois, le système View doit mettre à jour l'état modifiable et les vues qui utilisent cet état.
Dans l'exemple suivant, CustomViewGroup
contient une TextView
et une ComposeView
contenant un composable TextField
. TextView
doit afficher le contenu de ce que l'utilisateur saisit dans TextField
.
class CustomViewGroup @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : LinearLayout(context, attrs, defStyle) { // Source of truth in the View system as mutableStateOf // to make it thread-safe for Compose private var text by mutableStateOf("") private val textView: TextView init { orientation = VERTICAL textView = TextView(context) val composeView = ComposeView(context).apply { setContent { MaterialTheme { TextField(value = text, onValueChange = { updateState(it) }) } } } addView(textView) addView(composeView) } // Update both the source of truth and the TextView private fun updateState(newValue: String) { text = newValue textView.text = newValue } }
Migrer une interface utilisateur partagée
Si vous migrez progressivement vers Compose, vous devrez peut-être utiliser des éléments d'interface utilisateur partagés par Compose et le système de vues. Par exemple, si votre application dispose d'un composant CallToActionButton
personnalisé, vous devrez peut-être l'utiliser à la fois dans des écrans Compose et des écrans basés sur les vues.
Dans Compose, les éléments d'interface utilisateur partagés deviennent des composables qui peuvent être réutilisés dans l'application, que leur style soit défini par du code XML ou qu'ils'agisse d'une vue personnalisée. Par exemple, vous pouvez créer un composable CallToActionButton
pour votre composant d'incitation à l'action Button
personnalisé.
Pour utiliser le composable sur les écrans basés sur une vue, créez un wrapper de vue personnalisé qui s'étend depuis AbstractComposeView
. Dans son composable Content
remplacé, placez le composable que vous avez créé encapsulé dans votre thème Compose, comme illustré dans l'exemple ci-dessous :
@Composable fun CallToActionButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, ) { Button( colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.secondary ), onClick = onClick, modifier = modifier, ) { Text(text) } } class CallToActionViewButton @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : AbstractComposeView(context, attrs, defStyle) { var text by mutableStateOf("") var onClick by mutableStateOf({}) @Composable override fun Content() { YourAppTheme { CallToActionButton(text, onClick) } } }
Notez que les paramètres du composable deviennent des variables modifiables dans la vue personnalisée. Ainsi, la vue CallToActionViewButton
personnalisée peut être gonflée et utilisée comme une vue traditionnelle. Voici un exemple avec View Binding ci-dessous :
class ViewBindingActivity : ComponentActivity() { private lateinit var binding: ActivityExampleBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityExampleBinding.inflate(layoutInflater) setContentView(binding.root) binding.callToAction.apply { text = getString(R.string.greeting) onClick = { /* Do something */ } } } }
Si le composant personnalisé contient un état modifiable, consultez la section Source de référence de l'état.
En priorité : séparer l'état de la présentation
Traditionnellement, une View
est associée à un état. La View
gère des champs qui décrivent ce qui doit être affiché, mais aussi comment l'afficher. Lorsque vous convertissez une View
en Compose, cherchez à séparer les données affichées afin d'obtenir un flux de données unidirectionnel, comme expliqué plus en détail dans la section Hissage d'état.
Par exemple, une View
comporte une propriété visibility
qui indique si elle est visible, invisible ou supprimée. Cette propriété est inhérente à la View
. Bien que d'autres éléments de code puissent affecter la visibilité d'une View
, seule la View
concernée connaît son état de visibilité actuel. La logique qui détermine la visibilité d'une View
peut être sujette aux erreurs et est souvent liée à la View
elle-même.
En revanche, Compose permet d'afficher facilement des composables complètement différents à l'aide de logiques conditionnelles en langage Kotlin :
@Composable fun MyComposable(showCautionIcon: Boolean) { if (showCautionIcon) { CautionIcon(/* ... */) } }
De par sa conception, CautionIcon
n'a pas besoin de connaître ni de considérer ce qui est affiché, et n'a aucun concept de visibility
. L'élément fait simplement partie de la composition, ou non.
En séparant clairement la gestion de l'état et la logique de présentation, vous pouvez modifier plus librement la manière dont le contenu est affiché en tant que conversion d'un état en UI. La possibilité de hisser l'état en cas de besoin rend également les composables plus réutilisables, car la propriété de l'état est plus flexible.
Privilégier les composants encapsulés et réutilisables
Les éléments View
ont souvent une notion de leur emplacement : dans une Activity
, un Dialog
, un Fragment
ou quelque part dans la hiérarchie d'une autre View
. Souvent gonflée à partir de fichiers de mise en page statiques, la structure globale de la View
a tendance à être très rigide. Cela entraîne un couplage plus fort et rend la View
plus difficile à modifier ou réutiliser.
Par exemple, une View
personnalisée peut supposer qu'elle dispose d'une vue enfant d'un type donné, associée à un ID donné, et modifier ses propriétés directement en réponse à une action. Cela permet un couplage fort de ces éléments View
: la View
personnalisée peut planter ou dysfonctionner si elle ne parvient pas à trouver l'enfant, et celui-ci ne pourra probablement pas être réutilisé sans la View
parent.
Grâce aux composables réutilisables, ce problème est moins contraignant dans Compose. Les parents peuvent facilement spécifier l'état et les rappels. Vous pouvez donc écrire des composables réutilisables sans avoir à connaître l'emplacement exact de leur utilisation.
@Composable fun AScreen() { var isEnabled by rememberSaveable { mutableStateOf(false) } Column { ImageWithEnabledOverlay(isEnabled) ControlPanelWithToggle( isEnabled = isEnabled, onEnabledChanged = { isEnabled = it } ) } }
Dans l'exemple ci-dessus, les trois parties sont davantage encapsulées et moins couplées :
ImageWithEnabledOverlay
a uniquement besoin de connaître l'étatisEnabled
actuel. Cet élément n'a pas besoin de savoir queControlPanelWithToggle
existe ni comment il est contrôlable.ControlPanelWithToggle
ne sait pas queImageWithEnabledOverlay
existe. Quelles que soient les possibilités d'affichage de l'élémentisEnabled
,ControlPanelWithToggle
n'a pas besoin de changer.Pour l'élément parent, la profondeur d'imbrication des éléments
ImageWithEnabledOverlay
ouControlPanelWithToggle
n'a pas d'importance. Ces enfants peuvent animer des changements, remplacer des contenus ou les transmettre à d'autres enfants.
Cette approche est connue sous le nom d'inversion du contrôle. Pour en savoir plus, consultez la documentation CompositionLocal
.
Gérer les changements de taille d'écran
L'un des principaux moyens pour créer des mises en page de View
responsives consiste à disposer de ressources différentes pour selon les formats de fenêtre. Les ressources qualifiées restent une option pour les décisions de mise en page au niveau de l'écran, mais Compose permet de modifier beaucoup plus facilement les mises en page, entièrement depuis le code, grâce à une logique conditionnelle normale. Pour en savoir plus, consultez la section Utiliser des classes de taille de fenêtre.
Reportez-vous également à la page Compatibilité avec différentes tailles d'écran pour en savoir plus sur les techniques prises en charge par Compose pour la création d'interfaces utilisateur adaptatives.
Défilement imbriqué avec des vues
Pour plus d'informations sur la mise en œuvre de l'interopérabilité du défilement imbriqué entre des éléments de vue et des composables à défilement, avec une imbrication bidirectionnelle, consultez la section Interopérabilité des défilements imbriqués.
Compose dans RecyclerView
Les composables dans RecyclerView
sont performants depuis la version 1.3.0-alpha02 de RecyclerView
. Pour bénéficier de ces avantages, assurez-vous d'utiliser au moins la version 1.3.0-alpha02 de RecyclerView
.
Interopérabilité WindowInsets
avec Views
Vous devrez peut-être remplacer les marges intérieures par défaut lorsque votre écran contient à la fois des vues et du code Compose dans la même hiérarchie. Dans ce cas, vous devez indiquer explicitement lequel doit consommer les encarts et lequel doit les ignorer.
Par exemple, si votre mise en page la plus externe est une mise en page de vue Android, vous devez utiliser les marges intérieures dans le système de vue et les ignorer pour Compose.
Si votre mise en page la plus externe est un composable, vous devez consommer les marges intérieures dans Compose et ajouter un espace aux composables AndroidView
en conséquence.
Par défaut, chaque ComposeView
consomme tous les insets au niveau de consommation WindowInsetsCompat
. Pour modifier ce comportement par défaut, définissez ComposeView.consumeWindowInsets
sur false
.
Pour en savoir plus, consultez la documentation sur WindowInsets
dans Compose.
Recommandations personnalisées
- Remarque : Le texte du lien s'affiche lorsque JavaScript est désactivé.
- Afficher des emojis
- Material Design 2 dans Compose
- Encarts de fenêtre dans Compose