Aucun résultat pour cette recherche.
Kotlin — Bases du langage
▶
data class
Équivalent d'un record Java — génère equals, hashCode, copy, toString automatiquement
Une data class est une classe dont le seul but est de stocker des données. Kotlin génère automatiquement equals(), hashCode(), toString() et copy().
kotlin
// Déclaration data class BullsAndCows(val bulls: Int, val cows: Int, val inexistent: Int) data class Fish(val filename: String) { val fishName: String get() = filename.substringBeforeLast(".") // propriété calculée } // Utilisation val result = BullsAndCows(bulls = 2, cows = 1, inexistent = 1) val copy = result.copy(bulls = 4) // copie avec modification val (b, c, i) = result // destructuring
Les val dans le constructeur sont immutables. Utilise var si tu veux modifier après création.
▶
companion object
Équivalent des méthodes static Java — méthodes de fabrique, constantes de classe
Le companion object est un objet singleton attaché à la classe. C'est là où on met les méthodes qu'on appellerait normalement static en Java. Très utilisé pour les méthodes de fabrique (factory) comme
buildFromResource ou loadFishes.kotlin
class LetterPicker(private val freqs: Map<Char, Int>) { fun pickLetters(n: Int): String { /* ... */ } companion object { // Méthode de fabrique — appelée comme LetterPicker.buildFromResource(...) fun buildFromResource(context: Context, resourceId: Int): LetterPicker { val map = mutableMapOf<Char, Int>() context.resources.openRawResource(resourceId) .bufferedReader().useLines { lines -> lines.forEach { line -> // parse "A 100" → map['A'] = 100 } } return LetterPicker(map) } // Pour Fish : charger depuis les assets fun loadFishes(context: Context): List<Fish> { return context.assets.list("fishes") ?.map { Fish(it) } ?: emptyList() } } } // Appel depuis l'extérieur val picker = LetterPicker.buildFromResource(this, R.raw.frequencies)
▶
typealias
Créer un synonyme pour un type existant
Permet de renommer un type pour plus de clarté. Le type reste le même, c'est juste un alias.
kotlin
typealias Dictionary = List<String> // Utilisation : Dictionary se comporte exactement comme List<String> fun loadDictionary(): Dictionary = listOf("abc", "def").sorted()
▶
Fonctions d'extension
Ajouter des méthodes à une classe existante sans la modifier
Une fonction d'extension s'écrit avec
NomDuType.maFonction(). À l'intérieur, this désigne l'objet sur lequel elle est appelée. Très utilisé dans les TPs pour String.containsAnagram, Context.loadDictionary, etc.kotlin
// Extension sur String fun String.containsAnagram(proposition: String): Boolean { val letters = this.toMutableList() for (c in proposition) { if (!letters.remove(c)) return false } return true } // Extension sur List générique fun <T> List<T>.pickRandomElements(n: Int): List<T> = this.shuffled().take(n) // Extension sur Context fun Context.loadDictionary(assetPath: String): Dictionary { val inputStream = assets.open(assetPath) return inputStream.bufferedReader().readLines().sorted() } // Appel : s'utilise comme une méthode normale "ESLRVEIOA".containsAnagram("VALORISEE") // true listOf(1,2,3,4,5).pickRandomElements(3) // liste de 3 éléments aléatoires
▶
Lambdas et fonctions d'ordre supérieur
Passer des fonctions en paramètre — forEach, map, filter, let
En Kotlin, les fonctions sont des valeurs qu'on peut passer en paramètre. Le type
(Int) -> Unit est une fonction qui prend un Int et ne retourne rien. Dans les composants Compose, les callbacks comme onChange ou onClick sont des lambdas.kotlin
// Types de fonctions lambda val f1: () -> Unit = { println("click") } // pas de param, rien val f2: (Int) -> Unit = { n -> println(n) } // 1 param nommé val f3: (Int) -> Int = { it * 2 } // it = param implicite val f4: (List<Int>) -> Unit = { list -> } // param complexe // Opérations classiques sur les listes val nums = listOf(1, 2, 3, 4, 5) nums.forEach { println(it) } // itérer nums.map { it * 2 } // transformer → [2,4,6,8,10] nums.filter { it > 2 } // filtrer → [3,4,5] nums.indices.forEach { i -> println(nums[i]) } // avec index nums.shuffled() // mélanger nums.take(3) // premiers éléments nums.chunked(2) // [[1,2],[3,4],[5]] // let : applique une lambda sur une valeur nullable someValue?.let { v -> println(v) } // Passer lambda en dernier paramètre (trailing lambda) fun doSomething(x: Int, action: (Int) -> Unit) { action(x) } doSomething(5) { v -> println(v) } // lambda hors des parenthèses
▶
Collections : List, Set, Map
Listes, ensembles, maps — mutable vs immutable
Immutable = on ne peut pas modifier après création. Mutable = on peut modifier. Dans Compose, préfère les immutables pour les états et crée des copies pour les modifier.
kotlin
// List val l1 = listOf(1, 2, 3) // immutable val l2 = mutableListOf(1, 2, 3) // mutable val l3 = l1.toMutableList() // copie mutable val l4 = l1 + listOf(4, 5) // concaténation → nouvelle liste val l5 = l1 + l1[0] // ajouter 1 élément → nouvelle liste // Set (pas de doublons) val s1 = setOf(1, 2, 3) val s2 = mutableSetOf<Fish>() s2.add(fish); s2.remove(fish); fish in s2 // contient // Map val m1 = mapOf('A' to 100, 'B' to 50) val m2 = mutableMapOf<Char, Int>() m2['A'] = 100; m2.getOrDefault('Z', 0) // Itérer une map m1.forEach { (letter, freq) -> println("$letter: $freq") } // BooleanArray (tableau primitif rapide) val visibility = BooleanArray(20) { false } visibility[5] = true val newVisibility = visibility.copyOf() // copie (important pour Compose!)
Dans Compose, Compose ne détecte pas les modifications internes d'un tableau. Toujours créer une copie :
array.copyOf() puis réaffecter.
▶
Null safety — ?, ?., ?:, !!
Gérer les valeurs potentiellement nulles en Kotlin
kotlin
var name: String? = null // nullable (avec ?) var name2: String = "ok" // non-nullable (jamais null) name?.length // safe call : null si name est null name?.length ?: 0 // elvis : 0 si null name!!.length // force (crash si null !) name?.let { println(it) } // exécute le bloc si non-null // Depuis assets.list() qui retourne Array<String>? context.assets.list("fishes")?.map { Fish(it) } ?: emptyList()
▶
Coroutines Kotlin
suspend, delay, withContext — pour les opérations asynchrones
Les coroutines permettent de faire des opérations asynchrones (réseau, fichiers, temporisations) sans bloquer l'UI. Dans Compose on les lance avec
LaunchedEffect.kotlin
// Fonction suspend = peut être suspendue sans bloquer le thread suspend fun loadImageFromAssets(context: Context, path: String): ImageBitmap { return withContext(Dispatchers.IO) { // changer de thread (IO pour fichiers) context.assets.open(path).use { BitmapFactory.decodeStream(it).asImageBitmap() } } } // Dans LaunchedEffect — boucle avec delay (ex: barre de chargement) LaunchedEffect(Unit) { var elapsed = 0L while (elapsed < duration) { delay(20L) // attendre 20ms elapsed += 20L fillRatio = elapsed.toFloat() / duration } onLoaded() // prévenir le parent }
Jetpack Compose — Bases
▶
@Composable — Créer un composant
Signature d'un composant, Modifier, @Preview
Toute fonction annotée
@Composable est un composant. Convention : le dernier paramètre est toujours modifier: Modifier = Modifier, et on le passe au composant racine de la fonction.kotlin
// Composant simple (stateless) @Composable fun DigitDisplayer(value: Int, fontSize: TextUnit, modifier: Modifier = Modifier) { Text( text = value.toString(16), // affichage hexa fontSize = fontSize, modifier = modifier .background(colorForDigit(value)) .padding(4.dp) ) } // Preview — composant sans argument pour prévisualiser @Preview @Composable fun DigitDisplayerPreview() { Column { repeat(2) { Row { (0..15).forEach { DigitDisplayer(it, 30.sp) } } } } } // Appel dans onCreate (activité) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { GameManager() // composant racine } }
▶
Text — Afficher du texte
fontSize, fontWeight, color, textAlign
kotlin
Text( text = "Bonjour !", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = Color.White, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth() ) // Avec une variable d'état Text(text = "Compteur : $counter") Text(text = "${result.bulls} 🐂 ${result.cows} 🐄") Text(text = "?".repeat(codeLength), ...) // afficher ???
▶
Button — Bouton cliquable
onClick, enabled, contenu via trailing lambda
kotlin
Button( onClick = { counter++ }, enabled = !validated, // désactivé si validé modifier = Modifier.fillMaxWidth() ) { Text("Valider") } // TextButton (sans fond) TextButton(onClick = { onValidation(result) }) { Text("???", fontSize = fontSize) }
▶
Image — Afficher une image
Depuis drawable ou depuis les assets
kotlin
// Depuis res/drawable Image( painter = painterResource(id = R.drawable.goldroid), contentDescription = "Goldroid", contentScale = ContentScale.Fit, modifier = Modifier.fillMaxSize() ) // Depuis les assets (chargement asynchrone) @Composable fun getAssetImage(path: String): ImageBitmap? { val context = LocalContext.current var bitmap by remember { mutableStateOf<ImageBitmap?>(null) } LaunchedEffect(path) { bitmap = loadImageFromAssets(context, path) } return bitmap } // Utilisation dans FishDisplayer val img = getAssetImage("fishes/${fish.filename}") if (img != null) { Image(bitmap = img, contentDescription = fish.fishName, ...) }
▶
Slider — Curseur de sélection
Sélectionner une valeur dans une plage
kotlin
var nbPairs by remember { mutableFloatStateOf(8f) } Slider( value = nbPairs, onValueChange = { nbPairs = it }, valueRange = 2f..20f, steps = 17, // nb de crans intermédiaires modifier = Modifier.fillMaxWidth() ) Text("${nbPairs.toInt()} paires")
Layouts — Mise en page
▶
Row et Column — Disposition linéaire
Horizontal (Row) ou vertical (Column), avec alignment et arrangement
kotlin
// Column = empilement vertical Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.SpaceBetween, horizontalAlignment = Alignment.CenterHorizontally ) { Text("En haut") Text("Au milieu") Text("En bas") } // Row = disposition horizontale Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text("Gauche") Spacer(Modifier.weight(1f)) // prend tout l'espace disponible Text("Droite") } // Arrangement options // Arrangement.Start / End / Center / SpaceBetween / SpaceAround / SpaceEvenly // Alignment.Start / End / CenterHorizontally / CenterVertically / Top / Bottom
▶
Box — Superposition d'éléments
Empiler des composants les uns par-dessus les autres
Utilisé pour superposer une légende sur une image, ou positionner des éléments en absolu dans un espace.
kotlin
Box( modifier = Modifier.fillMaxSize().clickable { onClick() } ) { // Arrière-plan Image(painter = painterResource(R.drawable.goldroid), ...) // Avant-plan — positionné en bas Text( text = message, modifier = Modifier .align(Alignment.BottomCenter) // positionner dans la Box .alpha(0.7f) .background(Color.Black) .fillMaxWidth() ) }
▶
BoxWithConstraints — Connaître la taille disponible
Calculer le nombre de colonnes/lignes d'une grille selon l'espace disponible
Utilisé dans GoldroidGrid et FishGrid pour calculer combien de poissons mettre par rangée selon la largeur de l'écran.
kotlin
BoxWithConstraints( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { val maxWidth = this.maxWidth val maxHeight = this.maxHeight val fishSize = 300.dp val nbCols = (maxWidth / fishSize).toInt().coerceAtLeast(1) val nbRows = (maxHeight / fishSize).toInt().coerceAtLeast(1) Column { repeat(nbRows) { row -> Row { repeat(nbCols) { col -> FishDisplayer( fish = fishes[row * nbCols + col], modifier = Modifier.size(fishSize) ) } } } } }
▶
LazyColumn — Liste défilante
Équivalent RecyclerView — charge les items paresseusement
Utilise
items() pour afficher une liste. L'avantage sur Column : seuls les items visibles sont rendus. Supporte le défilement nativement.kotlin
LazyColumn(modifier = Modifier.fillMaxSize()) { // items() avec une liste items(fishes) { fish -> FishDisplayer(fish = fish) } // items avec chunked (pour une grille défilante) items(fishes.chunked(nbCols)) { fishGroup -> Row { fishGroup.forEach { fish -> FishWithAnagramDisplayer( fish = fish, modifier = Modifier.size(fishSize) ) } } } // item() pour un élément unique (ex: en-tête) item { Text("En-tête", fontWeight = FontWeight.Bold) } } // Column scrollable (alternative simple) Column(Modifier.verticalScroll(rememberScrollState())) { // ... items }
▶
Modifier — Styliser et positionner
padding, size, background, weight, clickable, alpha…
Le Modifier est chaînable : l'ordre compte !
padding avant background met le padding à l'intérieur du fond coloré, après = à l'extérieur.kotlin
Modifier .fillMaxSize() // prend tout l'espace parent .fillMaxWidth() // toute la largeur .fillMaxHeight() // toute la hauteur .size(300.dp) // largeur + hauteur fixe .width(200.dp) // largeur fixe .height(50.dp) // hauteur fixe .weight(1f) // dans Row/Column: prend 1 part de l'espace .weight(2f) // prend 2 parts (le double) .padding(8.dp) // marges intérieures .padding(horizontal = 16.dp, vertical = 4.dp) .background(Color.Blue) // couleur de fond .background(Color(0xFF123456)) // couleur hexa .clickable { doSomething() } // rend cliquable .alpha(0.5f) // transparence (0=invisible, 1=opaque) .border(2.dp, Color.Black) .aspectRatio(1f) // carré .aspectRatio(16f/9f) // 16:9 .wrapContentSize() // taille minimale // Gestion des clics longs — pour incrémentation continue Modifier.pointerInput(Unit) { detectTapGestures( onTap = { onChange((value + 1) % base) }, onLongPress = { /* ... */ } ) }
Gestion de l'état
▶
remember + mutableStateOf — État local
Mémoriser une valeur entre les recompositions
remember conserve la valeur entre les recompositions. Sans
remember, la variable serait réinitialisée à chaque recomposition. mutableStateOf rend la valeur observable : quand elle change, Compose recompose.kotlin
// Syntaxe de base val counter by remember { mutableStateOf(0) } // ou sans "by" (besoin de .value alors) val counter = remember { mutableStateOf(0) } counter.value++; println(counter.value) // Variants optimisés var count by remember { mutableIntStateOf(0) } var ratio by remember { mutableFloatStateOf(0f) } var letters by remember { mutableStateOf<List<Char>>(emptyList()) } var bitmap by remember { mutableStateOf<ImageBitmap?>(null) } // Exemple complet dans GoldroidCounter @Composable fun GoldroidCounter(modifier: Modifier = Modifier) { var counter by remember { mutableIntStateOf(0) } var startTime by remember { mutableStateOf<Long?>(null) } GoldroidDisplayer( message = "Clics : $counter", onClick = { if (startTime == null) startTime = SystemClock.elapsedRealtime() counter++ }, modifier = modifier.alpha(1f - counter / 100f) ) }
▶
derivedStateOf — État calculé
Calculer un état à partir d'autres états (évite les recompositions inutiles)
Utilisé dans MemoryBoardManager pour calculer le tableau de visibilité des cartes à partir des ensembles "paires trouvées" et "cartes retournées".
kotlin
var foundPairs by remember { mutableStateOf(setOf<Fish>()) } var flippedCards by remember { mutableStateOf(setOf<MemoryCard>()) } // Recalculé automatiquement quand foundPairs ou flippedCards changent val visibility by remember { derivedStateOf { BooleanArray(cards.size) { i -> cards[i].fish in foundPairs || cards[i] in flippedCards } } }
▶
LaunchedEffect — Effets et coroutines
Exécuter du code asynchrone quand un état change
Se lance quand les clés changent. Avec
Unit comme clé = une seule fois au démarrage. Avec une variable comme clé = se relance à chaque changement de cette variable.kotlin
// Une seule fois au démarrage LaunchedEffect(Unit) { bitmap = loadImageFromAssets(context, path) } // Se relance si "path" change LaunchedEffect(path) { bitmap = loadImageFromAssets(context, path) } // Barre de chargement animée LaunchedEffect(Unit) { var elapsed = 0L while (elapsed < duration) { delay(20L) elapsed += 20L fillRatio = elapsed.toFloat() / duration } onLoaded() } // Temporisation pour cacher les cartes mémoire LaunchedEffect(flippedCards) { if (flippedCards.size == 2) { val (c1, c2) = flippedCards.toList() if (c1.fish == c2.fish) { foundPairs = foundPairs + c1.fish // trouvé ! flippedCards = emptySet() } else { delay(2000L) // attendre 2s flippedCards = emptySet() // remasquer } } }
Pour "relancer" une LaunchedEffect (ex: réinitialiser la barre), change sa clé — par exemple passe un entier
sessionId qui s'incrémente.
▶
rememberSaveable / ViewModel — Survivre à la rotation
Conserver l'état quand l'écran tourne
kotlin
// Option 1 : rememberSaveable (objets Serializable seulement) var score by rememberSaveable { mutableIntStateOf(0) } // Rendre une data class Serializable @Serializable data class MemoryCard(val fish: Fish, val sample: Int) : Serializable // Option 2 : ViewModel (recommandé) class BoardViewModel : ViewModel() { var foundPairs by mutableStateOf(setOf<Fish>()) var attempts by mutableIntStateOf(0) } // Dans le composant — viewModel est recréé si besoin ou récupéré @Composable fun MemoryBoardManager( viewModel: BoardViewModel = viewModel() ) { // viewModel.foundPairs, viewModel.attempts... }
▶
key { } — Contrôler l'identité des composants
Éviter les recompositions inutiles lors d'un shuffle
Par défaut, Compose identifie les composants par leur position dans la boucle. En utilisant
key, on donne une identité unique → Compose déplace le composant au lieu de le recomposer.kotlin
// Sans key : position 0 reste LetterDisplayer 0, même si lettre change letters.forEach { letter -> LetterDisplayer(letter = letter) } // Avec key : chaque LetterDisplayer est lié à un identifiant unique // (ici un index car la même lettre peut apparaître plusieurs fois) letters.forEachIndexed { index, (letter, uniqueId) -> key(uniqueId) { LetterDisplayer(letter = letter) // conserve son état (couleur) lors du shuffle } }
Patterns architecturaux
▶
Stateless vs Stateful — Hoisting d'état
Le composant affiche, le parent gère l'état — pattern fondamental
Dans les TPs, tous les Selector et Displayer sont stateless : ils reçoivent la valeur et notifient le parent via un callback. Le parent appelle
onChange et met à jour son état, ce qui provoque une recomposition.kotlin
// STATELESS : composant "bête" — affiche ce qu'on lui donne @Composable fun DigitSelector( value: Int, base: Int, fontSize: TextUnit, onChange: (Int) -> Unit // callback vers le parent ) { DigitDisplayer( value = value, modifier = Modifier.clickable { onChange((value + 1) % base) } ) } // STATEFUL : composant "intelligent" — gère son propre état @Composable fun CodeSelectorParent() { var code by remember { mutableStateOf(listOf(0, 0, 0, 0)) } CodeSelector( code = code, base = 10, fontSize = 30.sp, onChange = { newCode -> code = newCode } // mise à jour état ) } // Pattern CodeSelector @Composable fun CodeSelector( code: List<Int>, base: Int, fontSize: TextUnit, onChange: (List<Int>) -> Unit ) { Row { code.forEachIndexed { index, digit -> DigitSelector( value = digit, base = base, fontSize = fontSize, onChange = { newVal -> val newCode = code.toMutableList() newCode[index] = newVal onChange(newCode) // propager au parent avec nouvelle liste } ) } } }
▶
CompositionLocal — Partager des données dans l'arbre
Éviter de passer base/fontSize à chaque composant
kotlin
// Définir la data class et le local data class GameConfig(val base: Int, val fontSize: TextUnit) val LocalGameConfig = compositionLocalOf { GameConfig(10, 24.sp) } // Fournir la valeur dans un composant parent CompositionLocalProvider(LocalGameConfig provides GameConfig(base, fontSize)) { GameGrid(sessionId = sessionId, codeLength = codeLength, onEnd = onEnd) } // Lire la valeur dans n'importe quel composant enfant val config = LocalGameConfig.current Text("Base : ${config.base}", fontSize = config.fontSize)
▶
Orientation portrait / paysage
Adapter l'UI selon l'orientation de l'écran
Dans le sujet de l'examen, l'écran de fin doit afficher les éléments côte à côte en paysage et empilés en portrait.
kotlin
val configuration = LocalConfiguration.current val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE if (isLandscape) { Row(Modifier.fillMaxSize()) { CowImage(Modifier.weight(1f)) ResultText(Modifier.weight(1f)) } } else { Column(Modifier.fillMaxSize()) { CowImage(Modifier.weight(0.6f)) ResultText(Modifier.weight(0.4f)) } }
▶
Machine à états — Gérer les écrans du jeu
Alterner entre chargement, jeu, fin de partie
Pattern utilisé dans GameManager et MemoryGame pour afficher différents composants selon la phase du jeu.
kotlin
enum class GamePhase { LOADING, SETTINGS, PLAYING, WON, LOST } // ou sealed class pour transporter des données sealed class GameState { object Loading : GameState() object Playing : GameState() data class End(val won: Boolean, val time: Long) : GameState() } @Composable fun GameManager(modifier: Modifier = Modifier) { var phase by remember { mutableStateOf<GameState>(GameState.Loading) } var sessionId by remember { mutableIntStateOf(0) } when (val p = phase) { is GameState.Loading -> GoldroidLoadScreen(onLoaded = { phase = GameState.Playing }) is GameState.Playing -> GameGrid( sessionId = sessionId, codeLength = CODE_LENGTH, onEnd = { won, time -> phase = GameState.End(won, time) } ) is GameState.End -> EndScreen( won = p.won, elapsedTime = p.time, onRestart = { sessionId++ // nouvelle partie phase = GameState.Playing } ) } }
Spécifique examen — Composants clés du sujet AN dernier
▶
DigitDisplayer — Afficher un chiffre coloré
Couleur de fond liée au chiffre, affichage hexa
1er exercice de l'ancien sujet (/3). Afficher un chiffre (0–15) en hexa avec couleur de fond déterministe selon la valeur.
kotlin
// Tableau de couleurs — 1 couleur par chiffre 0..15 val DIGIT_COLORS = listOf( Color(0xFFE74C3C), Color(0xFFE67E22), Color(0xFFF1C40F), Color(0xFF2ECC71), Color(0xFF1ABC9C), Color(0xFF3498DB), Color(0xFF9B59B6), Color(0xFF34495E), Color(0xFF95A5A6), Color(0xFFD35400), Color(0xFF27AE60), Color(0xFF16A085), Color(0xFF2980B9), Color(0xFF8E44AD), Color(0xFF2C3E50), Color(0xFF7F8C8D) ) @Composable fun DigitDisplayer( value: Int, fontSize: TextUnit, modifier: Modifier = Modifier ) { Box( modifier = modifier .background(DIGIT_COLORS[value]) .padding(4.dp), contentAlignment = Alignment.Center ) { Text( text = value.toString(16), // hexa : 0→"0", 10→"a", 15→"f" fontSize = fontSize, color = Color.White ) } }
▶
pickSecretCode — Tirer un code sans répétition
Générer une liste de chiffres uniques tirés au sort
Exercice du sujet (/3). Code secret de n chiffres distincts entre 0 et base-1. La version courte tient en 42 caractères.
kotlin
// Version complète fun pickSecretCode(n: Int, base: Int): List<Int> { require(n <= base) { "n must be <= base" } return (0 until base).shuffled().take(n) } // Version courte (42 caractères dans le body) : fun pickSecretCode(n: Int, base: Int) = (0 until base).shuffled().take(n)
▶
CodeProposer — Saisir et valider une proposition
Affiche CodeSelector + bouton validation + résultat taureaux/vaches
Exercice (/3). Composant stateless : si validated=false → bouton "???". Après validation → affiche 🐂🐄🚫.
kotlin
@Composable fun CodeProposer( referenceCode: List<Int>, proposedCode: List<Int>, base: Int, fontSize: TextUnit, validated: Boolean, onProposedCodeChange: (List<Int>) -> Unit, onValidation: (BullsAndCows) -> Unit ) { Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { // Sélecteur de code à gauche (prend tout l'espace restant) CodeSelector( code = proposedCode, base = base, fontSize = fontSize, onChange = if (!validated) onProposedCodeChange else { _ -> }, modifier = Modifier.weight(1f) ) // À droite : bouton ou résultat if (!validated) { TextButton(onClick = { val result = computeBullsAndCows(referenceCode, proposedCode) onValidation(result) }) { Text("???", fontSize = fontSize) } } else { val result = computeBullsAndCows(referenceCode, proposedCode) Text( text = "${"🐂".repeat(result.bulls)}${"🐄".repeat(result.cows)}${"🚫".repeat(result.inexistent)}", fontSize = fontSize ) } } }
▶
GameGrid — Grille de jeu Bulls & Cows
Liste scrollable de CodeProposer + chronomètre + bouton abandon
Exercice (/4). sessionId change → nouvelle partie. Chrono en temps réel. Bouton abandon. LazyColumn scrollable.
kotlin
@Composable fun GameGrid( sessionId: Int, codeLength: Int, onEnd: (Boolean, Long) -> Unit // won, elapsedMs ) { val base = 10 // Relancer à chaque nouveau sessionId val secretCode = remember(sessionId) { pickSecretCode(codeLength, base) } val startTime = remember(sessionId) { SystemClock.elapsedRealtime() } // Liste des propositions : liste de (code, validated, result?) var proposals by remember(sessionId) { mutableStateOf(listOf(listOf(0, 0, 0, 0))) } var validated by remember(sessionId) { mutableStateOf(listOf(false)) } // Chronomètre en temps réel var elapsed by remember { mutableLongStateOf(0L) } LaunchedEffect(sessionId) { while (true) { delay(1000L) elapsed = (SystemClock.elapsedRealtime() - startTime) / 1000L } } Column(Modifier.fillMaxSize()) { Row(Modifier.fillMaxWidth()) { Text("⏱ ${elapsed}s", Modifier.weight(1f)) Button(onClick = { onEnd(false, SystemClock.elapsedRealtime() - startTime) }) { Text("Abandonner") } } LazyColumn(Modifier.weight(1f)) { itemsIndexed(proposals) { index, code -> CodeProposer( referenceCode = secretCode, proposedCode = code, base = base, fontSize = 24.sp, validated = validated[index], onProposedCodeChange = { newCode -> proposals = proposals.toMutableList().also { it[index] = newCode } }, onValidation = { result -> validated = validated.toMutableList().also { it[index] = true } if (result.bulls == codeLength) { onEnd(true, SystemClock.elapsedRealtime() - startTime) } else { proposals = proposals + listOf(List(codeLength) { 0 }) validated = validated + listOf(false) } } ) } } } }
▶
LoadBar / barre de progression
Barre qui se remplit progressivement avec weight
kotlin
@Composable fun LoadBar( fillRatio: Float, // entre 0f et 1f backgroundColor: Color = Color.White, foregroundColor: Color = Color.Blue, modifier: Modifier = Modifier ) { Row(modifier) { // Partie remplie if (fillRatio > 0f) Box(Modifier.weight(fillRatio).fillMaxHeight().background(foregroundColor)) // Partie vide if (fillRatio < 1f) Box(Modifier.weight(1f - fillRatio).fillMaxHeight().background(backgroundColor)) } } // GoldroidLoadScreen avec animation @Composable fun GoldroidLoadScreen( duration: Long = 5000L, onLoaded: () -> Unit, modifier: Modifier = Modifier ) { var fillRatio by remember { mutableFloatStateOf(0f) } LaunchedEffect(Unit) { var elapsed = 0L while (elapsed < duration) { delay(20L) elapsed += 20L fillRatio = elapsed.toFloat() / duration } onLoaded() } Column(modifier.fillMaxSize()) { Image(painterResource(R.drawable.goldroid), "loading", modifier = Modifier.weight(0.9f).fillMaxWidth()) LoadBar(fillRatio = fillRatio, modifier = Modifier.weight(0.1f).fillMaxWidth()) } }
▶
AmountBar — Barre de quantité verticale
Barre verticale remplie de bas en haut selon une valeur 0–1
Exercice ♣ (/3). Barre verticale blanche remplie en noir de bas en haut.
kotlin
@Composable fun AmountBar(value: Float, modifier: Modifier = Modifier) { Column( modifier .background(Color.White) .border(1.dp, Color.Gray) ) { // Partie vide en haut if (value < 1f) Box(Modifier.weight(1f - value).fillMaxWidth().background(Color.White)) // Partie remplie en bas if (value > 0f) Box(Modifier.weight(value).fillMaxWidth().background(Color.Black)) } }
Divers — Utilitaires
▶
Toast — Message court
Afficher une notification temporaire
kotlin
val context = LocalContext.current // Dans un callback (pas dans la composition directement) Toast.makeText(context, "Bravo !", Toast.LENGTH_SHORT).show() // Dans un bouton par exemple Button(onClick = { Toast.makeText(context, "Félicitations !", Toast.LENGTH_LONG).show() }) { Text("Valider") }
▶
Intent — Lancer une autre activité
Passer des données à une autre Activity
kotlin
// Lancer une activité avec des données fun startAnagramActivity(context: Context, fish: Fish) { val intent = Intent() intent.setClassName("com.example.myapp", "com.example.myapp.AnagramActivity") intent.putExtra("word", fish.fishName) intent.putExtra("anagram", fish.fishName.toCharArray().toMutableList().shuffled() .joinToString("")) try { context.startActivity(intent) } catch (e: Exception) { Toast.makeText(context, "App non trouvée", Toast.LENGTH_SHORT).show() } } // Lire les données dans l'activité de destination (dans onCreate) val word = intent.getStringExtra("word") ?: "default" val anagram = intent.getStringExtra("anagram") ?: ""
▶
Générer une couleur aléatoire
Couleurs pastel (composantes R,G,B entre 128–255)
kotlin
fun pickRandomColor(): Color { val r = (128..255).random() val g = (128..255).random() val b = (128..255).random() return Color(r, g, b) } // Dans LetterDisplayer — couleur fixée à l'installation val backgroundColor by remember { mutableStateOf(pickRandomColor()) }
▶
Mesurer le temps écoulé
SystemClock.elapsedRealtime() pour chronomètre
kotlin
// Temps depuis le démarrage du système (en ms) — ne se réinitialise pas val start = SystemClock.elapsedRealtime() // ... plus tard val elapsed = SystemClock.elapsedRealtime() - start // en millisecondes val elapsedSeconds = elapsed / 1000L // Stocker comme état nullable (null = pas encore commencé) var startTime by remember { mutableStateOf<Long?>(null) } // Au premier clic : if (startTime == null) startTime = SystemClock.elapsedRealtime()
▶
Recherche dans une liste triée
binarySearch, binarySearchBy pour trouver rapidement
kotlin
// Liste doit être TRIÉE pour binarySearch val dictionary: List<String> = loadDictionary().sorted() // binarySearch retourne l'index si trouvé, sinon un nombre négatif val found = dictionary.binarySearch("VALORISEE") >= 0 // binarySearchBy (pour objets complexes) val cumFreqs: List<CumulatedFrequency> = /* ... liste triée par value */ val idx = cumFreqs.binarySearchBy(pickedNumber) { it.value } // Si idx < 0, la lettre est à l'index -(idx+1) fun List<CumulatedFrequency>.findLetter(n: Int): Char { val idx = binarySearchBy(n) { it.value } return if (idx >= 0) this[idx].letter else this[-(idx + 1)].letter }
▶
Manipulations de String
toCharArray, split, joinToString, substringBeforeLast
kotlin
val s = "GOLDROID" s.toCharArray() // → CharArray ['G','O','L','D','R','O','I','D'] s.toList() // → List<Char> s.length // → 8 s[0] // → 'G' s.lowercase() // → "goldroid" s.uppercase() // → "GOLDROID" s.substringBeforeLast(".") // "file.jpeg" → "file" s.repeat(3) // → "GOLDROIDGOLDROIDGOLDROID" "🐂".repeat(result.bulls) // → "🐂🐂" si bulls=2 listOf('A', 'B').joinToString("") // → "AB" // toString(base) pour convertir en hexadécimal 10.toString(16) // → "a" 15.toString(16) // → "f" 255.toString(16) // → "ff"
▶
LocalContext — Obtenir le Context dans Compose
Accéder aux assets, resources, etc. depuis un composant
kotlin
@Composable fun MonComposant() { val context = LocalContext.current // Charger les fishes depuis assets val fishes = remember { context.assets.list("fishes")?.map { Fish(it) } ?: emptyList() } // Lire une ressource raw context.resources.openRawResource(R.raw.frequencies) // Lancer une activité context.startActivity(intent) }
▶
var vs val — Variables
val = immuable, var = mutable
kotlin
val name = "Goldroid" // immuable — ne peut pas être réassigné var count = 0 // mutable — peut être modifié count++; count = 5 // Type inféré automatiquement val list = listOf(1, 2, 3) // List<Int> inféré val map: Map<Char, Int> = mutableMapOf() // type explicite // Constantes top-level const val CODE_LENGTH = 4 const val BASE = 10
▶
Setter personnalisé
Exécuter du code lors de l'affectation d'une variable
Utilisé dans le TP Anagrammes pour mettre à jour automatiquement le TextView quand
bestAnagrams change.kotlin
var bestAnagrams = listOf<String>() set(value) { field = value // "field" = la valeur interne // Mise à jour automatique du TextView textView.text = value.joinToString("\n") } // Propriété calculée (get uniquement) val fishName: String get() = filename.substringBeforeLast(".")