☕ KOTLIN
Variables & Types
Kotlinval nom: String = "Adrien" // immuable (lecture seule)
var age: Int = 30 // mutable
val pi = 3.14 // inférence de type
// Types primitifs
Int, Long, Float, Double, Boolean, Char, String, Byte, Short
// Constantes (compile-time)
const val MAX = 100 // top-level ou dans companion objectNull Safety
Kotlinvar s: String? = null // nullable
val len = s?.length // safe call → null si s est null
val len = s?.length ?: 0 // Elvis operator → valeur par défaut
val len = s!!.length // force unwrap → NullPointerException si null
// Cast sécurisé
val n = value as? Int // null si cast impossibleFonctions
Kotlin// Basique
fun add(a: Int, b: Int): Int = a + b
// Paramètres nommés & défaut
fun greet(name: String = "World") = "Hello, $name!"
// Vararg
fun sum(vararg nums: Int) = nums.sum()
// Extension function
fun String.isPalindrome() = this == this.reversed()
"racecar".isPalindrome() // true
// Infix
infix fun Int.plusTen(x: Int) = this + x + 10
5 plusTen 3 // 18
// Inline (évite l'overhead des lambdas)
inline fun <T> measure(block: () -> T): T { ... }
// Lambdas
val double: (Int) -> Int = { x -> x * 2 }
val square = { x: Int -> x * x }
listOf(1,2,3).map { it * 2 } // it = paramètre implicite unique
// Fonction d'ordre supérieur
fun transform(n: Int, fn: (Int) -> Int) = fn(n)Strings
Kotlinval name = "Adrien"
val msg = "Bonjour $name !" // template
val calc = "Résultat : ${1 + 2}" // expression
val multi = """
Ligne 1
Ligne 2
""".trimIndent()Contrôle de flux
Kotlin// if expression
val max = if (a > b) a else b
// when expression (switch amélioré)
when (x) {
1 -> "un"
2, 3 -> "deux ou trois"
in 4..10 -> "entre 4 et 10"
is String -> "c'est une String"
else -> "autre"
}
// Ranges
for (i in 1..5) { } // 1 à 5 inclus
for (i in 1 until 5) { } // 1 à 4
for (i in 5 downTo 1 step 2) { } // 5, 3, 1Classes
Kotlin// Classe standard
class Person(val name: String, var age: Int) {
init { println("Créé : $name") }
fun greet() = "Bonjour, $name"
}
// Data class (equals, hashCode, copy, toString automatiques)
data class User(val id: Int, val name: String)
val u1 = User(1, "Alice")
val u2 = u1.copy(name = "Bob")
// Sealed class (héritage restreint, idéal pour les états UI)
sealed class UiState {
object Loading : UiState()
data class Success(val data: List<String>) : UiState()
data class Error(val message: String) : UiState()
}
// Enum class
enum class Direction(val label: String) {
NORD("Nord"), SUD("Sud"), EST("Est"), OUEST("Ouest");
fun opposite() = when (this) {
NORD -> SUD; SUD -> NORD; EST -> OUEST; OUEST -> EST
}
}
// Object (singleton)
object Database { val url = "jdbc:sqlite:..." }
// Companion object
class MyClass {
companion object {
const val TAG = "MyClass"
fun create() = MyClass()
}
}
// Interface
interface Drawable {
fun draw()
fun resize(factor: Float) = println("Resize x$factor") // impl. par défaut
}Héritage
Kotlinopen class Animal(val name: String) {
open fun sound() = "..."
}
class Dog(name: String) : Animal(name) {
override fun sound() = "Woof"
}
// Délégation d'interface
class Logger(private val base: Drawable) : Drawable by baseGenerics
Kotlinfun <T> wrap(item: T): List<T> = listOf(item)
// Contrainte
fun <T : Comparable<T>> max(a: T, b: T) = if (a > b) a else b
// Covariance / contravariance
class Box<out T>(val value: T) // out = producteur (covariant)
class Sink<in T> { fun consume(v: T) {} } // in = consommateurScope Functions
Kotlin| Fonction | Contexte | Retourne | Usage typique |
|---|---|---|---|
| let | it | lambda result | null-check, transformation |
| run | this | lambda result | init + calcul |
| with | this | lambda result | opérations groupées |
| apply | this | l'objet | configuration |
| also | it | l'objet | effets de bord (log) |
val user = User(1, "Alice").apply { name = "Bob" }
val len = str?.let { it.length } ?: 0
val result = with(user) { "$name a ${age} ans" }Collections & Opérateurs
Kotlinval list = listOf(1, 2, 3, 4, 5) // immuable
val mList = mutableListOf(1, 2, 3)
val map = mapOf("a" to 1, "b" to 2)
val set = setOf("x", "y")
list.map { it * 2 } // [2, 4, 6, 8, 10]
list.filter { it > 2 } // [3, 4, 5]
list.reduce { acc, i -> acc + i } // 15
list.fold(10) { acc, i -> acc + i } // 25
list.flatMap { listOf(it, it * 10) } // [1, 10, 2, 20, ...]
list.groupBy { if (it % 2 == 0) "pair" else "impair" }
list.sortedBy { it }
list.partition { it > 3 } // Pair([4,5], [1,2,3])
list.zip(listOf("a","b","c")) // [(1,a), (2,b), (3,c)]
list.any { it > 4 } // true
list.all { it > 0 } // true
list.none { it > 10 } // true
list.count { it % 2 == 0 } // 2
list.first { it > 2 } // 3
list.firstOrNull { it > 10 } // null
list.take(3) // [1, 2, 3]
list.drop(2) // [3, 4, 5]
list.chunked(2) // [[1,2],[3,4],[5]]
list.windowed(3) // [[1,2,3],[2,3,4],[3,4,5]]Coroutines
Kotlin// Scopes
GlobalScope.launch { } // déconseillé en prod
viewModelScope.launch { } // lié au ViewModel
lifecycleScope.launch { } // lié au cycle de vie
CoroutineScope(Dispatchers.IO).launch { }
// Dispatchers
Dispatchers.Main // UI thread
Dispatchers.IO // I/O (réseau, fichiers)
Dispatchers.Default // CPU intensif
// Lancement
launch { } // fire & forget → Job
async { } // retourne Deferred<T>
val result = async { fetchData() }.await()
// Suspension
suspend fun fetchUser(): User {
return withContext(Dispatchers.IO) { api.getUser() }
}
// Job & annulation
val job = launch { ... }
job.cancel()
job.join() // attend la fin
// Timeout
withTimeout(5000L) { fetchData() }
withTimeoutOrNull(5000L) { fetchData() } // null si timeout
// Exception handling
val handler = CoroutineExceptionHandler { _, e -> Log.e(TAG, e.message) }
launch(handler) { ... }Flow
Kotlin// Cold flow (démarre à la collecte)
fun numbers(): Flow<Int> = flow {
for (i in 1..5) { delay(100); emit(i) }
}
// Collecte
viewModelScope.launch {
numbers()
.filter { it % 2 == 0 }
.map { it * 10 }
.collect { value -> println(value) }
}
// StateFlow (hot, toujours une valeur courante)
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state.asStateFlow()
_state.value = UiState.Success(data)
// SharedFlow (hot, broadcast)
private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events.asSharedFlow()
_events.emit(Event.Navigate)
// Opérateurs
flow.map { }
flow.filter { }
flow.onEach { }
flow.catch { e -> }
flow.onStart { emit(Loading) }
flow.debounce(300) // utile pour la recherche
flow.distinctUntilChanged()
flow.take(3)
flow.combine(otherFlow) { a, b -> a + b }
flow.flatMapLatest { query -> searchApi(query) } // annule si nouvelle valeur🚀 JETPACK COMPOSE
Bases des Composables
Compose@Composable
fun Greeting(name: String) {
Text(text = "Bonjour, $name !")
}
// Règles clés :
// - Nommés en PascalCase
// - Pas d'effets de bord directs (utiliser les side effects)
// - Appelables uniquement depuis un autre @Composable ou setContent {}
// - Peuvent se recomposer à tout moment
// Point d'entrée Activity
setContent {
MyAppTheme {
Greeting("Adrien")
}
}State & Recomposition
Compose// remember : survit aux recompositions, pas aux changements de config
var count by remember { mutableStateOf(0) }
// rememberSaveable : survit aussi aux changements de config (rotation)
var text by rememberSaveable { mutableStateOf("") }
// State hoisting : remonter l'état pour le partager
@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
Button(onClick = onIncrement) { Text("$count") }
}
// collectAsState : observe un Flow/StateFlow
val uiState by viewModel.state.collectAsState()
// produceState : convertit des données non-Compose en state
val data by produceState<String>(initialValue = "") {
value = fetchData()
}
// derivedStateOf : recalculé uniquement si les inputs changent
val isValid by remember { derivedStateOf { text.length > 3 } }Modifiers
Compose// S'appliquent dans l'ordre (de haut en bas)
Modifier
.fillMaxSize()
.fillMaxWidth()
.size(100.dp)
.width(200.dp).height(50.dp)
.wrapContentSize()
.padding(16.dp)
.padding(horizontal = 8.dp, vertical = 4.dp)
.offset(x = 10.dp, y = 5.dp)
.background(Color.Blue)
.background(MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(8.dp))
.clip(RoundedCornerShape(12.dp))
.border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
.clickable { onClick() }
.alpha(0.5f)
.rotate(45f)
.scale(1.2f)
.weight(1f) // dans Row/Column
.align(Alignment.CenterHorizontally)
.zIndex(1f)
.statusBarsPadding()
.navigationBarsPadding()
.imePadding() // évite que le clavier cache le contenu
.testTag("myButton")Layouts courants
ComposeColumn(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) { ... }
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) { ... }
Box(contentAlignment = Alignment.Center) {
Image(...)
Text("Overlay")
}
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.weight(1f)) // pousse les éléments
// Arrangement options
Arrangement.Start / End / Center
Arrangement.SpaceBetween / SpaceAround / SpaceEvenly
Arrangement.spacedBy(8.dp)Composants UI courants
Compose// Text
Text(
text = "Hello",
style = MaterialTheme.typography.titleLarge,
color = Color.Red,
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center
)
// Buttons
Button(onClick = { }, enabled = true) { Text("Cliquer") }
OutlinedButton(onClick = { }) { Text("Outlined") }
TextButton(onClick = { }) { Text("Text") }
IconButton(onClick = { }) { Icon(Icons.Default.Add, null) }
FloatingActionButton(onClick = { }) { Icon(Icons.Default.Add, null) }
// TextField
var text by remember { mutableStateOf("") }
TextField(
value = text, onValueChange = { text = it },
label = { Text("Nom") },
placeholder = { Text("Entrez votre nom") },
leadingIcon = { Icon(Icons.Default.Person, null) },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Done
),
singleLine = true,
isError = text.isEmpty()
)
OutlinedTextField(value = text, onValueChange = { text = it }, label = { Text("Label") })
// Image
Image(
painter = painterResource(R.drawable.logo),
contentDescription = "Logo",
contentScale = ContentScale.Crop,
modifier = Modifier.size(64.dp).clip(CircleShape)
)
AsyncImage(model = "https://...", contentDescription = null) // Coil
// Toggles
Switch(checked = checked, onCheckedChange = { checked = it })
Checkbox(checked = checked, onCheckedChange = { checked = it })
RadioButton(selected = selected, onClick = { })
Slider(value = sliderVal, onValueChange = { sliderVal = it }, valueRange = 0f..100f)
// Misc
HorizontalDivider(thickness = 1.dp, color = Color.LightGray)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) { ... }
CircularProgressIndicator()
CircularProgressIndicator(progress = { 0.7f })
LinearProgressIndicator(progress = { 0.5f })Listes & Grilles
Compose// LazyColumn (RecyclerView vertical)
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
item { HeaderComposable() }
items(myList) { item -> ItemCard(item) }
items(myList, key = { it.id }) { item -> ItemCard(item) }
itemsIndexed(myList) { index, item -> Text("$index: $item") }
}
// LazyRow (horizontal)
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
items(myList) { ItemChip(it) }
}
// LazyVerticalGrid
LazyVerticalGrid(
columns = GridCells.Fixed(2), // ou GridCells.Adaptive(minSize = 150.dp)
contentPadding = PaddingValues(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(myList) { GridItem(it) }
}
// Pull to refresh
PullToRefreshBox(isRefreshing = isRefreshing, onRefresh = { viewModel.refresh() }) {
LazyColumn { ... }
}
// LazyListState (scroll programmatique)
val listState = rememberLazyListState()
LazyColumn(state = listState) { ... }
LaunchedEffect(targetIndex) { listState.animateScrollToItem(targetIndex) }
// Détecter la fin de liste
val isAtBottom by remember {
derivedStateOf {
listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == myList.size - 1
}
}ViewModel & StateFlow
Composeclass MyViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
private val _events = MutableSharedFlow<UiEvent>()
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
fun loadData() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val data = withContext(Dispatchers.IO) { repository.getData() }
_uiState.value = UiState.Success(data)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "Erreur")
}
}
}
}
// Dans le Composable
@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (uiState) {
is UiState.Loading -> CircularProgressIndicator()
is UiState.Success -> ContentList((uiState as UiState.Success).data)
is UiState.Error -> ErrorMessage((uiState as UiState.Error).message)
}
}Side Effects
Compose// LaunchedEffect : coroutine liée au cycle de vie du composable
LaunchedEffect(Unit) { viewModel.loadData() } // une seule fois
LaunchedEffect(userId) { viewModel.loadUser(userId) } // relancé si userId change
// DisposableEffect : avec nettoyage (onStart / onStop)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event -> ... }
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
// SideEffect : s'exécute à chaque recomposition réussie
SideEffect { analytics.log("Recomposition") }
// rememberCoroutineScope : coroutine déclenchée par un event UI
val scope = rememberCoroutineScope()
Button(onClick = { scope.launch { doSomething() } }) { Text("Go") }
// rememberUpdatedState : garder une référence à jour dans un effet long
val currentOnEvent by rememberUpdatedState(onEvent)
// snapshotFlow : convertit un state Compose en Flow
LaunchedEffect(Unit) {
snapshotFlow { listState.firstVisibleItemIndex }
.distinctUntilChanged()
.collect { index -> println("Visible: $index") }
}Scaffold & Navigation Bar
ComposeScaffold(
topBar = {
TopAppBar(
title = { Text("Mon App") },
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(Icons.Default.ArrowBack, null)
}
},
actions = {
IconButton(onClick = { }) { Icon(Icons.Default.Settings, null) }
}
)
},
bottomBar = {
NavigationBar {
val navBackStack by navController.currentBackStackEntryAsState()
val current = navBackStack?.destination?.route
items.forEach { item ->
NavigationBarItem(
selected = current == item.route,
onClick = { navController.navigate(item.route) },
icon = { Icon(item.icon, null) },
label = { Text(item.label) }
)
}
}
},
floatingActionButton = {
FloatingActionButton(onClick = { }) { Icon(Icons.Default.Add, null) }
},
snackbarHost = { SnackbarHost(snackbarHostState) }
) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) { ... }
}
// Snackbar
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(errorMessage) {
if (errorMessage != null) snackbarHostState.showSnackbar(errorMessage)
}Theming (Material 3)
Compose@Composable
fun MyAppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content)
}
// Accès dans les composables
MaterialTheme.colorScheme.primary
MaterialTheme.colorScheme.surface
MaterialTheme.colorScheme.onBackground
MaterialTheme.typography.titleLarge
MaterialTheme.typography.bodyMedium
MaterialTheme.shapes.medium
// CompositionLocal custom
val LocalDimens = staticCompositionLocalOf { Dimens() }
data class Dimens(val paddingSmall: Dp = 8.dp, val paddingLarge: Dp = 16.dp)
// CompositionLocalProvider
val LocalUserName = compositionLocalOf<String> { error("Non fourni") }
CompositionLocalProvider(LocalUserName provides "Adrien") {
val name = LocalUserName.current
}Animations
Compose// animate*AsState : interpolation simple
val alpha by animateFloatAsState(targetValue = if (visible) 1f else 0f, label = "alpha")
val size by animateDpAsState(targetValue = if (expanded) 200.dp else 100.dp, label = "size")
val color by animateColorAsState(targetValue = if (active) Color.Green else Color.Gray, label = "color")
// AnimatedVisibility
AnimatedVisibility(
visible = isVisible,
enter = fadeIn() + slideInVertically(),
exit = fadeOut() + slideOutVertically()
) { ContentComposable() }
// AnimatedContent
AnimatedContent(targetState = currentScreen, label = "screen") { screen ->
when (screen) {
Screen.Home -> HomeScreen()
Screen.Detail -> DetailScreen()
}
}
// Crossfade
Crossfade(targetState = currentTab, label = "tab") { tab -> when (tab) { ... } }
// updateTransition (animations coordonnées)
val transition = updateTransition(targetState = expanded, label = "expand")
val size by transition.animateDp(label = "size") { if (it) 200.dp else 100.dp }
val alpha by transition.animateFloat(label = "alpha") { if (it) 1f else 0.5f }
// Infinite animation (ex: pulse, loading)
val infiniteTransition = rememberInfiniteTransition(label = "pulse")
val scale by infiniteTransition.animateFloat(
initialValue = 1f, targetValue = 1.2f, label = "scale",
animationSpec = infiniteRepeatable(animation = tween(600), repeatMode = RepeatMode.Reverse)
)Interopérabilité & Divers
Compose// Clavier
val focusManager = LocalFocusManager.current
focusManager.clearFocus()
val keyboardController = LocalSoftwareKeyboardController.current
keyboardController?.hide()
// Contexte & densité
val context = LocalContext.current
val density = LocalDensity.current
with(density) { 16.dp.toPx() }
// Focus
val focusRequester = remember { FocusRequester() }
TextField(..., modifier = Modifier.focusRequester(focusRequester))
LaunchedEffect(Unit) { focusRequester.requestFocus() }
// Clipboard
val clipboardManager = LocalClipboardManager.current
clipboardManager.setText(AnnotatedString("Copié !"))
// Layout personnalisé
Layout(content = { ... }) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
layout(constraints.maxWidth, constraints.maxHeight) {
placeables.forEach { it.placeRelative(0, 0) }
}
}
// Canvas
Canvas(modifier = Modifier.size(200.dp)) {
drawCircle(color = Color.Blue, radius = size.minDimension / 2)
drawLine(Color.Red, Offset(0f, 0f), Offset(size.width, size.height), strokeWidth = 4f)
}Bonnes pratiques
Compose✅ Hisser l'état (state hoisting) au niveau le plus bas qui en a besoin
✅ Utiliser sealed class pour les états UI (Loading / Success / Error)
✅ Préférer collectAsStateWithLifecycle() à collectAsState() en production
✅ Toujours fournir un contentDescription pour l'accessibilité
✅ Utiliser key { } dans les listes pour éviter les recompositions inutiles
✅ Éviter les calculs lourds dans les composables (utiliser remember ou ViewModel)
✅ Préférer des composables stateless (données en paramètre, callbacks en sortie)
✅ Utiliser @Stable / @Immutable pour les classes passées en paramètre
✅ Nommer les animations avec label = "..." pour le Layout Inspector
✅ Tester avec @PreviewParameter pour les previews dynamiques