Top 20 questions d'entretien Jetpack Compose en 2026

Les 20 questions les plus posées en entretien sur Jetpack Compose : recomposition, state, navigation, performance et architecture.

Questions d'entretien Jetpack Compose pour développeurs Android

Jetpack Compose s'est imposé comme le toolkit UI standard pour Android. En entretien technique, les recruteurs testent désormais systématiquement la maîtrise de Compose, de la recomposition au state management en passant par la performance. Voici les 20 questions les plus fréquentes, avec des réponses détaillées et des exemples de code.

Comment utiliser ce guide

Chaque question est accompagnée d'une réponse structurée et d'un exemple de code. Les questions sont classées par difficulté croissante : fondamentaux, intermédiaire, puis avancé.

Fondamentaux de Jetpack Compose

1. Quelle est la différence entre Compose et le système de vues XML ?

Compose adopte un paradigme déclaratif : l'UI est décrite en fonction de l'état, et le framework se charge de la mettre à jour automatiquement. Le système XML traditionnel est impératif — il faut manipuler les vues manuellement via findViewById ou View Binding.

DeclarativeExample.ktkotlin
// Compose : l'UI se met à jour automatiquement quand count change
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }  // État réactif
    Button(onClick = { count++ }) {               // Déclaration de l'UI
        Text("Clics : $count")                    // Recomposé automatiquement
    }
}

En Compose, pas besoin de chercher une référence à un TextView pour le mettre à jour : la recomposition s'en charge.

2. Qu'est-ce que la recomposition ?

La recomposition est le processus par lequel Compose rappelle les fonctions @Composable lorsque leur état change. Seules les fonctions dont les paramètres ont changé sont réexécutées, ce qui optimise les performances.

RecompositionExample.ktkotlin
@Composable
fun UserCard(name: String, age: Int) {
    Column {
        Text("Nom : $name")   // Recomposé uniquement si name change
        Text("Âge : $age")    // Recomposé uniquement si age change
        StaticBadge()          // Non recomposé si ses inputs ne changent pas
    }
}

@Composable
fun StaticBadge() {
    Text("Badge statique")  // Compose sait que cette fonction est stable
}

Point clé à mentionner en entretien : la recomposition est optimiste (Compose suppose qu'elle peut être annulée) et non ordonnée (l'ordre d'exécution des composables n'est pas garanti).

3. À quoi sert remember ?

remember permet de conserver une valeur entre les recompositions. Sans remember, chaque recomposition réinitialiserait la variable.

RememberExample.ktkotlin
@Composable
fun InputField() {
    // ✅ La valeur survit aux recompositions
    var text by remember { mutableStateOf("") }

    // ❌ Sans remember, text serait réinitialisé à "" à chaque recomposition
    // var text by mutableStateOf("")

    TextField(
        value = text,
        onValueChange = { text = it },  // Déclenche une recomposition
        label = { Text("Saisir du texte") }
    )
}

4. Quelle différence entre remember et rememberSaveable ?

remember conserve la valeur lors des recompositions, mais la perd lors des changements de configuration (rotation d'écran). rememberSaveable persiste la valeur à travers les changements de configuration en utilisant le mécanisme SavedInstanceState.

RememberSaveableExample.ktkotlin
@Composable
fun SearchBar() {
    // Perdu après rotation d'écran
    var query by remember { mutableStateOf("") }

    // Conservé après rotation d'écran
    var savedQuery by rememberSaveable { mutableStateOf("") }

    TextField(
        value = savedQuery,
        onValueChange = { savedQuery = it },
        placeholder = { Text("Rechercher...") }
    )
}

State Management en Compose

5. Qu'est-ce que le state hoisting ?

Le state hoisting consiste à remonter l'état d'un composable vers son parent. Le composable enfant devient stateless : il reçoit l'état en paramètre et notifie les changements via des callbacks.

StateHoistingExample.ktkotlin
// ✅ Composable stateless — facile à tester et réutiliser
@Composable
fun EmailInput(
    email: String,                    // État fourni par le parent
    onEmailChange: (String) -> Unit,  // Callback vers le parent
    modifier: Modifier = Modifier
) {
    TextField(
        value = email,
        onValueChange = onEmailChange,
        label = { Text("Email") },
        modifier = modifier
    )
}

// Parent qui gère l'état
@Composable
fun LoginForm() {
    var email by remember { mutableStateOf("") }
    EmailInput(
        email = email,
        onEmailChange = { email = it }  // Le parent contrôle l'état
    )
}

Ce pattern est fondamental en Compose et revient très souvent en entretien.

6. Comment fonctionne derivedStateOf ?

derivedStateOf crée un état dérivé qui ne déclenche une recomposition que lorsque le résultat du calcul change, pas à chaque modification de la source.

DerivedStateExample.ktkotlin
@Composable
fun FilteredList(items: List<String>) {
    var searchQuery by remember { mutableStateOf("") }

    // Recalculé uniquement quand le résultat filtré change réellement
    val filteredItems by remember(items) {
        derivedStateOf {
            items.filter { it.contains(searchQuery, ignoreCase = true) }
        }
    }

    Column {
        TextField(value = searchQuery, onValueChange = { searchQuery = it })
        LazyColumn {
            items(filteredItems) { item -> Text(item) }
        }
    }
}
Quand utiliser derivedStateOf

Ce mécanisme est utile quand un état change fréquemment mais que le résultat dérivé change rarement (ex : une liste filtrée, un bouton activé/désactivé selon un formulaire).

7. Quelle est la différence entre StateFlow et State<T> de Compose ?

StateFlow (Kotlin coroutines) est un flux réactif du ViewModel. State<T> est le mécanisme natif de Compose pour déclencher la recomposition. En pratique, StateFlow est collecté dans un composable via collectAsStateWithLifecycle().

StateFlowExample.ktkotlin
class UserViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(UserUiState())
    val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
}

@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    // Convertit StateFlow en State<T> pour Compose
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    Text("Bonjour, ${uiState.userName}")
}

La recommandation est d'utiliser collectAsStateWithLifecycle() (plutôt que collectAsState()) car cette fonction respecte le cycle de vie et arrête la collecte quand l'écran n'est plus visible.

Side Effects et cycle de vie

8. Quels sont les principaux side effects en Compose ?

Les side effects permettent d'exécuter du code non-composable (appels réseau, logs, navigation) de manière contrôlée. Les trois principaux :

SideEffectsExample.ktkotlin
@Composable
fun AnalyticsScreen(screenName: String) {
    // LaunchedEffect : exécuté une fois quand screenName change
    LaunchedEffect(screenName) {
        analyticsTracker.logScreenView(screenName)  // Appel suspendu
    }

    // DisposableEffect : avec nettoyage (comme useEffect avec cleanup)
    DisposableEffect(Unit) {
        val listener = onScrollListener()
        scrollView.addListener(listener)
        onDispose {
            scrollView.removeListener(listener)  // Nettoyage garanti
        }
    }

    // SideEffect : exécuté après chaque recomposition réussie
    SideEffect {
        logger.log("Screen recomposée")  // Code non-suspendu
    }
}

9. Quand utiliser LaunchedEffect vs rememberCoroutineScope ?

LaunchedEffect est lié à la composition : la coroutine est annulée quand le composable quitte la composition ou quand la clé change. rememberCoroutineScope fournit un scope contrôlé par l'utilisateur, utile pour des actions déclenchées par l'utilisateur (clic de bouton).

CoroutineScopeExample.ktkotlin
@Composable
fun DataScreen(userId: String) {
    // ✅ LaunchedEffect : chargement automatique lié au cycle de vie
    LaunchedEffect(userId) {
        loadUserData(userId)  // Relancé si userId change
    }

    // ✅ rememberCoroutineScope : action utilisateur ponctuelle
    val scope = rememberCoroutineScope()
    Button(onClick = {
        scope.launch { refreshData() }  // Déclenché manuellement
    }) {
        Text("Rafraîchir")
    }
}

Prêt à réussir tes entretiens Android ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Layouts et composants avancés

10. Comment fonctionne LazyColumn et en quoi diffère-t-elle de RecyclerView ?

LazyColumn est l'équivalent Compose de RecyclerView. Elle ne compose que les éléments visibles à l'écran et recycle les composables sortis de la fenêtre visible.

LazyColumnExample.ktkotlin
@Composable
fun UserList(users: List<User>) {
    LazyColumn(
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)  // Espacement entre items
    ) {
        items(
            items = users,
            key = { it.id }  // Clé stable pour optimiser les recompositions
        ) { user ->
            UserCard(user)
        }
    }
}

Point important : toujours fournir un paramètre key stable pour éviter des recompositions inutiles lors du tri ou de la suppression d'éléments.

11. Comment créer un layout personnalisé ?

Compose permet de créer des layouts sur mesure via la fonction Layout. Cela remplace les ViewGroup custom du système de vues.

CustomLayoutExample.ktkotlin
@Composable
fun OverlappingRow(
    overlapOffset: Dp = (-16).dp,  // Décalage négatif pour le chevauchement
    content: @Composable () -> Unit
) {
    Layout(content = content) { measurables, constraints ->
        val placeables = measurables.map { it.measure(constraints) }
        val width = placeables.sumOf { it.width } + (overlapOffset.roundToPx() * (placeables.size - 1))
        val height = placeables.maxOf { it.height }

        layout(width, height) {
            var xOffset = 0
            placeables.forEach { placeable ->
                placeable.placeRelative(xOffset, 0)
                xOffset += placeable.width + overlapOffset.roundToPx()
            }
        }
    }
}

12. Comment implémenter un thème personnalisé avec MaterialTheme ?

Le theming en Compose repose sur CompositionLocal. MaterialTheme fournit des valeurs de couleur, typographie et forme accessibles dans tout l'arbre de composables.

CustomThemeExample.ktkotlin
// Définition de couleurs personnalisées
private val DarkColorScheme = darkColorScheme(
    primary = Color(0xFF6200EE),
    secondary = Color(0xFF03DAC6),
    background = Color(0xFF121212)
)

@Composable
fun AppTheme(content: @Composable () -> Unit) {
    MaterialTheme(
        colorScheme = DarkColorScheme,
        typography = AppTypography,    // Typographie personnalisée
        content = content
    )
}

// Utilisation dans un composable
@Composable
fun ThemedCard() {
    Card(colors = CardDefaults.cardColors(
        containerColor = MaterialTheme.colorScheme.surface  // Accès au thème
    )) {
        Text(
            text = "Contenu",
            style = MaterialTheme.typography.bodyLarge  // Typographie du thème
        )
    }
}

13. Comment fonctionne la navigation avec Compose Navigation ?

Compose Navigation utilise un NavHost avec des routes déclarées comme des chaînes de caractères (ou des types sérialisables depuis Navigation 2.8+).

NavigationExample.ktkotlin
@Composable
fun AppNavigation() {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = "home") {
        composable("home") {
            HomeScreen(onNavigateToDetail = { id ->
                navController.navigate("detail/$id")  // Navigation avec argument
            })
        }
        composable(
            route = "detail/{userId}",
            arguments = listOf(navArgument("userId") { type = NavType.StringType })
        ) { backStackEntry ->
            val userId = backStackEntry.arguments?.getString("userId") ?: ""
            DetailScreen(userId = userId)
        }
    }
}

14. Comment passer des données entre écrans ?

Les arguments simples (String, Int) passent directement via la route. Pour des objets complexes, les recommandations actuelles sont d'utiliser un ViewModel partagé ou de passer uniquement un identifiant et de charger les données dans l'écran de destination.

NavigationArgsExample.ktkotlin
// Type-safe navigation avec Kotlin Serialization (Navigation 2.8+)
@Serializable
data class ProfileRoute(val userId: String, val tab: String = "info")

// Déclaration
composable<ProfileRoute> { backStackEntry ->
    val route = backStackEntry.toRoute<ProfileRoute>()
    ProfileScreen(userId = route.userId, tab = route.tab)
}

// Navigation
navController.navigate(ProfileRoute(userId = "123", tab = "stats"))
Anti-pattern à éviter

Ne jamais passer des objets complexes sérialisés dans la route. Passer un ID et laisser l'écran de destination charger les données via le ViewModel.

Performance et optimisation

15. Comment éviter les recompositions inutiles ?

Trois stratégies principales pour limiter les recompositions inutiles :

PerformanceExample.ktkotlin
// 1. Utiliser des classes stables (data class avec propriétés immuables)
@Stable  // Indique à Compose que cette classe est stable
data class UserState(
    val name: String,
    val avatar: String
)

// 2. Extraire les lambdas avec remember
@Composable
fun OptimizedList(onItemClick: (String) -> Unit) {
    val stableCallback = remember(onItemClick) { onItemClick }
    LazyColumn {
        items(100) { index ->
            ItemRow(onClick = { stableCallback("item_$index") })
        }
    }
}

// 3. Utiliser key() pour aider Compose à identifier les éléments
@Composable
fun UserTabs(users: List<User>) {
    Column {
        users.forEach { user ->
            key(user.id) {    // Identité stable
                UserRow(user)
            }
        }
    }
}

16. Comment profiler les performances d'une app Compose ?

Le Layout Inspector d'Android Studio affiche le nombre de recompositions par composable. Le flag debugInspectorInfo et les CompositionTracer aident au diagnostic.

ProfilingExample.ktkotlin
// Activer les compteurs de recomposition en debug
@Composable
fun DebugRecomposition(tag: String, content: @Composable () -> Unit) {
    val recompositionCount = remember { mutableIntStateOf(0) }

    SideEffect {
        recompositionCount.intValue++  // Incrémenté à chaque recomposition
        Log.d("Recomposition", "$tag : ${recompositionCount.intValue} fois")
    }

    content()
}

// Utilisation
DebugRecomposition("UserCard") {
    UserCard(user)
}

En complément, le Compose Compiler Metrics génère un rapport détaillé des fonctions skippable, restartable et des classes stables/instables.

17. Qu'est-ce que le modificateur Modifier et pourquoi est-il important ?

Modifier est une chaîne d'instructions ordonnée qui modifie l'apparence et le comportement d'un composable. L'ordre des modificateurs impacte directement le rendu.

ModifierOrderExample.ktkotlin
@Composable
fun ModifierOrderDemo() {
    // ❌ Padding PUIS background = padding non coloré
    Text(
        text = "Hello",
        modifier = Modifier
            .padding(16.dp)
            .background(Color.Red)
    )

    // ✅ Background PUIS padding = padding coloré
    Text(
        text = "Hello",
        modifier = Modifier
            .background(Color.Red)
            .padding(16.dp)
    )
}

Bonne pratique : toujours accepter un paramètre modifier: Modifier = Modifier dans les composables réutilisables pour permettre la personnalisation par le parent.

Architecture et patterns avancés

18. Comment structurer un écran Compose avec un ViewModel ?

Le pattern recommandé sépare l'état UI dans un data class, les événements dans une sealed interface, et le ViewModel gère la logique métier.

ScreenArchitectureExample.ktkotlin
// État UI
data class ProfileUiState(
    val user: User? = null,
    val isLoading: Boolean = false,
    val error: String? = null
)

// Événements utilisateur
sealed interface ProfileEvent {
    data object Refresh : ProfileEvent
    data class UpdateName(val name: String) : ProfileEvent
}

// ViewModel
class ProfileViewModel(private val repo: UserRepository) : ViewModel() {
    private val _uiState = MutableStateFlow(ProfileUiState(isLoading = true))
    val uiState = _uiState.asStateFlow()

    fun onEvent(event: ProfileEvent) {
        when (event) {
            is ProfileEvent.Refresh -> loadProfile()
            is ProfileEvent.UpdateName -> updateName(event.name)
        }
    }
}

// Écran Compose
@Composable
fun ProfileScreen(viewModel: ProfileViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    ProfileContent(
        uiState = uiState,
        onEvent = viewModel::onEvent  // Délégation des événements
    )
}

19. Comment tester des composables ?

Compose fournit une bibliothèque de test avec ComposeTestRule pour les tests d'UI et les assertions sémantiques.

ComposableTestExample.ktkotlin
@get:Rule
val composeTestRule = createComposeRule()

@Test
fun counter_incrementsOnClick() {
    composeTestRule.setContent {
        Counter()  // Le composable à tester
    }

    // Vérifier l'état initial
    composeTestRule.onNodeWithText("Clics : 0").assertIsDisplayed()

    // Simuler un clic
    composeTestRule.onNodeWithText("Clics : 0").performClick()

    // Vérifier le nouvel état
    composeTestRule.onNodeWithText("Clics : 1").assertIsDisplayed()
}

Pour les tests unitaires des composables stateless, tester le ViewModel séparément avec des tests classiques JUnit/Turbine est souvent plus efficace.

20. Comment intégrer Compose dans une application existante avec des vues XML ?

L'interopérabilité est bidirectionnelle : ComposeView permet d'intégrer du Compose dans du XML, et AndroidView permet d'utiliser des vues classiques dans Compose.

InteropExample.ktkotlin
// Compose dans XML (dans un Fragment ou Activity)
class ProfileFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        return ComposeView(requireContext()).apply {
            setViewCompositionStrategy(
                ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
            )
            setContent {
                AppTheme { ProfileScreen() }
            }
        }
    }
}

// Vue XML dans Compose
@Composable
fun LegacyMapView() {
    AndroidView(
        factory = { context -> MapView(context).apply { onCreate(null) } },
        update = { mapView -> mapView.getMapAsync { /* config */ } }
    )
}
Stratégie de migration recommandée

Migrer écran par écran, en commençant par les écrans les plus simples. Chaque nouvel écran devrait être entièrement en Compose, tandis que les écrans existants migrent progressivement.

Prêt à réussir tes entretiens Android ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Conclusion

Ces 20 questions couvrent les fondamentaux que tout développeur Android doit maîtriser pour réussir un entretien sur Jetpack Compose. Voici une checklist récapitulative :

  • ✅ Comprendre la recomposition et son fonctionnement optimiste
  • ✅ Maîtriser remember, rememberSaveable et derivedStateOf
  • ✅ Appliquer le state hoisting systématiquement
  • ✅ Connaître les side effects (LaunchedEffect, DisposableEffect, SideEffect)
  • ✅ Savoir optimiser les performances (classes stables, clés, lambdas)
  • ✅ Structurer un écran avec ViewModel + UiState + Events
  • ✅ Tester les composables avec ComposeTestRule
  • ✅ Gérer l'interopérabilité Compose/Views

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#jetpack compose
#android
#interview
#kotlin
#ui

Partager

Articles similaires