décembre 21, 2025
18 min de lecture

Questions d'entretien pour développeur mobile senior (Android) : Guide complet

interview
career-advice
job-search
Questions d'entretien pour développeur mobile senior (Android) : Guide complet
MB

Milad Bonakdar

Auteur

Maîtrisez le développement Android avancé avec des questions d'entretien essentielles couvrant les modèles d'architecture, l'optimisation des performances, l'injection de dépendances, les tests, la sécurité et la conception de systèmes pour les développeurs seniors.


Introduction

On attend des développeurs Android seniors qu'ils conçoivent des applications évolutives et maintenables, tout en garantissant des performances élevées et une qualité de code optimale. Ce rôle exige une expertise approfondie des frameworks Android, des modèles architecturaux, de l'injection de dépendances, des stratégies de test et la capacité de prendre des décisions techniques éclairées.

Ce guide complet couvre les questions d'entretien essentielles pour les développeurs Android seniors, allant des concepts avancés de Kotlin aux modèles architecturaux, en passant par l'optimisation des performances, l'injection de dépendances, les tests et la conception du système. Chaque question comprend des réponses détaillées, une évaluation de la rareté et des niveaux de difficulté.


Kotlin Avancé et Caractéristiques du Langage (5 Questions)

1. Expliquez les coroutines Kotlin et leurs avantages par rapport aux threads.

Réponse: Les coroutines sont des primitives de concurrence légères qui permettent d'écrire du code asynchrone de manière séquentielle.

  • Avantages par rapport aux Threads:
    • Légèreté: Peut créer des milliers de coroutines sans problèmes de performance
    • Concurrence structurée: La relation parent-enfant assure un nettoyage approprié
    • Prise en charge de l'annulation: Propagation d'annulation intégrée
    • Gestion des exceptions: Gestion structurée des exceptions
  • Composants clés:
    • CoroutineScope: Définit le cycle de vie
    • Dispatchers: Contrôle le contexte d'exécution (Main, IO, Default)
    • Fonctions suspend: Peuvent être mises en pause et reprises
class UserRepository(private val api: ApiService) {
    suspend fun getUser(id: Int): Result<User> = withContext(Dispatchers.IO) {
        try {
            val user = api.fetchUser(id)
            Result.success(user)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

class UserViewModel : ViewModel() {
    private val _user = MutableLiveData<User>()
    val user: LiveData<User> = _user
    
    fun loadUser(id: Int) {
        viewModelScope.launch {
            when (val result = repository.getUser(id)) {
                is Result.Success -> _user.value = result.data
                is Result.Error -> handleError(result.exception)
            }
        }
    }
}

Rareté: Très Courant Difficulté: Difficile


2. Que sont les classes scellées et quand devez-vous les utiliser ?

Réponse: Les classes scellées représentent des hiérarchies de classes restreintes où toutes les sous-classes sont connues au moment de la compilation.

  • Avantages:
    • Expressions when exhaustives
    • Gestion d'état de type sécurisé
    • Mieux que les énumérations pour les données complexes
  • Cas d'utilisation: Représentation des états, des résultats, des événements de navigation
sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val message: String) : UiState<Nothing>()
}

class UserViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState<User>>(UiState.Loading)
    val uiState: StateFlow<UiState<User>> = _uiState.asStateFlow()
    
    fun loadUser() {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            try {
                val user = repository.getUser()
                _uiState.value = UiState.Success(user)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

// In UI
when (val state = uiState.value) {
    is UiState.Loading -> showLoading()
    is UiState.Success -> showData(state.data)
    is UiState.Error -> showError(state.message)
    // Compiler ensures all cases are handled
}

Rareté: Courant Difficulté: Moyenne


3. Expliquez Kotlin Flow et en quoi il diffère de LiveData.

Réponse: Flow est le flux asynchrone froid de Kotlin qui émet des valeurs séquentiellement.

  • Flow vs LiveData:
    • Flow: Flux froid, prend en charge les opérateurs, n'est pas sensible au cycle de vie, plus flexible
    • LiveData: Flux chaud, sensible au cycle de vie, spécifique à Android, plus simple pour l'UI
  • Types de Flow:
    • Flow: Flux froid (démarre lors de la collecte)
    • StateFlow: Flux chaud avec état actuel
    • SharedFlow: Flux chaud pour les événements
class UserRepository {
    // Cold Flow - starts when collected
    fun getUsers(): Flow<List<User>> = flow {
        val users = api.fetchUsers()
        emit(users)
    }.flowOn(Dispatchers.IO)
    
    // StateFlow - hot, holds state
    private val _userState = MutableStateFlow<List<User>>(emptyList())
    val userState: StateFlow<List<User>> = _userState.asStateFlow()
    
    // SharedFlow - hot, for events
    private val _events = MutableSharedFlow<Event>()
    val events: SharedFlow<Event> = _events.asSharedFlow()
}

class UserViewModel : ViewModel() {
    val users = repository.getUsers()
        .map { it.filter { user -> user.isActive } }
        .catch { emit(emptyList()) }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )
}

// Collect in Activity/Fragment
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.users.collect { users ->
            updateUI(users)
        }
    }
}

Rareté: Très Courant Difficulté: Difficile


4. Que sont les fonctions inline et quand devez-vous les utiliser ?

Réponse: Les fonctions inline copient le corps de la fonction sur le site d'appel, évitant ainsi la surcharge d'appel de fonction.

  • Avantages:
    • Élimine la surcharge d'allocation lambda
    • Permet les retours non locaux des lambdas
    • Meilleures performances pour les fonctions d'ordre supérieur
  • Cas d'utilisation: Fonctions d'ordre supérieur avec paramètres lambda
  • Compromis: Augmente la taille du code
// Without inline - creates lambda object
fun measureTime(block: () -> Unit) {
    val start = System.currentTimeMillis()
    block()
    val end = System.currentTimeMillis()
    println("Time: ${end - start}ms")
}

// With inline - no lambda object created
inline fun measureTimeInline(block: () -> Unit) {
    val start = System.currentTimeMillis()
    block()
    val end = System.currentTimeMillis()
    println("Time: ${end - start}ms")
}

// noinline - prevents specific parameter from being inlined
inline fun performOperation(
    inline operation: () -> Unit,
    noinline logger: () -> Unit
) {
    operation()
    saveLogger(logger)  // Can store noinline lambda
}

// crossinline - allows inline but prevents non-local returns
inline fun runAsync(crossinline block: () -> Unit) {
    thread {
        block()  // Can't return from outer function
    }
}

Rareté: Moyenne Difficulté: Difficile


5. Expliquez la délégation en Kotlin.

Réponse: La délégation permet à un objet de déléguer certaines de ses responsabilités à un autre objet.

  • Délégation de classe: Mot-clé by
  • Délégation de propriété: Lazy, observable, delegates
  • Avantages: Réutilisation du code, composition par rapport à l'héritage
// Class delegation
interface Repository {
    fun getData(): String
}

class RemoteRepository : Repository {
    override fun getData() = "Remote data"
}

class CachedRepository(
    private val remote: Repository
) : Repository by remote {
    private var cache: String? = null
    
    override fun getData(): String {
        return cache ?: remote.getData().also { cache = it }
    }
}

// Property delegation
class User {
    // Lazy initialization
    val database by lazy {
        Room.databaseBuilder(context, AppDatabase::class.java, "db").build()
    }
    
    // Observable property
    var name: String by Delegates.observable("Initial") { prop, old, new ->
        println("$old -> $new")
    }
    
    // Custom delegate
    var token: String by PreferenceDelegate("auth_token", "")
}

class PreferenceDelegate(
    private val key: String,
    private val defaultValue: String
) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return sharedPreferences.getString(key, defaultValue) ?: defaultValue
    }
    
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        sharedPreferences.edit().putString(key, value).apply()
    }
}

Rareté: Moyenne Difficulté: Moyenne


Modèles d'architecture (6 Questions)

6. Expliquez l'architecture MVVM et ses avantages.

Réponse: MVVM (Model-View-ViewModel) sépare la logique de l'interface utilisateur de la logique métier.

Loading diagram...
  • Model: Couche de données (repositories, sources de données)
  • View: Couche d'interface utilisateur (Activities, Fragments, Composables)
  • ViewModel: Logique de présentation, survit aux changements de configuration
  • Avantages: Testable, séparation des préoccupations, sensible au cycle de vie
// Model
data class User(val id: Int, val name: String, val email: String)

// Repository
class UserRepository(
    private val api: ApiService,
    private val dao: UserDao
) {
    suspend fun getUser(id: Int): User {
        return dao.getUser(id) ?: api.fetchUser(id).also {
            dao.insertUser(it)
        }
    }
}

// ViewModel
class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {
    private val _uiState = MutableStateFlow<UiState<User>>(UiState.Loading)
    val uiState: StateFlow<UiState<User>> = _uiState.asStateFlow()
    
    fun loadUser(id: Int) {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            try {
                val user = repository.getUser(id)
                _uiState.value = UiState.Success(user)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message ?: "Error")
            }
        }
    }
}

// View (Fragment)
class UserFragment : Fragment() {
    private val viewModel: UserViewModel by viewModels()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        viewLifecycleOwner.lifecycleScope.launch {
            viewModel.uiState.collect { state ->
                when (state) {
                    is UiState.Loading -> showLoading()
                    is UiState.Success -> showUser(state.data)
                    is UiState.Error -> showError(state.message)
                }
            }
        }
    }
}

Rareté: Très Courant Difficulté: Moyenne


7. Qu'est-ce que l'architecture propre et comment l'implémentez-vous dans Android ?

Réponse: L'architecture propre sépare le code en couches avec des dépendances claires.

Loading diagram...
  • Presentation: UI, ViewModels
  • Domain: Use Cases, Business Logic, Entities
  • Data: Repositories, Data Sources (API, Database)
  • Dependency Rule: Les couches internes ne connaissent pas les couches externes
// Domain Layer - Entities
data class User(val id: Int, val name: String, val email: String)

// Domain Layer - Repository Interface
interface UserRepository {
    suspend fun getUser(id: Int): Result<User>
}

// Domain Layer - Use Case
class GetUserUseCase(private val repository: UserRepository) {
    suspend operator fun invoke(id: Int): Result<User> {
        return repository.getUser(id)
    }
}

// Data Layer - Repository Implementation
class UserRepositoryImpl(
    private val remoteDataSource: UserRemoteDataSource,
    private val localDataSource: UserLocalDataSource
) : UserRepository {
    override suspend fun getUser(id: Int): Result<User> {
        return try {
            val localUser = localDataSource.getUser(id)
            if (localUser != null) {
                Result.success(localUser)
            } else {
                val remoteUser = remoteDataSource.fetchUser(id)
                localDataSource.saveUser(remoteUser)
                Result.success(remoteUser)
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

// Presentation Layer - ViewModel
class UserViewModel(
    private val getUserUseCase: GetUserUseCase
) : ViewModel() {
    fun loadUser(id: Int) {
        viewModelScope.launch {
            when (val result = getUserUseCase(id)) {
                is Result.Success -> handleSuccess(result.data)
                is Result.Failure -> handleError(result.exception)
            }
        }
    }
}

Rareté: Courant Difficulté: Difficile


8. Expliquez l'injection de dépendances et Dagger/Hilt.

Réponse: L'injection de dépendances fournit des dépendances aux classes au lieu de les créer en interne.

  • Avantages: Testabilité, couplage lâche, réutilisabilité
  • Dagger: Framework DI au moment de la compilation
  • Hilt: Dagger simplifié pour Android
// Define dependencies with Hilt
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
    
    @Provides
    @Singleton
    fun provideApiService(retrofit: Retrofit): ApiService {
        return retrofit.create(ApiService::class.java)
    }
}

@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(context, AppDatabase::class.java, "app_db")
            .build()
    }
    
    @Provides
    fun provideUserDao(database: AppDatabase): UserDao {
        return database.userDao()
    }
}

// Repository with injected dependencies
@Singleton
class UserRepository @Inject constructor(
    private val apiService: ApiService,
    private val userDao: UserDao
) {
    suspend fun getUser(id: Int): User {
        return userDao.getUser(id) ?: apiService.fetchUser(id).also {
            userDao.insertUser(it)
        }
    }
}

// ViewModel with injected repository
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repository: UserRepository
) : ViewModel() {
    // ViewModel logic
}

// Activity/Fragment
@AndroidEntryPoint
class UserActivity : AppCompatActivity() {
    private val viewModel: UserViewModel by viewModels()
}

Rareté: Très Courant Difficulté: Difficile


9. Qu'est-ce que le modèle Repository et pourquoi l'utiliser ?

Réponse: Le modèle Repository abstrait les sources de données, fournissant une API propre pour l'accès aux données.

  • Avantages:
    • Source unique de vérité
    • Logique de données centralisée
    • Facile de changer les sources de données
    • Testable
  • Implementation: Coordonne entre plusieurs sources de données
interface UserRepository {
    suspend fun getUsers(): Flow<List<User>>
    suspend fun getUser(id: Int): User?
    suspend fun saveUser(user: User)
    suspend fun deleteUser(id: Int)
}

class UserRepositoryImpl @Inject constructor(
    private val remoteDataSource: UserRemoteDataSource,
    private val localDataSource: UserLocalDataSource,
    private val cacheDataSource: UserCacheDataSource
) : UserRepository {
    
    override suspend fun getUsers(): Flow<List<User>> = flow {
        // Emit cached data first
        val cachedUsers = cacheDataSource.getUsers()
        if (cachedUsers.isNotEmpty()) {
            emit(cachedUsers)
        }
        
        // Fetch from local database
        val localUsers = localDataSource.getUsers()
        if (localUsers.isNotEmpty()) {
            emit(localUsers)
            cacheDataSource.saveUsers(localUsers)
        }
        
        // Fetch from remote
        try {
            val remoteUsers = remoteDataSource.fetchUsers()
            localDataSource.saveUsers(remoteUsers)
            cacheDataSource.saveUsers(remoteUsers)
            emit(remoteUsers)
        } catch (e: Exception) {
            // If remote fails, we already emitted cached/local data
        }
    }
    
    override suspend fun getUser(id: Int): User? {
        return cacheDataSource.getUser(id)
            ?: localDataSource.getUser(id)
            ?: remoteDataSource.fetchUser(id)?.also {
                localDataSource.saveUser(it)
                cacheDataSource.saveUser(it)
            }
    }
}

Rareté: Très Courant Difficulté: Moyenne


10. Expliquez l'architecture à activité unique.

Réponse: L'architecture à activité unique utilise une seule activité avec plusieurs fragments, gérés par le composant de navigation.

  • Avantages:
    • Navigation simplifiée
    • ViewModels partagés entre les fragments
    • Meilleures animations
    • Liaison profonde plus facile
  • Composant de navigation: Gère les transactions de fragments, la pile de retour, les arguments
// Navigation graph (nav_graph.xml)
<navigation>
    <fragment
        android:id="@+id/homeFragment"
        android:name="com.app.HomeFragment">
        <action
            android:id="@+id/action_home_to_detail"
            app:destination="@id/detailFragment" />
    </fragment>
    
    <fragment
        android:id="@+id/detailFragment"
        android:name="com.app.DetailFragment">
        <argument
            android:name="userId"
            app:argType="integer" />
    </fragment>
</navigation>

// MainActivity - single activity
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        val navController = findNavController(R.id.nav_host_fragment)
        setupActionBarWithNavController(navController)
    }
}

// HomeFragment - navigate to detail
class HomeFragment : Fragment() {
    fun navigateToDetail(userId: Int) {
        val action = HomeFragmentDirections.actionHomeToDetail(userId)
        findNavController().navigate(action)
    }
}

// DetailFragment - receive arguments
class DetailFragment : Fragment() {
    private val args: DetailFragmentArgs by navArgs()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val userId = args.userId
        viewModel.loadUser(userId)
    }
}

Rareté: Courant Difficulté: Moyenne


11. Qu'est-ce que l'architecture MVI (Model-View-Intent) ?

Réponse: MVI est une architecture de flux de données unidirectionnelle inspirée de Redux.

  • Composants:
    • Model: Représente l'état de l'interface utilisateur
    • View: Rend l'état, émet des intents
    • Intent: Actions/événements de l'utilisateur
  • Avantages: État prévisible, débogage plus facile, débogage temporel
// State
data class UserScreenState(
    val isLoading: Boolean = false,
    val user: User? = null,
    val error: String? = null
)

// Intent (user actions)
sealed class UserIntent {
    data class LoadUser(val id: Int) : UserIntent()
    object RetryLoading : UserIntent()
}

// ViewModel
class UserViewModel : ViewModel() {
    private val _state = MutableStateFlow(UserScreenState())
    val state: StateFlow<UserScreenState> = _state.asStateFlow()
    
    fun processIntent(intent: UserIntent) {
        when (intent) {
            is UserIntent.LoadUser -> loadUser(intent.id)
            is UserIntent.RetryLoading -> retryLoading()
        }
    }
    
    private fun loadUser(id: Int) {
        viewModelScope.launch {
            _state.value = _state.value.copy(isLoading = true, error = null)
            try {
                val user = repository.getUser(id)
                _state.value = _state.value.copy(
                    isLoading = false,
                    user = user
                )
            } catch (e: Exception) {
                _state.value = _state.value.copy(
                    isLoading = false,
                    error = e.message
                )
            }
        }
    }
}

// View
class UserFragment : Fragment() {
    private val viewModel: UserViewModel by viewModels()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // Send intent
        viewModel.processIntent(UserIntent.LoadUser(123))
        
        // Observe state
        viewLifecycleOwner.lifecycleScope.launch {
            viewModel.state.collect { state ->
                render(state)
            }
        }
    }
    
    private fun render(state: UserScreenState) {
        when {
            state.isLoading -> showLoading()
            state.error != null -> showError(state.error)
            state.user != null -> showUser(state.user)
        }
    }
}

Rareté: Moyenne Difficulté: Difficile


Performance et Optimisation (5 Questions)

12. Comment optimiser les performances de RecyclerView ?

Réponse: Plusieurs stratégies améliorent les performances de défilement de RecyclerView :

  1. Modèle ViewHolder: Réutiliser les vues (intégré)
  2. DiffUtil: Mises à jour de liste efficaces
  3. ID stables: Remplacer getItemId() et setHasStableIds(true)
  4. Prérécupération: Augmenter la distance de prérécupération
  5. Chargement d'images: Utiliser des bibliothèques comme Glide/Coil avec un dimensionnement approprié
  6. Éviter les opérations lourdes: Ne pas effectuer de calculs coûteux dans onBindViewHolder
  7. RecyclerViews imbriqués: Définir setRecycledViewPool() et setHasFixedSize(true)
class UserAdapter : ListAdapter<User, UserViewHolder>(UserDiffCallback()) {
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        val binding = ItemUserBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )
        return UserViewHolder(binding)
    }
    
    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        holder.bind(getItem(position))
    }
    
    override fun getItemId(position: Int): Long {
        return getItem(position).id.toLong()
    }
}

class UserDiffCallback : DiffUtil.ItemCallback<User>() {
    override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
        return oldItem.id == newItem.id
    }
    
    override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
        return oldItem == newItem
    }
}

// In Fragment/Activity
recyclerView.apply {
    setHasFixedSize(true)
    adapter = userAdapter
    // Increase prefetch
    (layoutManager as? LinearLayoutManager)?.initialPrefetchItemCount = 4
}

// Update list efficiently
userAdapter.submitList(newUsers)

Rareté: Très Courant Difficulté: Moyenne


13. Comment détecter et corriger les fuites de mémoire dans Android ?

Réponse: Les fuites de mémoire se produisent lorsque des objets sont conservés en mémoire plus longtemps que nécessaire.

  • Causes courantes:
    • Fuites de contexte (références Activity/Fragment)
    • Références statiques
    • Classes internes anonymes
    • Écouteurs non désenregistrés
    • Coroutines non annulées
  • Outils de détection:
    • Bibliothèque LeakCanary
    • Android Studio Memory Profiler
    • Vidages de tas
// BAD - Memory leak
class MyActivity : AppCompatActivity() {
    companion object {
        var instance: MyActivity? = null  // Static reference leaks
    }
    
    private val handler = Handler()  // Implicit reference to Activity
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        instance = this
        
        // Anonymous class holds Activity reference
        handler.postDelayed({
            // Activity might be destroyed
        }, 10000)
    }
}

// GOOD - No memory leak
class MyActivity : AppCompatActivity() {
    private val handler = Handler(Looper.getMainLooper())
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // Use weak reference if needed
        val weakRef = WeakReference(this)
        handler.postDelayed({
            weakRef.get()?.let { activity ->
                // Safe to use activity
            }
        }, 10000)
    }
    
    override fun onDestroy() {
        super.onDestroy()
        handler.removeCallbacksAndMessages(null)  // Clean up
    }
}

// ViewModel - survives configuration changes
class MyViewModel : ViewModel() {
    private val job = Job()
    private val scope = CoroutineScope(Dispatchers.Main + job)
    
    override fun onCleared() {
        super.onCleared()
        job.cancel()  // Cancel coroutines
    }
}

Rareté: Très Courant Difficulté: Moyenne


14. Comment optimiser le temps de démarrage de l'application ?

Réponse: Un démarrage plus rapide améliore l'expérience utilisateur :

  1. Initialisation paresseuse: Initialiser les objets uniquement lorsque cela est nécessaire
  2. Éviter le travail lourd dans Application.onCreate():
    • Déplacer vers un thread d'arrière-plan
    • Différer l'initialisation non critique
  3. Fournisseurs de contenu: Minimiser ou charger de manière paresseuse
  4. Réduire les dépendances: Moins de bibliothèques = démarrage plus rapide
  5. Bibliothèque de démarrage de l'application: Initialisation structurée
  6. Profils de base: Conseils de compilation anticipée
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        
        // Critical initialization only
        initCrashReporting()
        
        // Defer non-critical work
        lifecycleScope.launch {
            delay(100)
            initAnalytics()
            initImageLoader()
        }
        
        // Background initialization
        CoroutineScope(Dispatchers.IO).launch {
            preloadData()
        }
    }
}

// App Startup library
class AnalyticsInitializer : Initializer<Analytics> {
    override fun create(context: Context): Analytics {
        return Analytics.getInstance(context).apply {
            initialize()
        }
    }
    
    override fun dependencies(): List<Class<out Initializer<*>>> {
        return emptyList()
    }
}

// Lazy initialization
class MyActivity : AppCompatActivity() {
    private val database by lazy {
        Room.databaseBuilder(this, AppDatabase::class.java, "db").build()
    }
    
    // Only created when accessed
    private val heavyObject by lazy {
        createHeavyObject()
    }
}

Rareté: Courant Difficulté: Moyenne


15. Comment gérer efficacement le chargement et la mise en cache des bitmaps ?

Réponse: Une gestion efficace des images est cruciale pour les performances :

  • Bibliothèques: Glide, Coil (gèrent automatiquement la mise en cache)
  • Optimisation manuelle:
    • Sous-échantillonnage (charger des images plus petites)
    • Cache mémoire (LruCache)
    • Cache disque
    • Pool de bitmaps
// Using Glide
Glide.with(context)
    .load(imageUrl)
    .placeholder(R.drawable.placeholder)
    .error(R.drawable.error)
    .diskCacheStrategy(DiskCacheStrategy.ALL)
    .override(width, height)  // Resize
    .into(imageView)

// Manual implementation with LruCache
class ImageCache {
    private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
    private val cacheSize = maxMemory / 8
    
    private val memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
        override fun sizeOf(key: String, bitmap: Bitmap): Int {
            return bitmap.byteCount / 1024
        }
    }
    
    fun getBitmap(key: String): Bitmap? {
        return memoryCache.get(key)
    }
    
    fun putBitmap(key: String, bitmap: Bitmap) {
        if (getBitmap(key) == null) {
            memoryCache.put(key, bitmap)
        }
    }
}

// Downsample large images
fun decodeSampledBitmap(file: File, reqWidth: Int, reqHeight: Int): Bitmap {
    return BitmapFactory.Options().run {
        inJustDecodeBounds = true
        BitmapFactory.decodeFile(file.path, this)
        
        inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
        inJustDecodeBounds = false
        
        BitmapFactory.decodeFile(file.path, this)
    }
}

fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
    val (height: Int, width: Int) = options.run { outHeight to outWidth }
    var inSampleSize = 1
    
    if (height > reqHeight || width > reqWidth) {
        val halfHeight: Int = height / 2
        val halfWidth: Int = width / 2
        
        while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
            inSampleSize *= 2
        }
    }
    
    return inSampleSize
}

Rareté: Courant Difficulté: Difficile


16. Qu'est-ce qu'un ANR et comment l'éviter ?

Réponse: ANR (Application Not Responding) se produit lorsque le thread principal est bloqué trop longtemps.

  • Causes:
    • Calcul lourd sur le thread principal
    • Appels réseau sur le thread principal
    • Opérations de base de données sur le thread principal
    • Impasses
  • Prévention:
    • Déplacer le travail lourd vers les threads d'arrière-plan
    • Utiliser des coroutines avec les dispatchers appropriés
    • Éviter les blocs synchronisés sur le thread principal
    • Utiliser WorkManager pour les tâches d'arrière-plan
// BAD - Blocks main thread
class BadActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // ANR risk!
        val data = fetchDataFromNetwork()  // Blocks UI thread
        processLargeFile()  // Blocks UI thread
    }
}

// GOOD - Background processing
class GoodActivity : AppCompatActivity() {
    private val viewModel: MyViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        viewModel.loadData()  // Handled in ViewModel with coroutines
    }
}

class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {
            // Automatically on Main dispatcher
            showLoading()
            
            // Switch to IO dispatcher for network
            val data = withContext(Dispatchers.IO) {
                fetchDataFromNetwork()
            }
            
            // Switch to Default dispatcher for heavy computation
            val processed = withContext(Dispatchers.Default) {
                processData(data)
            }
            
            // Back to Main for UI update
            updateUI(processed)
        }
    }
}

Rareté: Courant Difficulté: Facile


Tests (3 Questions)

17. Comment écrire des tests unitaires pour les ViewModels ?

Réponse: Les ViewModels doivent être testés isolément avec des dépendances simulées.

class UserViewModelTest {
    @get:Rule
    val instantExecutorRule = InstantTaskExecutorRule()
    
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()
    
    private lateinit var viewModel: UserViewModel
    private lateinit var repository:
Newsletter subscription

Conseils de carrière hebdomadaires qui fonctionnent vraiment

Recevez les dernières idées directement dans votre boîte de réception

Decorative doodle

Créez un CV qui Vous Fait Embaucher 60% Plus Vite

En quelques minutes, créez un CV personnalisé et compatible ATS qui a prouvé obtenir 6 fois plus d'entretiens.

Créer un meilleur CV

Partager cet article

Réduisez Votre Temps de Rédaction de CV de 90%

Le chercheur d'emploi moyen passe plus de 3 heures à formater un CV. Notre IA le fait en moins de 15 minutes, vous permettant d'atteindre la phase de candidature 12 fois plus rapidement.