☕ KOTLIN

Variables & Types

val 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 object

Null Safety

var 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 impossible

Fonctions

// 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

val 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

// 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, 1

Classes

// 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

open 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 base

Generics

fun <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 = consommateur

Scope Functions

FonctionContexteRetourneUsage typique
letitlambda resultnull-check, transformation
runthislambda resultinit + calcul
withthislambda resultopérations groupées
applythisl'objetconfiguration
alsoitl'objeteffets 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

val 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

// 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

// 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

@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

// 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

// 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

Column(
    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

// 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

// 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

class 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

// 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

Scaffold(
    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)

@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

// 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

// 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

✅ 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