Questions d'entretien Confirmé Ingénieur Mobile
Sur quoi porte un entretien Confirmé de Ingénieur Mobile, les questions que vous rencontrerez et comment vous entraîner avec un retour IA instantané.
Ce qui est attendu au niveau Confirmé
On attend de l'architecture, du réglage de performance et une livraison autonome de fonctionnalités.
Exemples de questions d'entretien pour Ingénieur Mobile
- TechniqueDécrivez le cycle de vie d'une activity/d'un view controller et les pièges courants.Ce qu'une bonne réponse couvre
- Activity: onCreate, onStart, onResume, onPause, onStop, onDestroy
- ViewController: viewDidLoad, viewWillAppear, viewDidAppear, viewWillDisappear, viewDidDisappear
- Piège: fuite mémoire avec références circulaires (ex: handler non static)
- Piège: perte d'état lors de changement de configuration (rotation)
- Piège: opérations asynchrones non annulées dans onDestroy
Voir un exemple de réponse
Le cycle de vie d'une Activity Android comprend onCreate(), onStart(), onResume(), onPause(), onStop(), onDestroy(). Pour un ViewController iOS, les méthodes clés sont viewDidLoad(), viewWillAppear(), viewDidAppear(), viewWillDisappear(), viewDidDisappear(). Les pièges courants incluent les fuites mémoire dues à des références non libérées (par exemple, un Handler interne non statique retient l'Activity), la perte d'état lors d'une rotation (solution: ViewModel ou saved instance state) et les opérations asynchrones qui continuent après la destruction (annuler dans onStop/onDestroy). Il est crucial de comprendre que les callbacks asynchrones peuvent exécuter après la destruction si non nettoyés. Une bonne pratique est d'utiliser le cycle de vie pour démarrer/arrêter les tâches lourdes et de gérer les configurations changes via des fragments retenus ou ViewModel. Un suivi recommandé en entretien est la différence entre onPause et onStop: onPause est rapide, onStop peut être différé.
- TechniqueComment gardez-vous fluide une longue liste défilante sur un appareil bas de gamme ?Ce qu'une bonne réponse couvre
- RecyclerView/UICollectionView avec ViewHolder réutilisable
- Chargement différé (lazy loading) des images avec Glide/Picasso
- Limiter le nombre de requêtes réseau simultanées
- Optimiser les layouts (éviter les hiérarchies profondes, utiliser ConstraintLayout)
- Profilage avec GPU Overdraw et Systrace
Voir un exemple de réponse
Pour une liste fluide sur un appareil bas de gamme, les clés sont la réutilisation des vues via RecyclerView (Android) ou UICollectionView (iOS). Le ViewHolder pattern évite de créer de nouvelles vues à chaque défilement. Le chargement des images doit être asynchrone et mis en cache (mémoire + disque) avec Glide ou Picasso, en réglant la taille des images au minimum nécessaire. Il faut aussi limiter les appels réseau à 3-4 simultanément et utiliser un debounce lors du défilement rapide. Côté layout, préférer ConstraintLayout à des LinearLayout imbriqués pour réduire le nombre de passes de mesure. Enfin, le profilage est essentiel: GPU Overdraw trace les zones redessinées, Systrace repère les opérations longues sur le thread principal. Éviter les animations lourdes pendant le défilement et utiliser DiffUtil (Android) ou IGListKit (iOS) pour des mises à jour efficaces. Un piège est de charger des images haute résolution inutilement; toujours préférer des vignettes adaptées à la taille d'affichage.
- TechniqueComment concevriez-vous une prise en charge hors-ligne avec synchronisation ultérieure ?Ce qu'une bonne réponse couvre
- Base de données locale (Room/CoreData) comme source de vérité
- File d'attente de synchronisation ordonnée
- Stratégie de résolution de conflits (horodatage, dernière écriture gagne)
- Service de synchronisation en arrière-plan avec WorkManager/BGTaskScheduler
- Écoute de l'état réseau pour déclencher la synchro
Voir un exemple de réponse
La conception hors-ligne commence par une base de données locale (Room sur Android, CoreData sur iOS) qui sert de source de vérité. Les données modifiées localement sont placées dans une file d'attente avec un ordre temporel. Lors de la reconnexion, un service de synchronisation (WorkManager pour Android, BGTaskScheduler pour iOS) envoie les modifications au serveur. La résolution des conflits peut être basée sur l'horodatage (dernière écriture gagne) ou une fusion plus fine. Il est important de gérer l'état réseau avec un observateur (ConnectivityManager, NWPathMonitor) pour déclencher la synchro automatiquement. Les pièges incluent les conflits de modification simultanée et la gestion des suppressions. Une approche avancée utilise le pattern CQRS ou un delta sync. La synchronisation doit être conçue comme incrémentale pour éviter de retélécharger l'intégralité des données. Un suivi possible: comment gérez-vous les grandes files d'attente? Réponse: avec pagination et priorisation.
- ProgrammationImplémentez un cache d'images avec des niveaux mémoire et disque.Ce qu'une bonne réponse couvre
- LRU en mémoire (LinkedHashMap avec accès order)
- Cache disque avec répertoire de fichiers et taille limite
- Thread safety: ReentrantReadWriteLock ou coroutines
- Stratégie d'éviction: âge ou taille
Voir un exemple de réponse
Voici une implémentation simple d'un cache d'images en Kotlin avec deux niveaux. Le cache mémoire utilise un LinkedHashMap avec accès-order pour implémenter LRU, limité en taille et nombre d'éléments. Le cache disque stocke les fichiers dans un répertoire, avec une taille max configurable (par exemple 50 Mo). La lecture est thread-safe grâce à un ReentrantReadWriteLock. Lors de la demande d'une image, on vérifie d'abord le cache mémoire (O(1)), puis le disque (lecture fichier), et en cas d'échec on télécharge et on stocke dans les deux caches. L'éviction disque supprime les fichiers les plus anciens selon leur date de modification. Cette approche réduit les coûts réseau et accélère l'affichage. Attention: ne pas bloquer le thread principal – utiliser des coroutines ou des callbacks. En production, des bibliothèques comme Glide ou Coil sont recommandées car elles gèrent le cycle de vie et les transformations. Complexité: mise en cache O(1), lecture disque O(1) (hash), éviction O(n) si scan complet, mais on peut l'optimiser.
Solution de référencekotlin import java.io.* import java.util.* import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.read import kotlin.concurrent.write class ImageCache(private val cacheDir: File, private val maxSizeMB: Long = 50) { private val memoryCache = object : LinkedHashMap<String, ByteArray>(0, 0.75f, true) { override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, ByteArray>?): Boolean { return size > 100 // max 100 images en mémoire } } private val lock = ReentrantReadWriteLock() private var currentDiskSize = 0L init { if (!cacheDir.exists()) cacheDir.mkdirs() // Calculer la taille initiale du disque cacheDir.listFiles()?.forEach { file -> currentDiskSize += file.length() } } fun getImage(key: String): ByteArray? { lock.read { memoryCache[key]?.let { return it } } // Vérifier disque val file = File(cacheDir, keyToFilename(key)) if (file.exists()) { val data = file.readBytes() lock.write { memoryCache[key] = data // replacer en mémoire } return data } return null } fun putImage(key: String, data: ByteArray) { lock.write { memoryCache[key] = data // Sauvegarder sur disque val file = File(cacheDir, keyToFilename(key)) file.writeBytes(data) currentDiskSize += data.size if (currentDiskSize > maxSizeMB * 1024 * 1024) { evictDisk() } } } private fun evictDisk() { val files = cacheDir.listFiles()?.sortedBy { it.lastModified() } ?: return for (file in files) { if (currentDiskSize <= maxSizeMB * 1024 * 1024) break currentDiskSize -= file.length() file.delete() } } private fun keyToFilename(key: String): String { return key.hashCode().toLong().toString() // simpliste, mieux: hash MD5 } } - ProgrammationConstruisez une liste paginée qui charge davantage à mesure que l'utilisateur fait défiler.Ce qu'une bonne réponse couvre
- Implémentation avec RecyclerView et OnScrollListener
- Indicateur de chargement (footer progressif)
- Seuil de pré-chargement (distance from bottom)
- Gestion des erreurs et bouton Reessayer
- Eviter les doubles requêtes avec un verrou
Voir un exemple de réponse
Une liste paginée se construit avec RecyclerView et un OnScrollListener qui détecte quand l'utilisateur s'approche du bas (ex: items visibles + seuil >= total). Un ViewType additionnel pour le pied de page affiche un indicateur de chargement. À chaque déclenchement, on vérifie qu'aucune requête n'est en cours (à l'aide d'un flag isLoading). Les données sont ajoutées à la liste via DiffUtil pour une mise à jour fluide. En cas d'erreur, on affiche un bouton 'Réessayer'. Le modèle de données inclut un état (loading, success, error). Pour éviter les appels multiples en défilement rapide, on utilise un debounce ou on vérifie isLoading. La complexité mémoire reste linéaire: on ne garde que les éléments visibles plus une marge. Un piège courant est de ne pas réinitialiser la pagination lors d'une recherche ou d'un filtre; il faut alors vider la liste et repartir à la page 1. En kotlin, on peut utiliser Paging 3 qui gère tout cela automatiquement.
Solution de référencekotlin class PaginatedAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() { private val items = mutableListOf<Any>() private var isLoading = false private var isLastPage = false private var currentPage = 1 private val VIEW_TYPE_ITEM = 0 private val VIEW_TYPE_LOADING = 1 override fun getItemViewType(position: Int): Int { return if (position == items.size - 1 && !isLastPage) VIEW_TYPE_LOADING else VIEW_TYPE_ITEM } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return if (viewType == VIEW_TYPE_ITEM) { val view = LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false) ItemViewHolder(view) } else { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_loading, parent, false) LoadingViewHolder(view) } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { /* bind data */ } override fun getItemCount() = items.size fun addItems(newItems: List<Any>) { val startPos = items.size items.addAll(newItems) notifyItemRangeInserted(startPos, newItems.size) } fun setLoading(loading: Boolean) { isLoading = loading if (loading) { items.add(LoadingItem()) notifyItemInserted(items.size - 1) } else { val pos = items.indexOfFirst { it is LoadingItem } if (pos >= 0) { items.removeAt(pos) notifyItemRemoved(pos) } } } fun setLastPage(isLast: Boolean) { isLastPage = isLast } // Dans l'activité/fragment: // recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { // override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { // val layoutManager = recyclerView.layoutManager as LinearLayoutManager // val visibleItemCount = layoutManager.childCount // val totalItemCount = layoutManager.itemCount // val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() // if (!isLoading && !isLastPage) { // if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - 5) { // loadMore() // } // } // } // }) } // Classes de support data class LoadingItem(val dummy: Boolean = true) class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) class LoadingViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) - Conception de systèmesConcevez une application de chat mobile fonctionnant hors-ligne et se synchronisant à la reconnexion.Ce qu'une bonne réponse couvre
- Base locale Room avec tables User, Message, SyncQueue
- File de messages sortants avec status (pending, synced)
- Service de synchronisation avec WorkManager (périodique + déclenché réseau)
- Gestion des conflits: timestamp ou merge
- Notifications push pour recevoir les messages en temps réel
Voir un exemple de réponse
L'application de chat hors-ligne stocke localement les messages dans Room (Android). Chaque message a un ID unique (généré côté client pour les messages envoyés), un timestamp, et un statut (synced/pending). Les messages sortants sont placés dans une file d'attente persistante et envoyés dès que la connexion est rétablie, gérée par un WorkManager déclenché par l'événement réseau. Pour les messages entrants, les notifications push (FCM) permettent de les recevoir même si l'app est en arrière-plan; ils sont stockés localement et une synchronisation delta récupère les messages manquants. La résolution des conflits peut être basée sur l'horodatage (dernière écriture gagne). Il faut aussi gérer la suppression de messages (soft delete). Le design doit éviter les doublons: on utilise l'ID client pour dédupliquer côté serveur. Un piège est la consommation de batterie due à des synchronisations fréquentes; utiliser WorkManager avec contrainte réseau. Architecture typique: ViewModel + Repository + Room + Retrofit pour l'API. Un suivi en entretien: comment garantir que les messages sont envoyés dans l'ordre ? Réponse: avec un numéro de séquence local.
- ComportementalParlez-moi d'un crash ou d'un bug mémoire difficile que vous avez traqué.Ce qu'une bonne réponse couvre
- Situation: fuite mémoire dans une app de streaming
- Tâche: identifier et corriger le crash
- Action: analyse de heap dump, révision des références fortes
- Résultat: suppression d'une référence cyclique, réduction de 30% de l'usage mémoire
Voir un exemple de réponse
Lors du développement d'une application de streaming, nous avions un crash récurrent après 20 minutes d'utilisation. Le diagnostic a commencé par une analyse du heap dump avec Android Studio Memory Profiler; on voyait des instances d'Activity qui ne se libéraient pas. En creusant, j'ai découvert qu'un MediaPlayer était retenu par un singleton via une interface de callback. Le singleton avait une référence forte vers l'Activity via un listener, et l'Activity avait une référence forte vers le singleton, créant un cycle. J'ai corrigé en utilisant une référence faible (WeakReference) pour le callback et en annulant l'enregistrement dans onStop(). Le bug a été reproduit sur des appareils bas de gamme. Résultat: plus de fuite mémoire et une diminution de 30% de l'empreinte mémoire. Cette expérience m'a appris à toujours inspecter les références croisées, en particulier avec les singletons et les listeners. Un suivi possible: comment avez-vous utilisé WeakReference ? Réponse: en créant une classe encapsulant l'interface avec une WeakReference, et en vérifiant qu'elle n'est pas null avant d'appeler le callback.
- ComportementalComment équilibrez-vous les nouvelles fonctionnalités face à la taille et à la performance de l'app ?Ce qu'une bonne réponse couvre
- Priorisation basée sur l'impact utilisateur et le coût du délai
- Performance budget: seuils de crash, temps de démarrage, taille d'APK
- Feature flags pour activer/désactiver des fonctionnalités
- Modularisation pour désolidariser les features
- Tests A/B pour mesurer l'impact sur les métriques clés
Voir un exemple de réponse
Pour équilibrer nouvelles fonctionnalités et performance/taille, j'adopte une approche centrée sur l'impact utilisateur. Chaque fonctionnalité est évaluée selon son bénéfice (ex: rétention, engagement) et son coût (temps de développement, impact sur les métriques de performance). Nous définissons un performance budget: par exemple, le temps de lancement froid ne doit pas dépasser 2 secondes, la taille de l'APK doit rester sous 40 Mo, les crashes <0.1%. Les features sont désactivables via des feature flags, permettant des rollbacks rapides. La modularisation (par exemple avec des modules dynamiques Android) réduit l'impact sur la taille initiale. Avant de lancer une fonctionnalité, on effectue des tests A/B pour mesurer son effet sur les métriques clés (crash rate, temps de rendu). Si une fonctionnalité dégrade significativement les performances, on la repousse ou on l'optimise. Cette approche permet d'innover sans sacrifier l'expérience utilisateur. Un piège est de négliger l'impact cumulatif de plusieurs features; il faut donc une surveillance continue. En entretien, on peut citer l'utilisation de ProGuard/R8 pour réduire la taille, et l'analyse des APK avec Android Studio.
Ce que les recruteurs évaluent
Profondeur de la plateforme
Cycle de vie, threads et mémoire sur iOS (Swift) ou Android (Kotlin).
UI et performance
Listes fluides, rendu, à-coups et considérations de batterie.
Architecture de l'app
MVVM/MVI, navigation, injection de dépendances et modularité.
Hors-ligne et synchronisation
Stockage local, cache, résolution de conflits et perte de connectivité.
Ingénierie de release
Builds pour les stores, rapports de crash et déploiements progressifs.
Comment se préparer
- Commencez par le cycle de vie et la mémoire — ce sont les points de défaillance les plus courants.
- Abordez de façon proactive le hors-ligne et les réseaux instables ; les recruteurs mobile s'y attendent.
- Montrez que vous mesurez : profilage, taux de crash et temps d'image valent mieux que des généralités.
Questions fréquentes
Les entretiens mobile incluent-ils du code de structures de données ?
Souvent oui, en plus de code propre à la plateforme comme construire un cache ou une liste paginée, et d'une discussion sur l'architecture de l'app.
Quelles questions d'architecture reviennent pour les postes mobile ?
Attendez-vous à MVVM/MVI, navigation, injection de dépendances et la conception d'apps capables de fonctionner hors-ligne avec synchronisation.
Comment me préparer à un entretien d'ingénierie mobile ?
Révisez le cycle de vie et la mémoire de la plateforme, construisez une petite fonctionnalité de zéro et répétez les décisions d'architecture en entretiens blancs.
Entraînez-vous aux questions de Ingénieur Mobile avec un retour instantané de l'IA
Offersly mène un entretien blanc adapté à votre CV et au poste visé, puis note chaque réponse sur la pertinence, la profondeur, la clarté et l'exactitude.