dicembre 21, 2025
17 min di lettura

Domande di colloquio per Senior Mobile Developer (Android): Guida Completa

interview
career-advice
job-search
Domande di colloquio per Senior Mobile Developer (Android): Guida Completa
MB

Milad Bonakdar

Autore

Padroneggia lo sviluppo Android avanzato con domande di colloquio essenziali che coprono pattern architetturali, ottimizzazione delle prestazioni, dependency injection, testing, sicurezza e progettazione del sistema per sviluppatori senior.


Introduzione

Ci si aspetta che gli sviluppatori Android senior progettino applicazioni scalabili e manutenibili, garantendo al contempo prestazioni elevate e qualità del codice. Questo ruolo richiede una profonda competenza nei framework Android, nei pattern architetturali, nell'injection delle dipendenze, nelle strategie di testing e la capacità di prendere decisioni tecniche informate.

Questa guida completa copre le domande essenziali per i colloqui per sviluppatori Android senior, spaziando tra concetti avanzati di Kotlin, pattern architetturali, ottimizzazione delle prestazioni, injection delle dipendenze, testing e progettazione del sistema. Ogni domanda include risposte dettagliate, valutazione della rarità e valutazioni della difficoltà.


Kotlin Avanzato e Funzionalità del Linguaggio (5 Domande)

1. Spiega le Coroutine di Kotlin e i loro vantaggi rispetto ai thread.

Risposta: Le coroutine sono primitive di concorrenza leggere che consentono di scrivere codice asincrono in modo sequenziale.

  • Vantaggi rispetto ai Thread:
    • Leggere: È possibile creare migliaia di coroutine senza problemi di prestazioni
    • Concorrenza Strutturata: La relazione padre-figlio garantisce una corretta pulizia
    • Supporto alla Cancellazione: Propagazione della cancellazione integrata
    • Gestione delle Eccezioni: Gestione strutturata delle eccezioni
  • Componenti Chiave:
    • CoroutineScope: Definisce il ciclo di vita
    • Dispatcher: Controlla il contesto di esecuzione (Main, IO, Default)
    • Funzioni suspend: Possono essere messe in pausa e riprese
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)
            }
        }
    }
}

Rarità: Molto Comune Difficoltà: Difficile


2. Cosa sono le Sealed Class e quando dovresti usarle?

Risposta: Le sealed class rappresentano gerarchie di classi ristrette in cui tutte le sottoclassi sono note in fase di compilazione.

  • Vantaggi:
    • Espressioni when esaustive
    • Gestione dello stato type-safe
    • Migliori degli enum per dati complessi
  • Casi d'Uso: Rappresentare stati, risultati, eventi di navigazione
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 ?: "Errore sconosciuto")
            }
        }
    }
}

// In UI
when (val state = uiState.value) {
    is UiState.Loading -> showLoading()
    is UiState.Success -> showData(state.data)
    is UiState.Error -> showError(state.message)
    // Il compilatore garantisce che tutti i casi siano gestiti
}

Rarità: Comune Difficoltà: Media


3. Spiega Kotlin Flow e come differisce da LiveData.

Risposta: Flow è lo stream asincrono freddo di Kotlin che emette valori in sequenza.

  • Flow vs LiveData:
    • Flow: Stream freddo, supporta operatori, non è lifecycle-aware, più flessibile
    • LiveData: Stream caldo, lifecycle-aware, specifico per Android, più semplice per l'UI
  • Tipi di Flow:
    • Flow: Stream freddo (inizia alla collection)
    • StateFlow: Stream caldo con stato corrente
    • SharedFlow: Stream caldo per eventi
class UserRepository {
    // Cold Flow - inizia quando viene raccolto
    fun getUsers(): Flow<List<User>> = flow {
        val users = api.fetchUsers()
        emit(users)
    }.flowOn(Dispatchers.IO)
    
    // StateFlow - hot, contiene lo stato
    private val _userState = MutableStateFlow<List<User>>(emptyList())
    val userState: StateFlow<List<User>> = _userState.asStateFlow()
    
    // SharedFlow - hot, per eventi
    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)
        }
    }
}

Rarità: Molto Comune Difficoltà: Difficile


4. Cosa sono le Inline Function e quando dovresti usarle?

Risposta: Le inline function copiano il corpo della funzione nel punto di chiamata, evitando l'overhead della chiamata di funzione.

  • Vantaggi:
    • Elimina l'overhead di allocazione della lambda
    • Consente return non locali dalle lambda
    • Prestazioni migliori per le funzioni di ordine superiore
  • Casi d'Uso: Funzioni di ordine superiore con parametri lambda
  • Compromesso: Aumenta la dimensione del codice
// Senza inline - crea un oggetto lambda
fun measureTime(block: () -> Unit) {
    val start = System.currentTimeMillis()
    block()
    val end = System.currentTimeMillis()
    println("Tempo: ${end - start}ms")
}

// Con inline - nessun oggetto lambda creato
inline fun measureTimeInline(block: () -> Unit) {
    val start = System.currentTimeMillis()
    block()
    val end = System.currentTimeMillis()
    println("Tempo: ${end - start}ms")
}

// noinline - impedisce che un parametro specifico venga inline
inline fun performOperation(
    inline operation: () -> Unit,
    noinline logger: () -> Unit
) {
    operation()
    saveLogger(logger)  // Può memorizzare lambda noinline
}

// crossinline - consente l'inline ma impedisce i return non locali
inline fun runAsync(crossinline block: () -> Unit) {
    thread {
        block()  // Non è possibile tornare dalla funzione esterna
    }
}

Rarità: Media Difficoltà: Difficile


5. Spiega la Delegation in Kotlin.

Risposta: La delegation consente a un oggetto di delegare alcune delle sue responsabilità a un altro oggetto.

  • Delegation di Classe: Parola chiave by
  • Delegation di Proprietà: Lazy, observable, delegates
  • Vantaggi: Riutilizzo del codice, composizione sull'ereditarietà
// Delegation di classe
interface Repository {
    fun getData(): String
}

class RemoteRepository : Repository {
    override fun getData() = "Dati remoti"
}

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

// Delegation di proprietà
class User {
    // Inizializzazione lazy
    val database by lazy {
        Room.databaseBuilder(context, AppDatabase::class.java, "db").build()
    }
    
    // Proprietà osservabile
    var name: String by Delegates.observable("Iniziale") { prop, old, new ->
        println("$old -> $new")
    }
    
    // Delegate personalizzato
    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()
    }
}

Rarità: Media Difficoltà: Media


Pattern Architetturali (6 Domande)

6. Spiega l'architettura MVVM e i suoi vantaggi.

Risposta: MVVM (Model-View-ViewModel) separa la logica dell'UI dalla logica di business.

Loading diagram...
  • Model: Livello dati (repository, data source)
  • View: Livello UI (Activity, Fragment, Composable)
  • ViewModel: Logica di presentazione, sopravvive ai cambi di configurazione
  • Vantaggi: Testabile, separazione delle preoccupazioni, lifecycle-aware
// 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 ?: "Errore")
            }
        }
    }
}

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

Rarità: Molto Comune Difficoltà: Media


7. Cosa è la Clean Architecture e come la implementi in Android?

Risposta: La Clean Architecture separa il codice in livelli con dipendenze chiare.

Loading diagram...
  • Presentation: UI, ViewModel
  • Domain: Use Case, Logica di Business, Entità
  • Data: Repository, Data Source (API, Database)
  • Dependency Rule: I livelli interni non conoscono i livelli esterni
// Domain Layer - Entità
data class User(val id: Int, val name: String, val email: String)

// Domain Layer - Interfaccia Repository
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 - Implementazione Repository
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)
            }
        }
    }
}

Rarità: Comune Difficoltà: Difficile


8. Spiega la Dependency Injection e Dagger/Hilt.

Risposta: La Dependency Injection fornisce dipendenze alle classi invece di crearle internamente.

  • Vantaggi: Testabilità, basso accoppiamento, riusabilità
  • Dagger: Framework DI in fase di compilazione
  • Hilt: Dagger semplificato per Android
// Definisci le dipendenze con 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 con dipendenze iniettate
@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 con repository iniettato
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repository: UserRepository
) : ViewModel() {
    // Logica del ViewModel
}

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

Rarità: Molto Comune Difficoltà: Difficile


9. Cosa è il pattern Repository e perché usarlo?

Risposta: Il pattern Repository astrae le fonti di dati, fornendo un'API pulita per l'accesso ai dati.

  • Vantaggi:
    • Singola fonte di verità
    • Logica dei dati centralizzata
    • Facile da cambiare le fonti di dati
    • Testabile
  • Implementazione: Coordina tra più fonti di dati
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 {
        // Emetti prima i dati memorizzati nella cache
        val cachedUsers = cacheDataSource.getUsers()
        if (cachedUsers.isNotEmpty()) {
            emit(cachedUsers)
        }
        
        // Recupera dal database locale
        val localUsers = localDataSource.getUsers()
        if (localUsers.isNotEmpty()) {
            emit(localUsers)
            cacheDataSource.saveUsers(localUsers)
        }
        
        // Recupera da remoto
        try {
            val remoteUsers = remoteDataSource.fetchUsers()
            localDataSource.saveUsers(remoteUsers)
            cacheDataSource.saveUsers(remoteUsers)
            emit(remoteUsers)
        } catch (e: Exception) {
            // Se il remoto fallisce, abbiamo già emesso dati memorizzati nella cache/locale
        }
    }
    
    override suspend fun getUser(id: Int): User? {
        return cacheDataSource.getUser(id)
            ?: localDataSource.getUser(id)
            ?: remoteDataSource.fetchUser(id)?.also {
                localDataSource.saveUser(it)
                cacheDataSource.saveUser(it)
            }
    }
}

Rarità: Molto Comune Difficoltà: Media


10. Spiega l'architettura Single Activity.

Risposta: L'architettura Single Activity utilizza una sola Activity con più Fragment, gestiti dal Navigation Component.

  • Vantaggi:
    • Navigazione semplificata
    • ViewModel condivisi tra i fragment
    • Animazioni migliori
    • Deep linking più facile
  • Navigation Component: Gestisce le transazioni dei fragment, lo stack di back, gli argomenti
// Grafico di navigazione (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 - naviga al dettaglio
class HomeFragment : Fragment() {
    fun navigateToDetail(userId: Int) {
        val action = HomeFragmentDirections.actionHomeToDetail(userId)
        findNavController().navigate(action)
    }
}

// DetailFragment - ricevi argomenti
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)
    }
}

Rarità: Comune Difficoltà: Media


11. Cosa è l'architettura MVI (Model-View-Intent)?

Risposta: MVI è un'architettura di flusso di dati unidirezionale ispirata a Redux.

  • Componenti:
    • Model: Rappresenta lo stato dell'UI
    • View: Renderizza lo stato, emette intent
    • Intent: Azioni/eventi dell'utente
  • Vantaggi: Stato prevedibile, debugging più facile, time-travel debugging
// State
data class UserScreenState(
    val isLoading: Boolean = false,
    val user: User? = null,
    val error: String? = null
)

// Intent (azioni dell'utente)
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)
        
        // Invia intent
        viewModel.processIntent(UserIntent.LoadUser(123))
        
        // Osserva lo stato
        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)
        }
    }
}

Rarità: Media Difficoltà: Difficile


Performance & Ottimizzazione (5 Domande)

12. Come ottimizzi le prestazioni di RecyclerView?

Risposta: Molteplici strategie migliorano le prestazioni di scorrimento di RecyclerView:

  1. ViewHolder Pattern: Riutilizza le viste (integrato)
  2. DiffUtil: Aggiornamenti efficienti della lista
  3. ID Stabili: Sovrascrivi getItemId() e setHasStableIds(true)
  4. Prefetching: Aumenta la distanza di prefetch
  5. Caricamento delle Immagini: Usa librerie come Glide/Coil con il corretto dimensionamento
  6. Evita Operazioni Pesanti: Non eseguire calcoli costosi in onBindViewHolder
  7. RecyclerView Nidificati: Imposta setRecycledViewPool() e 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
    // Aumenta il prefetch
    (layoutManager as? LinearLayoutManager)?.initialPrefetchItemCount = 4
}

// Aggiorna la lista in modo efficiente
userAdapter.submitList(newUsers)

Rarità: Molto Comune Difficoltà: Media


13. Come rilevi e correggi le memory leak in Android?

Risposta: Le memory leak si verificano quando gli oggetti vengono mantenuti in memoria più a lungo del necessario.

  • Cause Comuni:
    • Leak di contesto (riferimenti Activity/Fragment)
    • Riferimenti statici
    • Classi interne anonime
    • Listener non deregistrati
    • Coroutine non cancellate
  • Strumenti di Rilevamento:
    • Libreria LeakCanary
    • Android Studio Memory Profiler
    • Heap dumps
// SBAGLIATO - Memory leak
class MyActivity : AppCompatActivity() {
    companion object {
        var instance: MyActivity? = null  // Il riferimento statico provoca una leak
    }
    
    private val handler = Handler()  // Riferimento implicito all'Activity
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        instance = this
        
        // La classe anonima mantiene il riferimento all'Activity
        handler.postDelayed({
            // L'Activity potrebbe essere distrutta
        }, 10000)
    }
}

// GIUSTO - Nessuna memory leak
class MyActivity : AppCompatActivity() {
    private val handler = Handler(Looper.getMainLooper())
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // Usa un riferimento debole se necessario
        val weakRef = WeakReference(this)
        handler.postDelayed({
            weakRef.get()?.let { activity ->
                // Sicuro da usare l'activity
            }
        }, 10000)
    }
    
    override fun onDestroy() {
        super.onDestroy()
        handler.removeCallbacksAndMessages(null)  // Pulisci
    }
}

// ViewModel - sopravvive ai cambi di configurazione
class MyViewModel : ViewModel() {
    private val job = Job()
    private val scope = CoroutineScope(Dispatchers.Main + job)
    
    override fun onCleared() {
        super.onCleared()
        job.cancel()  // Cancella le coroutine
    }
}

Rarità: Molto Comune Difficoltà: Media


14. Come ottimizzi il tempo di avvio dell'app?

Risposta: Un avvio più veloce migliora l'esperienza dell'utente:

  1. Inizializzazione Lazy: Inizializza gli oggetti solo quando necessario
  2. Evita Lavoro Pesante in Application.onCreate():
    • Sposta su un thread in background
    • Rimanda l'inizializzazione non critica
  3. Content Provider: Minimizza o carica in modo lazy
  4. Riduci le Dipendenze: Meno librerie = avvio più veloce
  5. App Startup Library: Inizializzazione strutturata
  6. Baseline Profiles: Suggerimenti di compilazione ahead-of-time
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        
        // Solo inizializzazione critica
        initCrashReporting()
        
        // Rimanda il lavoro non critico
        lifecycleScope.launch {
            delay(100)
            initAnalytics()
            initImageLoader()
        }
        
        // Inizializzazione in background
        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()
    }
}

// Inizializzazione lazy
class MyActivity : AppCompatActivity() {
    private val database by lazy {
        Room.databaseBuilder(this, AppDatabase::class.java, "db").build()
    }
    
    // Creato solo quando viene acceduto
    private val heavyObject by lazy {
        createHeavyObject()
    }
}

Rarità: Comune Difficoltà: Media


15. Come gestisci il caricamento e la caching delle bitmap in modo efficiente?

Risposta: Una gestione efficiente delle immagini è fondamentale per le prestazioni:

  • Librerie: Glide, Coil (gestiscono automaticamente la caching)
  • Ottimizzazione Manuale:
    • Downsampling (carica immagini più piccole)
    • Cache di memoria (LruCache)
    • Cache su disco
    • Bitmap pooling
// Usando Glide
Glide.with(context)
    .load(imageUrl)
    .placeholder(R.drawable.placeholder)
    .error(R.drawable.error)
    .diskCacheStrategy(DiskCacheStrategy.ALL)
    .override(width, height)  // Ridimensiona
    .into(imageView)

// Implementazione manuale con 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 le immagini grandi
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
}

Rarità: Comune Difficoltà: Difficile


16. Cosa è un ANR e come lo previeni?

Risposta: ANR (Application Not Responding) si verifica quando il thread principale è bloccato troppo a lungo.

  • Cause:
    • Calcoli pesanti sul thread principale
    • Chiamate di rete sul thread principale
    • Operazioni di database sul thread principale
    • Deadlock
  • Prevenzione:
    • Sposta il lavoro pesante su thread in background
    • Usa coroutine con i dispatcher corretti
    • Evita blocchi synchronized sul thread principale
    • Usa WorkManager per le attività in background
// SBAGLIATO - Blocca il thread principale
class BadActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // Rischio ANR!
        val data = fetchDataFromNetwork()  // Blocca il thread dell'UI
        processLargeFile()  // Blocca il thread dell'UI
    }
}

// GIUSTO - Elaborazione in background
class GoodActivity : AppCompatActivity() {
    private val viewModel: MyViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        viewModel.loadData()  // Gestito in ViewModel con coroutine
    }
}

class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {
            // Automaticamente sul dispatcher Main
            showLoading()
            
            // Passa al dispatcher IO per la rete
            val data = withContext(Dispatchers.IO) {
                fetchDataFromNetwork()
            }
            
            // Passa al dispatcher Default per calcoli pesanti
            val processed = withContext(Dispatchers.Default) {
                processData(data)
            }
            
            // Torna a Main per l'aggiornamento dell'UI
            updateUI(processed)
        }
    }
}

Rarità: Comune Difficoltà: Facile


Testing (3 Domande)

17. Come scrivi unit test per i ViewModel?

Risposta: I ViewModel devono essere testati in isolamento con dip

Newsletter subscription

Consigli di carriera settimanali che funzionano davvero

Ricevi le ultime idee direttamente nella tua casella di posta

Decorative doodle

Il Tuo Prossimo Colloquio Dista Solo un Curriculum

Crea un curriculum professionale e ottimizzato in pochi minuti. Non servono competenze di design—solo risultati comprovati.

Crea il mio curriculum

Condividi questo post

Vieni Assunto il 50% Più Velocemente

Le persone in cerca di lavoro che utilizzano curriculum professionali migliorati dall'IA trovano ruoli in una media di 5 settimane rispetto alle 10 standard. Smetti di aspettare e inizia a fare colloqui.