Multiplatform

Publication de Compose Multiplatform 1.5.0

Read this post in other languages:

Nous avons le plaisir d’annoncer la disponibilité de Compose Multiplatform 1.5.0. Cette mise à jour utilise le framework d’interface utilisateur déclaratif Jetpack Compose pour Kotlin et étend sa prise en charge à de nouvelles plateformes, au-delà d’Android : bureau, iOS et web. La version bureau est stable, la versions iOS est en phase Alpha et la prise en charge pour le web est encore expérimentale. Pour une présentation détaillée, veuillez consulter le site Compose Multiplatform.

Voici les principales améliorations qu’apporte cette mise à jour :

  1. Les API Dialog, Popup et WindowInsets sont maintenant en code commun.
  2. Nous avons amélioré le défilement, la gestion des ressources et les champs de texte sous iOS.
  3. Le framework de test de l’interface utilisateur a été stabilisé sur la version bureau.

Compose Multiplatform 1.5.0 est basé sur Jetpack Compose 1.5, qui est principalement dédié à l’amélioration des performances. Elle s’appuie également sur la version 1.1 de Material Design 3, qui fournit de nouveaux composants tels que les sélecteurs de date et d’heure.

Essayer Compose Multiplatform 1.5.0

Compose Multiplatform prend en charge les boîtes de dialogue, popups et WindowInsets

Les boîtes de dialogue et les popups sont maintenant disponibles dans Compose Multiplatform. Les boîtes de dialogue sont utilisées pour les événements modaux, qui requièrent une action de la part de l’utilisateur, par exemple faire un choix ou saisir des données. Les fenêtres contextuelles sont quant à elles utilisées pour les actions non modales, comme la fourniture de fonctionnalités optionnelles.

Dans cette version, les types de base Dialog et Popup, ainsi que DropdownMenu and AlertDialog, sont accessibles à partir du code commun, ce qui évite de devoir fournir des fonctionnalités spécifiques aux plateformes.

Par exemple, l’élément Composable ci-dessous est entièrement écrit en code commun :

@Composable
fun CommonDialog() {
   var isDialogOpen by remember { mutableStateOf(false) }
   Button(onClick = { isDialogOpen = true }) {
       Text("Open")
   }
   if (isDialogOpen) {
       AlertDialog(
           onDismissRequest = { },
           confirmButton = {
               Button(onClick = { isDialogOpen = false }) {
                   Text("OK")
               }
           },
           title = { Text("Alert Dialog") },
           text = { Text("Lore ipsum") },
       )
   }
}

Voici comment il apparaît sur bureau, Android et iOS :

Démo des boîtes de dialogue sur Bureau

Démo des boîtes de dialogue sur Android et iOS

Une troisième fonctionnalité disponible dans cette version est l’API WindowInsets API, qui décrit le niveau d’ajustement requis pour éviter que votre contenu chevauche l’interface utilisateur du système. Avec Compose Multiplatform 1.5.0, cette fonctionnalité peut être utilisée aussi bien sur Android que sur iOS.

Grâce à l’API WindowInsets, vous pouvez dessiner du contenu d’arrière-plan via Compose Multiplatform derrière l’encoche, sans ajout de ligne blanche en haut de l’application. Les captures d’écran ci-dessous montrent la différence que cela fait :

Utilisation de l'API WindowInsets pour dessiner un contenu d'arrière-plan dans Compose Multiplatform

Améliorations pour iOS

Cette mise jour apporte de nombreuses améliorations pour la plateforme iOS. Le défilement reproduit le look and feel de la plateforme, la gestion des ressources a été simplifiée et la manipulation du texte améliorée.

Défilement naturel

Nous avons adapté le défilement sur iOS de façon à ce qu’il reproduise le défilement natif. Supposons que nous ayons du code dans lequel le nombre et/ou la taille des éléments à afficher excède l’espace disponible :

@Composable
fun NaturalScrolling() {
   val items = (1..30).map { "Item $it" }
   LazyColumn {
       items(items) {
           Text(
               text = it,
               fontSize = 30.sp,
               modifier = Modifier.padding(start = 20.dp)
           )
       }
   }
}

Lors du défilement, les éléments apparaissent depuis les bords de l’écran, comme avec les applications iPhone natives :

Affichage des éléments lors du défilement sur iOS

Prise en charge de Dynamic Type

La fonctionnalité Dynamic Type sur iOS permet à l’utilisateur de définir sa taille de police préférée : plus grande pour une meilleure lisibilité ou plus petite pour afficher plus de contenu. La taille du texte utilisée dans une application doit être relative à ce paramètre système.

Cette fonctionnalité est maintenant prise en charge dans Compose Multiplatform. Les incréments utilisés pour le redimensionnement du texte sont les mêmes que ceux utilisés dans les applications natives, le comportement sera donc identique.

Prenons par exemple le Composable suivant :

@Composable
fun DynamicType() {
   Text("This is some sample text", fontSize = 30.sp)
}

Voici ce qui s’affiche lorsque la taille de lecture préférée est définie au minimum :

Fonctionnalité Dynamic Type sur iOS dans Compose Multiplatform (petit texte)

Et voici ce que cela donne lorsque la taille de lecture choisie est au maximum :

Fonctionnalité Dynamic Type sur iOS dans Compose Multiplatform (grand texte)

Prise en charge des écrans à taux de rafraîchissement élevé

Dans les versions précédentes, la fréquence d’images maximale était de 60 FPS, Cela pouvait entraîner des problèmes de lenteur et de latence de l’interface utilisateur sur les appareils dotés d’écrans 120 Hz. La nouvelle version prend en charge des fréquences d’images allant jusqu’à 120 FPS.

Simplification de la gestion des ressources

À partir de la version 1.5.0, touts les éléments du dossier « resources » d’un ensemble de sources iOS sont copiés dans le bundle de l’application par défaut. Si vous placez un fichier image dans src/commonMain/resources/, il sera donc copié dans le paquet et utilisable dans votre code.

Si vous utilisez CocoaPods, désormais vous n’avez plus à configurer ce comportement dans le fichier de build Gradle. Il n’est pas nécessaire non plus d’appeler de nouveau podInstall pour que les ressources sont copiées après modification.

Si vous essayez de configurer le comportement explicitement dans les scripts de build (comme indiqué ci-dessous), une erreur se produira :

kotlin {
    cocoapods {
        extraSpecAttributes["resources"] = "..."
    }
}

Pour plus de détails et un guide pour la migration du code existant, consultez ce document.

Amélioration de TextField

Dans les versions précédentes, deux cas de saisie de texte pouvaient entraîner un comportement indésirable. Pour la nouvelle version, nous avons travaillé à l’amélioration de TextField afin d’éradiquer ces problèmes.

Problèmes de capitalisation

Tout d’abord, TextField est maintenant capable de détecter si la mise en majuscule automatique de la première lettre a été désactivée. Cela peut notamment être utile lors de la saisie de mots de passe. Le contrôle de ce comportement se fait via l’argument KeyboardOptions.

Regardez le Composable ci-dessous :

fun TextFieldCapitalization() {
   var text by remember { mutableStateOf("") }
   TextField(
       value = text,
       onValueChange = { text = it },
       keyboardOptions = KeyboardOptions(
           capitalization = KeyboardCapitalization.Sentences,
           autoCorrect = false,
           keyboardType = KeyboardType.Ascii,
       ),
   )
}

L’image de gauche montre ce que l’on obtient lorsque la propriété de capitalisation est définie sur KeyboardCapitalization.None. Sur l’image de droite, on voit le résultat lorsque la valeur est définie sur KeyboardCapitalization.Sentences.

Démo de la capitalisation avec TextField

Claviers physiques

La deuxième situation concerne les claviers physiques. Dans les versions précédentes, lors de l’utilisation d’un clavier physique, appuyer sur Entrée entraînait l’ajout plusieurs nouvelles lignes, et appuyer sur Retour arrière déclenchait plusieurs suppressions. Avec la nouvelle version, ces événements sont désormais traités correctement.

Améliorations pour desktop

Stabilisation du framework de test

Cette version stabilise la prise en charge des tests sur Compose for Desktop. Jetpack Compose fournit un ensemble d’API de tests pour vérifier le comportement de votre code Compose. Ces API ont été portées sur desktop et la nouvelle version a supprimé toutes les limitations de disponibilité que l’on pouvait rencontrer auparavant, vous pouvez donc désormais écrire des tests d’interface utilisateur complets pour votre application.

Créons et testons une interface utilisateur simple pour explorer brièvement la fonctionnalité de test. Voici notre exemple de Composable :

@Composable
fun App() {
   var searchText by remember { mutableStateOf("cats") }
   val searchHistory = remember { mutableStateListOf() }


   Column(modifier = Modifier.padding(30.dp)) {
       TextField(
           modifier = Modifier.testTag("searchText"),
           value = searchText,
           onValueChange = {
               searchText = it
           }
       )
       Button(
           modifier = Modifier.testTag("search"),
           onClick = {
               searchHistory.add("You searched for: $searchText")
           }
       ) {
           Text("Search")
       }
       LazyColumn {
           items(searchHistory) {
               Text(
                   text = it,
                   fontSize = 20.sp,
                   modifier = Modifier.padding(start = 10.dp).testTag("attempt")
               )
           }
       }
   }
}

Cela crée une interface utilisateur simple qui enregistre les tentatives de recherche :

Application de recherche utilisée pour les tests

Vous remarquerez que Modifier.testTag est utilisé pour attribuer des noms à TextField, Button et aux éléments dans LazyColumn.

Nous pouvons alors manipuler l’interface utilisateur dans un test JUnit :

class SearchAppTest {
   @get:Rule
   val compose = createComposeRule()


   @Test
   fun `Should display search attempts`() {
       compose.setContent {
           App()
       }


       val testSearches = listOf("cats", "dogs", "fish", "birds")


       for (text in testSearches) {
           compose.onNodeWithTag("searchText").performTextReplacement(text)
           compose.onNodeWithTag("search").performClick()
       }


       val lastAttempt = compose
           .onAllNodesWithTag("attempt")
           .assertCountEquals(testSearches.size)
           .onLast()


       val expectedText = "You searched for: ${testSearches.last()}"
       lastAttempt.assert(hasText(expectedText))
   }
}

En utilisant la règle JUnit spécifique à Compose, nous :

  1. Définissons le contenu de l’interface utilisateur en tant qu’App Composable.
  2. Repérons le champ de texte et le bouton à l’aide de onNodeWithTag .
  3. Entrons à plusieurs reprises des exemples de valeurs dans le champ de texte et cliquons sur le bouton.
  4. Trouvons tous les nœuds de texte qui ont été générés via onAllNodesWithTag.
  5. Vérifions que le nombre actuel de nœuds de texte ont été créés et récupérons le dernier.
  6. Vérifions que cette dernière tentative contient le message attendu.

Amélioration de l’interopérabilité avec Swing

Cette version introduit une prise en charge expérimentale visant à améliorer le rendu des panneaux de composition dans les composants Swing. Cela évite les problèmes de rendu transitoires lorsque les panneaux sont affichés, masqués ou redimensionnés. Cela permet également d’avoir une superposition correcte lors de la combinaison de composants Swing et de panneaux compose. Un composant Swing peut désormais être affiché au-dessus ou en dessous d’un ComposePanel.

Regardez cet exemple :

fun main() {
   System.setProperty("compose.swing.render.on.graphics", "true")
   SwingUtilities.invokeLater {
       val composePanel = ComposePanel().apply {
           setContent {
               Box(modifier = Modifier.background(Color.Black).fillMaxSize())
           }
       }


       val popup = object : JComponent() { ... }


       val rightPanel = JLayeredPane().apply {
           add(composePanel)
           add(popup)
           ...
       }


       val leftPanel = JPanel().apply { background = CYAN }


       val splitter = JSplitPane(..., leftPanel,rightPanel)


       JFrame().apply {
           add(splitter)
           setSize(600, 600)
           isVisible = true
       }
   }
}

Ce code crée et affiche une JFrame Swing avec le contenu suivant :

  1. La JFrame contient un JSplitPane avec un séparateur vertical.
  2. Sur la gauche du volet divisé se trouve un JPanel standard de couleur cyan.
  3. À droite se trouve un JLayeredPane composé de deux couches :
    • Un ComposePanel contenant un composable Box de couleur noire
    • Un composant Swing personnalisé, dans lequel le texte « Popup » apparaît dans un rectangle blanc. Nous avons réalisé cela en remplaçant la méthode paintComponent.

Lorsque la propriété compose.swing.render.on.graphics est définie sur true, alors :

  • Le composant Swing personnalisé s’affiche au-dessus du Composable Box.
  • Il n’y a pas d’artefacts graphiques de transition lorsque le curseur est déplacé.

Démo de l'interopérabilité avec Swing fonctionnant

Si ce flag n’avait pas été défini, le composant personnalisé ne serait pas visible et il pourrait y avoir des artefacts de transition lors du déplacement du curseur :

Démonstration de l'interopérabilité Swing qui ne fonctionne pas

Faites-nous part de vos commentaires sur Compose Multiplatform et participez aux discussions sur Compose Multiplatform et Jetpack Compose en rejoignant le canal Slack #compose de Kotlin. Vous trouverez les discussions sur Compose Multiplatform for iOS dans le canal dédié #compose-ios.

Essayer Compose Multiplatform 1.5.0

Autres lectures et vidéos

Auteur de l’article original en anglais :

Delphine Massenhove

Garth Gilmour

image description

Discover more