Preparazione al colloquio Mobile Developer

Domande di colloquio per Mobile Developer

20 domande frequenti di colloquio per Mobile Developer. Lo sviluppo mobile è un ambito molto richiesto incentrato su app iOS, Android e cross-platform affidabili, con problemi di prodotto, performance, release, offline e integrazione con i dispositivi. Le domande coprono livelli diversi e puoi esercitarti a rispondere a voce nel nostro simulatore di colloquio.

Inizia un colloquio AI per Mobile DeveloperNessuna carta di credito richiesta. 1 sessione gratuita disponibile.
Pratica per colloqui tecnici in ingleseUna modalità in cui chi non è madrelingua può esercitarsi a superare i colloqui tecnici.

Domande per principianti

1Qual è la differenza tra la memorizzazione di un token di autenticazione in uno storage chiave-valore standard come SharedPreferences o UserDefaults e uno storage sicuro di piattaforma come Android Keystore o iOS Keychain?

I meccanismi di archiviazione standard come `SharedPreferences` di Android e `UserDefaults` di iOS sono progettati per preferenze leggere e non sensibili. Memorizzano i dati in file di testo non crittografati (XML o plist) all'interno della directory sandbox dell'applicazione. Chiunque abbia accesso fisico a un dispositivo con root o jailbreak, a un backup non crittografato o al file system può leggere questi token direttamente. Al contrario, gli archivi sicuri forniti dalla piattaforma — nello specifico iOS Keychain e Android Keystore (utilizzato di frequente tramite `EncryptedSharedPreferences`) — offrono la crittografia dei dati a riposo (data-at-rest). iOS Keychain cifra gli elementi archiviati usando chiavi legate all'hardware del dispositivo (come il Secure Enclave) e consente policy di accesso a grana fine. Android Keystore genera e memorizza le chiavi crittografiche all'interno di moduli di sicurezza isolati a livello hardware (Trusted Execution Environment o StrongBox), garantendo che le chiavi crittografiche non siano mai esposte nella memoria dell'applicazione e non possano essere estratte dal file system.

// --- ANDROID ---
// INSECURE (Plain SharedPreferences):
val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
prefs.edit().putString("auth_token", token).apply() // Plaintext XML on disk

// SECURE (EncryptedSharedPreferences backed by Android Keystore):
val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .build()
val securePrefs = EncryptedSharedPreferences.create(
    context,
    "secure_prefs",
    masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
securePrefs.edit().putString("auth_token", token).apply()

// --- IOS (Swift) ---
// INSECURE:
UserDefaults.standard.set(token, forKey: "auth_token") // Plain plist on disk

// SECURE (Keychain Services API):
let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "user_auth_token",
    kSecValueData as String: token.data(using: .utf8)!,
    kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
]
SecItemAdd(query as CFDictionary, nil)
Prova a rispondere a questa domanda con un coach IA

2Qual è lo scopo principale di una pipeline di CI/CD (Continuous Integration/Continuous Delivery) per dispositivi mobili e quali fasi e vincoli specifici la distinguono dalle pipeline di deployment per il web o per il backend?

Lo scopo principale di una pipeline di CI/CD per dispositivi mobili è automatizzare le fasi di compilazione (build), test, firma e distribuzione delle applicazioni client mobili per mantenere una qualità costante e rilasci ripetibili. La Continuous Integration (CI) valida le modifiche al codice tramite build automatiche, analisi statica e test unitari. La Continuous Delivery/Deployment (CD) impacchetta, firma e distribuisce gli artefatti binari a canali di test (come TestFlight o Firebase App Distribution) o agli app store. La CI/CD mobile differisce dalle pipeline web e backend per diversi aspetti chiave: 1. Tipo di artefatto: le build producono binari client compilati (.ipa, .apk, .aab) anziché essere eseguiti direttamente su server o all'interno di immagini di container. 2. Vincoli hardware del runner: la compilazione per iOS richiede hardware macOS e gli strumenti da riga di comando di Xcode, mentre le pipeline backend in genere vengono eseguite su container Linux leggeri. 3. Latenza di rilascio e approvazioni: la pubblicazione verso gli utenti finali comporta cicli di revisione degli app store di terze parti (Apple App Store / Google Play Store), il che significa che i deployment non possono essere annullati istantaneamente tramite un nuovo deploy su server. Le correzioni richiedono l'invio di un nuovo binario firmato o l'uso di feature flag a runtime.

name: Mobile CI/CD Workflow

on:
  pull_request:
    branches: [main]
  push:
    tags:
      - 'v*.*.*'

jobs:
  ci_validation:
    name: Lint & Unit Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Linter
        run: ./gradlew lint
      - name: Run Unit Tests
        run: ./gradlew testDebugUnitTest

  cd_release_ios:
    name: Build & Distribute iOS
    needs: ci_validation
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - name: Install Signing Certificates
        run: fastlane match appstore --readonly
      - name: Build & Upload to TestFlight
        run: fastlane ios beta
Prova a rispondere a questa domanda con un coach IA

3In un'applicazione mobile multipiattaforma, qual è lo scopo di separare i livelli di presentazione, di dominio e di dati, e in che modo questa struttura facilita la condivisione del codice tra iOS e Android?

Separare un'applicazione nei layer di presentazione (presentation), dominio (domain) e dati (data) stabilisce confini di responsabilità netti: 1. **Livello di presentazione (Presentation Layer):** gestisce il rendering della UI, l'input dell'utente e lo stato a livello di schermata (ad es. View, Widget, ViewModel o Presenter). 2. **Livello di dominio (Domain Layer):** incapsula la pura logica di business, i modelli/entità di dominio e i casi d'uso (use case). Rimane indipendente dalla UI e dai framework di piattaforma. 3. **Livello dati (Data Layer):** gestisce il recupero e la persistenza dei dati da API remote, database locali o cache del dispositivo tramite repository e sorgenti di dati (data sources). Nello sviluppo mobile multipiattaforma, questa struttura facilita la condivisione del codice poiché i layer di dominio e di dati sono agnostici rispetto alla piattaforma. Le regole di business fondamentali, le trasformazioni dei dati e la gestione della rete possono essere condivise al 100% tra iOS e Android, consentendo ai team di condividere l'intero stack (come in Flutter o React Native) o di condividere la logica di business e di dati mantenendo livelli di presentazione nativi specifici per piattaforma (come in Kotlin Multiplatform).

[Presentation Layer] (UI, Screens, ViewModels)
         │
         ▼ calls
[Domain Layer]       (Use Cases, Business Rules, Entities)
         ▲
         │ implements interfaces
[Data Layer]         (Repositories, API Clients, Local Storage)
Prova a rispondere a questa domanda con un coach IA

4Quali sono le differenze fondamentali tra i paradigmi di sviluppo UI (User Interface) imperativo e dichiarativo nell'ingegneria mobile, e in che modo i widget di Flutter e i componenti di React Native incarnano il modello dichiarativo?

Nello sviluppo UI imperativo, gli sviluppatori scrivono istruzioni esplicite passo dopo passo per creare, modificare e distruggere gli elementi dell'interfaccia (come cercare una vista per ID e chiamare direttamente metodi quali setText o setVisibility). Al contrario, i modelli UI dichiarativi descrivono l'aspetto che l'interfaccia deve avere per un determinato stato (spesso espresso come UI = f(state)). Quando lo stato cambia, il framework determina come aggiornare la rappresentazione visiva in modo efficiente. Sia Flutter che React Native incarnano questo paradigma dichiarativo. In Flutter, i widget sono descrizioni di configurazione immutabili; quando i dati dinamici cambiano, la chiamata a setState() in uno StatefulWidget pianifica una ricostruzione in cui il metodo build() restituisce una nuova descrizione dell'albero dei widget che Flutter riconcilia con i suoi alberi Element e RenderObject. In React Native, i componenti sono funzioni o classi che restituiscono JSX; l'aggiornamento di state o props attiva un re-render in cui React riconcilia l'albero virtuale degli elementi e applica i minimi aggiornamenti nativi necessari attraverso il bridge o il runtime nativo.

class CounterWidget extends StatefulWidget {
  const CounterWidget({super.key});
  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: _increment,
      child: Text('Count: $_count'),
    );
  }
}
Prova a rispondere a questa domanda con un coach IA

5Qual è la differenza operativa tra un cold start, un warm start e un hot start nelle applicazioni mobile, e perché il cold start è la metrica più critica per i budget di prestazioni?

Nel ciclo di vita delle applicazioni mobile, gli stati di avvio differiscono in base alla presenza o meno del processo dell'app e del suo stato in memoria nel sistema operativo (OS, Operating System). - Cold Start (avvio a freddo): l'app si avvia da zero perché l'OS non ha ancora creato il processo, oppure il processo è stato terminato in precedenza. L'OS deve allocare un nuovo processo, caricare binari/runtime, inizializzare il contesto di runtime/Application ed eseguire l'inflate della prima vista. Presenta la latenza più elevata. - Warm Start (avvio a caldo intermedio): il processo dell'app è generalmente già in memoria, ma la gerarchia di activity/view è stata distrutta o rimossa (ad esempio a causa della ricreazione in background o della navigazione a ritroso). L'OS ricrea la UI/activity senza dover generare un nuovo processo da zero. - Hot Start (avvio a caldo): l'app e lo stato della relativa interfaccia utente risiedono ancora interamente in memoria (ad esempio se l'utente ha premuto Home ed è tornato subito all'app). L'OS porta semplicemente la gerarchia di viste esistente in primo piano con un overhead di inizializzazione quasi nullo. Il cold start è la metrica più critica per i budget di prestazioni perché rappresenta la prima impressione, nonché l'esperienza più lenta per gli utenti. Tempi lunghi di cold start si correlano direttamente con l'abbandono immediato da parte degli utenti, una minore fidelizzazione e valutazioni negative negli store di applicazioni (come i limiti di Android Vitals).

Cold Start: [OS Process Spawn] -> [App Runtime Init] -> [Activity/UI Creation] -> [First Frame Drawn]
Warm Start: [Existing Process]  -> [Activity/UI Recreation] -> [First Frame Drawn]
Hot Start:  [Existing Process]  -> [Bring Existing View to Foreground] (Instant)
Prova a rispondere a questa domanda con un coach IA

6Che cos'è un native bridge in un'applicazione mobile multipiattaforma e quando è opportuno utilizzarne uno invece di implementare la funzionalità interamente con codice Flutter o React Native?

Un native bridge è il livello di comunicazione che consente al codice multipiattaforma di chiamare codice specifico per la piattaforma iOS o Android, e permette al codice nativo di restituire risultati o inviare eventi. In Flutter questo viene comunemente gestito tramite platform channel o plugin. In React Native viene in genere realizzato con moduli nativi, TurboModules o componenti UI nativi. Si utilizza un bridge quando una funzionalità richiede elementi a cui il puro codice Flutter o React Native non può accedere in modo adeguato, come un'API (Application Programming Interface) di piattaforma, funzionalità del dispositivo, un SDK (Software Development Kit) nativo, un'implementazione nativa critica per le prestazioni o un componente UI nativo. Esempi comuni includono le funzionalità della fotocamera, il Bluetooth, le notifiche push, i pagamenti, lo storage sicuro, i servizi in background, le API per la salute o gli SDK che forniscono solo integrazioni in Swift/Objective-C o Kotlin/Java. Un buon bridge espone solitamente un'API snella e chiara al livello multipiattaforma, come getBatteryLevel, startBluetoothScan o openNativePaymentSheet, mentre l'implementazione specifica della piattaforma gestisce i dettagli concreti di iOS e Android.

Cross-platform screen:
  calls NativePayments.openPaymentSheet(orderId)

iOS implementation:
  uses Apple Pay / native payment SDK

Android implementation:
  uses Google Pay / native payment SDK
Prova a rispondere a questa domanda con un coach IA

7Cosa si intende per architettura offline-first nello sviluppo mobile e in che modo si differenzia fondamentalmente dal caching standard delle risposte HTTP (Hypertext Transfer Protocol)?

Nello sviluppo mobile, un'architettura offline-first considera lo storage locale come l'unica fonte di verità (single source of truth) primaria sia per le operazioni di lettura che di scrittura. Anziché attendere il completamento delle richieste di rete prima di effettuare il rendering o consentire le interazioni dell'utente, l'applicazione interagisce direttamente con il database locale o il livello di persistenza, mentre processi di sincronizzazione in background gestiscono la riconciliazione delle modifiche locali con il server remoto non appena la connettività è disponibile. Questo approccio si differenzia fondamentalmente dal caching convenzionale delle risposte HTTP per diversi aspetti: 1. **Modello dei dati**: il caching HTTP memorizza risposte di rete grezze (ad es. payload JSON) indicizzate tramite URL di richiesta e intestazioni. L'architettura offline-first memorizza entità di dominio strutturate in un database locale (come Room, SQLite o SwiftData/Core Data). 2. **Scritture rispetto a letture**: il caching HTTP è principalmente un meccanismo di ottimizzazione della lettura e non supporta nativamente transazioni di scrittura o mutazioni offline. L'offline-first consente scritture locali complete, accodando le modifiche per la successiva sincronizzazione con il server. 3. **Interrogabilità e ciclo di vita**: le risposte HTTP memorizzate nella cache sono soggette a criteri di rimozione automatica (eviction) e non possono essere interrogate arbitrariamente, filtrate o unite tramite join. La persistenza offline-first offre funzionalità complete di interrogazione, indicizzazione e gestione deterministica del ciclo di vita indipendentemente dallo stato della rete.

/* Network-First with HTTP Caching */
UI -> Network Request -> [Cache Hit ? Return Cached JSON : Fetch from API -> Save to HTTP Cache] -> UI
// Limitation: Read-only optimization; offline writes fail immediately.

/* Offline-First with Local Source of Truth */
UI <-> Observes Local DB (e.g., Room / Core Data / SQLite)
User Write -> Write to Local DB -> Queue Sync Task
Sync Worker (Background) -> Send queued mutations to Server -> Update Local DB with Server Response
Prova a rispondere a questa domanda con un coach IA

8Qual è la differenza tra una notifica push remota e una notifica locale in un'applicazione mobile?

La differenza principale tra notifiche push locali e remote risiede nel punto di origine e nella modalità con cui vengono attivate: 1. Le notifiche locali vengono create, pianificate e attivate interamente sul dispositivo dall'applicazione tramite le API del sistema operativo (come UserNotifications su iOS o AlarmManager/WorkManager/NotificationManager su Android). Si attivano in base a condizioni lato dispositivo, quali una data/ora specifica, un timer di conto alla rovescia o un perimetro geografico (geofencing). Poiché vengono gestite localmente dal daemon del sistema operativo, non richiedono un server di backend né una connessione Internet attiva al momento della consegna. 2. Le notifiche push remote hanno origine da un server applicativo esterno e vengono trasmesse via Internet attraverso i gateway push della piattaforma (APNs per i dispositivi Apple, FCM per Android). Il gateway invia il payload al daemon a livello di sistema operativo del dispositivo, che risveglia l'app o mostra un banner. Le notifiche remote sono necessarie per eventi esterni in tempo reale, come messaggi di chat in arrivo, richieste di amicizia o notizie dell'ultima ora.

let content = UNMutableNotificationContent()
content.title = "Workout Reminder"
content.body = "Time for your daily exercise routine."
content.sound = .default

// Triggers locally after 1 hour (3600 seconds) without any server interaction
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3600, repeats: false)
let request = UNNotificationRequest(identifier: "dailyWorkout", content: content, trigger: trigger)

UNUserNotificationCenter.current().add(request) { error in
    if let error = error {
        print("Failed to schedule notification: \(error)")
    }
}
Prova a rispondere a questa domanda con un coach IA

9Qual è la differenza strutturale e operativa tra un Android App Bundle (.aab), un APK (Android Package) e un archivio iOS IPA (iOS App Store Package) durante la distribuzione delle app tramite app store?

Un APK (Android Package) è un formato di pacchetto eseguibile contenente bytecode DEX compilato, risorse, asset e librerie native che può essere installato ed eseguito direttamente su un dispositivo Android. Un APK universale include le risorse per tutte le densità di schermo, architetture CPU e lingue, comportando dimensioni di download maggiori. Un Android App Bundle (.aab) è un formato di caricamento e pubblicazione richiesto da Google Play per la distribuzione sullo store. Un file AAB non può essere installato direttamente su un dispositivo; Google Play utilizza invece il bundle e Play App Signing per generare split APK ottimizzati e personalizzati dinamicamente in base all'architettura, alla densità dello schermo e alla lingua dello specifico dispositivo (Dynamic Delivery). Un file iOS IPA (.ipa) è un archivio di applicazione (un contenitore zip che racchiude la directory Payload, il bundle `.app`, gli asset di firma e i metadati). Quando viene caricato su App Store Connect, Apple applica l'App Thinning (come l'App Slicing) per distribuire solo i binari e gli asset necessari per il dispositivo che effettua il download. Dal punto di vista operativo, gli APK e gli IPA possono essere installati direttamente su dispositivi fisici di test (previa firma e provisioning), mentre un AAB deve essere prima convertito in split APK (ad esempio tramite `bundletool`) prima dell'installazione locale sul dispositivo.

# 1. Standalone APK installs directly via ADB
adb install myapp-universal.apk

# 2. AAB requires bundletool to generate device-tailored split APKs
bundletool build-apks --bundle=myapp.aab --output=myapp.apks --connected-device

# 3. Install the generated split APK set onto the connected device
bundletool install-apks --apks=myapp.apks
Prova a rispondere a questa domanda con un coach IA

10Quale protezione offre il protocollo TLS (Transport Layer Security) per le chiamate di rete di un'applicazione mobile e in che modo i moderni sistemi operativi mobili applicano impostazioni predefinite di trasporto sicuro?

Il protocollo TLS (Transport Layer Security) protegge le chiamate di rete delle applicazioni mobili fornendo tre garanzie fondamentali: riservatezza (cifrando i dati in transito in modo che eventuali intercettatori non possano leggere payload o header), integrità dei dati (rilevando qualsiasi manomissione o modifica di richieste e risposte durante il transito) e autenticazione del server (validando il certificato digitale del server rispetto ad Certificate Authority attendibili per prevenire attacchi Man-in-the-Middle). I moderni sistemi operativi mobili impongono il trasporto sicuro per impostazione predefinita bloccando il traffico HTTP in chiaro non cifrato: 1. iOS applica App Transport Security (ATS), richiedendo che le connessioni di rete (come quelle tramite URLSession) utilizzino HTTPS con TLS 1.2+ a meno che non siano definite esplicitamente eccezioni per i domini in Info.plist. 2. Android (API 28+) disabilita per impostazione predefinita il traffico HTTP in chiaro (`cleartextTrafficPermitted=false`). L'autorizzazione di traffico HTTP non cifrato richiede eccezioni esplicite tramite Network Security Configuration (`network_security_config.xml`) o l'attributo del manifest `android:usesCleartextTraffic`.

<!-- Android: res/xml/network_security_config.xml -->
<network-security-config>
    <!-- Cleartext HTTP blocked by default. Exceptions require explicit declaration -->
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">api.example.com</domain>
    </domain-config>
</network-security-config>

<!-- iOS: Info.plist ATS configuration -->
<!-- ATS blocks HTTP by default; exceptions must be declared inside NSAppTransportSecurity -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <false/>
</dict>
Prova a rispondere a questa domanda con un coach IA

Domande intermedie

11Come si implementa l'autenticazione biometrica (Face ID / impronta digitale) con lo storage sicuro della piattaforma per garantire che le chiavi crittografiche vengano sbloccate solo dopo una verifica biometrica riuscita?

Per vincolare in modo sicuro le chiavi crittografiche ai dati biometrici, un'app non deve affidarsi semplicemente a una callback booleana della UI proveniente da un prompt biometrico. Al contrario, le chiavi crittografiche devono essere generate e memorizzate all'interno di uno storage basato su hardware (Secure Enclave su iOS, Android Keystore / StrongBox su Android) configurato con criteri di controllo dell'accesso che impongono l'autenticazione biometrica prima dell'uso della chiave. Su iOS, le chiavi o gli elementi del Keychain vengono configurati utilizzando `SecAccessControlCreateWithFlags` con flag come `.biometryCurrentSet` (o `.userPresence`). Quando la chiave privata viene richiesta per la firma o la decifratura, il sistema operativo mostra automaticamente il prompt per Face ID / Touch ID. Su Android, le chiavi vengono generate utilizzando `KeyGenParameterSpec.Builder` con `.setUserAuthenticationRequired(true)`. Un'operazione crittografica (come un `Cipher` o `Signature`) viene inizializzata e passata come `BiometricPrompt.CryptoObject` a `BiometricPrompt.authenticate()`. La chiave viene sbloccata e resa utilizzabile solo all'interno di `onAuthenticationSucceeded` tramite tale `CryptoObject` autenticato. L'uso di `.biometryCurrentSet` (iOS) o `setInvalidatedByBiometricEnrollment(true)` (Android) garantisce che, in caso di registrazione di una nuova impronta digitale o elemento biometrico sul dispositivo, le chiavi esistenti diventino permanentemente non valide, mitigando l'accesso non autorizzato in caso di compromissione del passcode del dispositivo.

val keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val spec = KeyGenParameterSpec.Builder(
    "biometric_auth_key",
    KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
    .setUserAuthenticationRequired(true)
    .setInvalidatedByBiometricEnrollment(true)
    .build()

keyGen.init(spec)
keyGen.generateKey()
Prova a rispondere a questa domanda con un coach IA

12Quali strategie di memorizzazione nella cache e ottimizzazioni delle pipeline è possibile implementare per ridurre in modo significativo i tempi di compilazione delle pull request nei runner di CI (Continuous Integration) mobile multipiattaforma?

Per ridurre drasticamente i tempi di compilazione delle pull request sui runner di CI mobile, le ottimizzazioni devono concentrarsi sulla risoluzione delle dipendenze, sulla memorizzazione nella cache della compilazione e sull'esecuzione selettiva. In primo luogo, implementare una memorizzazione nella cache efficace delle dipendenze basata sugli hash dei file di blocco (ad esempio `package-lock.json`, `Podfile.lock`, `gradle/verification-metadata.xml` o i file risolti dei pacchetti SPM). Ciò evita di scaricare o risolvere nuovamente i pacchetti esterni su istanze pulite dei runner. In secondo luogo, configurare le cache di build native. Per Android, abilitare la Gradle Build Cache (`--build-cache`) con caching HTTP remoto o caching persistente nativo di CI per `~/.gradle/caches` e le directory di compilazione. Per iOS, memorizzare nella cache con attenzione `DerivedData` di Xcode e gli artefatti di compilazione di Swift Package / CocoaPods, oppure sfruttare strumenti moderni di remote caching come Tuist o Bazel. Per React Native/Flutter, memorizzare nella cache l'output del bundle JS, i `node_modules` e la cache SDK del motore Flutter. In terzo luogo, applicare l'esecuzione selettiva dei job (rilevamento delle modifiche e analisi dell'impatto dei test). Utilizzare filtri di percorso Git per saltare le build iOS quando sono cambiati solo file Android o backend, eseguire fasi rapide di lint e unit test prima di costosi test UI o fasi di compilazione, ed evitare di generare file binari di rilascio completi (come AAB o IPA universali) nelle esecuzioni di PR in cui è necessaria solo una build simulatore/debug o l'esecuzione di unit test.

name: PR Fast Check
on:
  pull_request:
    paths:
      - 'android/**'
      - 'shared/**'

jobs:
  android-unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: 'zulu'
          java-version: '17'

      # Gradle build caching for dependencies & task outputs
      - uses: gradle/actions/setup-gradle@v3
        with:
          cache-read-only: false

      - name: Run Unit Tests & Lint Only
        run: ./gradlew testDebugUnitTest lintDebug --build-cache --parallel
Prova a rispondere a questa domanda con un coach IA

13Come struttureresti una funzionalità condivisa in modo che presentazione, casi d'uso di dominio e accesso ai dati rimangano testabili in modo indipendente e riutilizzabili su iOS e Android?

Per strutturare una funzionalità condivisa garantendo testabilità indipendente e riutilizzabilità tra iOS e Android, è opportuno adottare un'architettura a livelli pulita suddivisa in Presentation, Domain e Data: 1. **Livello Domain (logica pura condivisa):** contiene le entità di business pure, le interfacce dei repository (contratti) e i casi d'uso/interactor (ad esempio `GetCartUseCase`, `ApplyPromoCodeUseCase`). Questo livello non ha dipendenze da framework di interfaccia utente (UIKit, SwiftUI, Android Views, Jetpack Compose, widget Flutter) o da API di piattaforma. Ciascun caso d'uso incapsula una singola operazione di business, rendendolo testabile al 100% con unit test tramite semplici mock. 2. **Livello Data (accesso ai dati e incapsulamento):** implementa le interfacce dei repository di dominio. Interagisce con origini dati remote (REST/GraphQL) e persistenza locale (SQLite/Key-Value). I DTO (Data Transfer Object) di rete e le entità del database vengono rigorosamente mappati in entità di dominio pulite prima di lasciare questo livello, impedendo che gli schemi di serializzazione esterni o le modifiche al database si propaghino nei livelli di dominio o di interfaccia utente. 3. **Livello Presentation (interfaccia utente e gestione dello stato):** è costituito da gestori dello stato (ViewModel, Bloc o Presenter) e viste/widget dell'interfaccia utente. Il gestore dello stato invoca i casi d'uso del dominio, gestisce lo stato della UI (caricamento, successo, errore) ed espone uno stato osservabile alla vista. La vista si limita a osservare tale stato e a visualizzarlo. Questa struttura consente di testare i casi d'uso in isolamento senza UI, simulare i repository per testare i view model e scambiare le implementazioni di presentazione tra le piattaforme riutilizzando l'intera logica di dominio e di dati.

[ UI View / Compose / SwiftUI ]
         |
         v (Observes State / Dispatches Events)
[ ViewModel / Presenter / BLoC ] (Presentation Layer)
         |
         v (Executes pure business operations)
[ Use Case / Interactor ] (Domain Layer - Pure Shared Logic)
         |
         v (Calls interface contract)
[ Repository Interface ] (Domain Layer Contract)
         ^
         | (Implements)
[ Repository Implementation ] (Data Layer)
         |---- Maps NetworkDTO -> DomainEntity
         |---- Maps DatabaseEntity -> DomainEntity
    [ API Client / Local DB / Storage ]
Prova a rispondere a questa domanda con un coach IA

14Come si valuta quando lo stato locale dei componenti non è più sufficiente e come si sceglie un approccio architetturale appropriato per la gestione dello stato in una codebase multipiattaforma gestita da più sviluppatori?

Nello sviluppo mobile multipiattaforma, il passaggio dallo stato locale del componente (`useState`/`StatefulWidget`) a una soluzione architetturale di gestione dello stato è guidato dall'ambito dello stato, dai requisiti di ciclo di vita e dalla testabilità: 1. **Quando lo stato locale non è più sufficiente**: - **Condivisione tra componenti e prop drilling**: quando lo stato deve essere accessibile o modificato attraverso percorsi di navigazione distinti o rami distanti dell'albero dei widget/componenti. - **Persistenza del ciclo di vita**: quando i dati devono sopravvivere alla distruzione delle schermate, alle transizioni di route o ai reset di navigazione (ad es. sessione utente, carrello, feed in cache). - **Separazione delle responsabilità e testabilità**: quando la logica di business, le validazioni e i side effect si intrecciano con i componenti della UI, rendendo complessi i test di unità automatici senza interfaccia grafica (headless). 2. **Scelta dell'architettura per codebase multi-sviluppatore**: - **Flusso di dati unidirezionale / Contenitori prevedibili (ad es. BLoC, Redux, Riverpod)**: impongono una rigida separazione in cui la UI invia eventi/azioni espliciti e renderizza lo stato immutabile emesso da unità dedicate alla logica di business. Questo definisce contratti chiari, riduce gli effetti collaterali e consente test di unità indipendenti sulla logica di business. - **Store reattivi atomici / con ambito (ad es. Zustand, MobX, Provider)**: offrono un boilerplate ridotto e sottoscrizioni flessibili tramite selettori, risultando adatti quando i team cercano un disaccoppiamento leggero e re-rendering mirati dei componenti. In un contesto di team, l'obiettivo architetturale principale è isolare la logica di business pura dai layer di rendering della UI e utilizzare sottoscrizioni selettive per prevenire inutili re-rendering dell'intero albero.

// Zustand store: pure state container isolated from React UI hierarchy
import { create } from 'zustand';

interface CartState {
  items: string[];
  addItem: (item: string) => void;
  clearCart: () => void;
}

export const useCartStore = create<CartState>((set) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
  clearCart: () => set({ items: [] }),
}));

// Usage in components uses selective subscriptions to avoid unnecessary re-renders:
// const itemCount = useCartStore((state) => state.items.length);
Prova a rispondere a questa domanda con un coach IA

15Come analizzeresti e ridurresti il jank in una schermata di un'app mobile multipiattaforma che presenta scatti durante lo scorrimento di una lunga lista di immagini e contenuti dinamici?

Per prima cosa riprodurrei lo scatto su dispositivi reali per analizzarlo con un profiler, poiché il jank durante lo scorrimento può dipendere da diverse cause: mancato rispetto del budget per frame, lavoro su JS o Dart, sovraccarico del main/UI thread, elaborazione GPU/raster, layout, decodifica delle immagini, pressione sulla memoria o caricamenti di rete. A 60 Hz l'applicazione dispone di circa 16,7 ms per frame, mentre a 120 Hz di circa 8,3 ms; pertanto rendering pesanti, decodifiche, calcoli di layout o operazioni sincrone durante lo scorrimento possono causare frame persi (dropped frames). Utilizzerei strumenti come Flutter DevTools, i performance tool di React Native o Flipper, Android Studio Profiler, Xcode Instruments e le visualizzazioni della timeline dei frame per individuare il reale collo di bottiglia. Per la lista lunga, mi assicurerei che sia virtualizzata o gestita in modo lazy: ad esempio tramite FlatList, FlashList o RecyclerListView in React Native, oppure ListView.builder, SliverList o builder equivalenti in Flutter. Utilizzerei chiavi stabili, eviterei di rieseguire il build o il re-render di ogni riga quando lo stato padre cambia, memorizzerei i componenti riga o i selettori ove opportuno, manterrei leggera l'esecuzione di build/renderItem e sposterei ordinamento, filtraggio, parsing JSON, formattazione o elaborazione delle immagini fuori dal percorso critico di scorrimento. Se le dimensioni degli elementi sono prevedibili, fornirei indicazioni di layout come getItemLayout in React Native o altezze fisse/prototipo in Flutter. Per le immagini, servirei miniature di dimensioni adeguate, ne implementerei la cache, eviterei di decodificare immagini a piena risoluzione per celle piccole, userei placeholder e lazy loading e monitorerei il ricambio eccessivo di memoria dovuto a troppi bitmap di grandi dimensioni. Semplificherei inoltre i layout di riga troppo complessi, ridurrei ombre pesanti, ritaglio (clipping), overdraw e trasparenze dove rilevante, raggrupperei o paginerei i dati e infine rieseguirei il profiling per confermare la riduzione dei frame persi e il miglioramento dei tempi per frame.

1. Record a trace while scrolling on a real device.
2. Check frame timeline: are frames exceeding 16.7 ms / 8.3 ms?
3. Identify bottleneck: JS/Dart, main/UI thread, raster/GPU, image decode, memory, or network.
4. Fix the largest measured bottleneck: virtualization, image resizing/caching, row memoization, layout simplification, moving work off scroll path.
5. Re-test on low-end and target-refresh-rate devices.
Prova a rispondere a questa domanda con un coach IA

16Come operano i confini tra thread nelle chiamate bridge native e come si possono prevenire blocchi dovuti al continuo passaggio tra thread (thread-hopping) o scatti nell'interfaccia grafica (UI, User Interface jank) quando i moduli nativi eseguono operazioni pesanti in background?

I framework multipiattaforma adottano convenzioni di threading specifiche per le chiamate bridge: ad esempio, le chiamate standard `MethodChannel` di Flutter arrivano sul thread UI principale della piattaforma, mentre le chiamate bridge tradizionali di React Native vengono eseguite su un thread JavaScript dedicato e smistate in modo asincrono ai thread nativi. Quando un metodo del bridge nativo viene eseguito sul thread nativo principale/UI, l'esecuzione di operazioni pesanti per la CPU, I/O sincrono su disco prolungato o chiamate di rete bloccanti blocca il main run loop. Ciò causa cali di frame rate, scatti visibili nell'interfaccia (UI jank) e, su Android, errori Application Not Responding (ANR) o terminazioni forzate dal watchdog su iOS. Per prevenire questo problema, i gestori del bridge nativo dovrebbero delegare il lavoro pesante o bloccante a pool di thread in background (ad esempio, Kotlin Coroutines con `Dispatchers.IO`/`Dispatchers.Default`, `ThreadPoolExecutor` di Android, oppure Swift `Task.detached` / GCD `DispatchQueue.global()`). Una volta completata l'operazione, il risultato deve essere reindirizzato al thread del bridge previsto dal framework (come la restituzione dei risultati sul thread UI per i MethodChannel standard o l'uso di code di attività in background), garantendo che gli aggiornamenti di stato multipiattaforma non blocchino il ciclo di rendering dell'interfaccia grafica.

class ImageProcessorPlugin : MethodChannel.MethodCallHandler {
    private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        if (call.method == "applyFilter") {
            val imageBytes = call.argument<ByteArray>("image") ?: return result.error("INVALID_ARG", "Image null", null)
            
            // Dispatch heavy computation off the main UI thread
            scope.launch {
                try {
                    val processed = processBitmapBytes(imageBytes) // CPU heavy work
                    withContext(Dispatchers.Main) {
                        result.success(processed)
                    }
                } catch (e: Exception) {
                    withContext(Dispatchers.Main) {
                        result.error("PROCESSING_FAILED", e.localizedMessage, null)
                    }
                }
            }
        } else {
            result.notImplemented()
        }
    }
}
Prova a rispondere a questa domanda con un coach IA

17Come progetteresti una strategia di caching e invalidazione per schermate di elenchi e feed utilizzando Stale-While-Revalidate e intestazioni HTTP (Hypertext Transfer Protocol) condizionali?

Una strategia solida di caching e invalidazione per feed ed elenchi combina il pattern Stale-While-Revalidate (SWR) con le intestazioni HTTP condizionali (come `ETag` / `If-None-Match` o `Last-Modified` / `If-Modified-Since`). All'apertura della schermata, i dati memorizzati nella cache vengono letti e renderizzati immediatamente da un database locale (la Single Source of Truth, ad esempio Room o Core Data/SQLite), garantendo all'utente una visualizzazione istantanea della UI. Contemporaneamente, viene inviata una richiesta di rete in background con l'intestazione memorizzata `If-None-Match: <etag>`. Se il server risponde con `304 Not Modified`, non viene trasferito alcun payload, confermando la validità della cache locale e risparmiando banda e batteria. Se il server risponde con `200 OK`, i nuovi dati e l'`ETag` aggiornato vengono scritti nel database locale all'interno di una transazione, e gli osservatori reattivi del database propagano automaticamente l'elenco aggiornato all'interfaccia grafica. Per la paginazione, i token di pagina o gli offset vengono salvati insieme ai record in cache. L'invalidazione viene attivata dalla scadenza del TTL (Time-To-Live), da azioni dell'utente (come il pull-to-refresh che forza l'aggiornamento ignorando le intestazioni condizionali) o da effetti collaterali di mutazioni locali (ad esempio, la creazione o eliminazione di un elemento aggiorna ottimisticamente il database locale e marca i cursori di pagina memorizzati per la riconvalida).

fun getFeed(): Flow<List<FeedItem>> = flow {
    val cached = feedDao.getFeedItems()
    if (cached.isNotEmpty()) emit(cached)
    
    val lastEtag = feedDao.getFeedEtag()
    try {
        val response = api.fetchFeed(ifNoneMatch = lastEtag)
        if (response.code() == 200 && response.body() != null) {
            feedDao.updateFeedTransaction(response.body()!!, response.headers()["ETag"])
            emit(feedDao.getFeedItems())
        }
    } catch (e: Exception) {
        if (cached.isEmpty()) throw e
    }
}
Prova a rispondere a questa domanda con un coach IA

Domande per esperti

18Un'applicazione mobile finanziaria deve memorizzare i refresh token in modo sicuro sul dispositivo, supportare lo sblocco biometrico, resistere agli aggiornamenti dell'OS (Operating System) e operare in sicurezza durante sessioni offline temporanee. Come progetteresti l'architettura di archiviazione e accesso ai token?

Un'architettura solida per l'archiviazione dei token su mobile sfrutta moduli di sicurezza basati su hardware: il Keychain di iOS supportato dal Secure Enclave e l'Android Keystore supportato da StrongBox o da un TEE (Trusted Execution Environment). I refresh token devono essere cifrati a riposo utilizzando chiavi protette da sblocco crittografico biometrico (come `kSecAccessControlBiometryCurrentSet` o `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` su iOS, e `setUserAuthenticationRequired(true)` con `AUTH_BIOMETRIC_STRONG` su Android). Il gating crittografico garantisce che la chiave venga decifrata dall'hardware esclusivamente a seguito di un'autenticazione biometrica riuscita, evitando di fare affidamento su controlli booleani aggirabili a livello di codice applicativo. Per resistere agli aggiornamenti del sistema operativo prevenendo al contempo accessi non autorizzati, le chiavi vengono associate all'hardware del dispositivo applicando policy sulle modifiche dell'enrollment biometrico, che invalidano o richiedono una nuova autenticazione qualora vengano registrate nuove impronte o volti (ad esempio tramite `setInvalidatedByBiometricEnrollment(true)` su Android). Durante sessioni offline temporanee, è possibile utilizzare access token cifrati a breve durata e stati di sessione offline locali con TTL (Time To Live) limitati e privilegi ridotti, rinviando le operazioni di refresh privilegiate al ripristino della connettività di rete. Alla riconnessione, la rotazione dei refresh token con rilevamento del riutilizzo (replay detection) sul backend convalida la sessione; in caso di riutilizzo o revoca dell'account, il backend restituisce un segnale di invalidazione che attiva l'epurazione delle chiavi hardware e lo svuotamento dell'archivio offline da parte del client.

val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val spec = KeyGenParameterSpec.Builder(
    "refreshTokenKeyAlias",
    KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
    .setUserAuthenticationRequired(true)
    .setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG)
    .setInvalidatedByBiometricEnrollment(true)
    .build()
keyGenerator.init(spec)
keyGenerator.generateKey()
Prova a rispondere a questa domanda con un coach IA

19Come progetteresti un'infrastruttura di CI/CD (Continuous Integration/Continuous Deployment) scalabile per un monorepo mobile multipiattaforma a supporto di più team, gestendo al contempo la capacità limitata e onerosa dei runner macOS?

Per progettare un'infrastruttura di CI/CD scalabile per un monorepo mobile multipiattaforma ottimizzando la capacità limitata e costosa delle macchine macOS, l'architettura deve basarsi sul rilevamento dei moduli impattati tramite grafo delle dipendenze, sulla suddivisione netta dei carichi di lavoro tra pool ibridi di runner e sulla virtualizzazione o orchestrazione effimera per macOS. In primo luogo, occorre adottare uno strumento per il grafo di build del monorepo (come Bazel, Nx o Turborepo) con caching remoto distribuito. A ogni pull request, la pipeline CI calcola il diff del grafo aciclico diretto (DAG) rispetto al branch di destinazione per compilare e testare solo i pacchetti impattati e i relativi moduli dipendenti a valle, ignorando del tutto i moduli non modificati. In secondo luogo, è necessario implementare una strategia di allocazione ibrida dei runner: tutti i task che non richiedono strettamente Xcode (come linting e compilazione TypeScript/Dart, unit test, analisi statica del codice, scansioni di sicurezza e build Android con Gradle) vanno delegati a runner Linux/Kubernetes, più scalabili ed economici. I runner macOS devono essere riservati esclusivamente all'assemblaggio finale per iOS, alla compilazione Swift/Objective-C, al code signing e all'esecuzione dei test su Simulatore iOS. In terzo luogo, i runner macOS vanno gestiti tramite infrastrutture di virtualizzazione (come Tart, Anka o nodi bare-metal su AWS/MacStadium orchestrati tramite Nomad o Kubernetes). Ogni build iOS viene eseguita in una macchina virtuale pulita ed effimera con toolchain e cache dei derived data preriscaldate, prevenendo il degrado dell'ambiente (runner drift) e consentendo un autoscaling rapido in base alla profondità delle code della pipeline.

name: Cross-Platform Monorepo CI
on: [pull_request]
jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      ios_affected: ${{ steps.filter.outputs.ios }}
      android_affected: ${{ steps.filter.outputs.android }}
    steps:
      - uses: actions/checkout@v4
      - id: filter
        run: |
          # Determine affected targets via monorepo tool (e.g. nx / bazel)
          echo "ios=$(./tools/affected.sh ios)" >> $GITHUB_OUTPUT
          echo "android=$(./tools/affected.sh android)" >> $GITHUB_OUTPUT

  build-android:
    needs: detect-changes
    if: needs.detect-changes.outputs.android_affected == 'true'
    runs-on: ubuntu-latest-16core
    steps:
      - uses: actions/checkout@v4
      - run: ./gradlew assembleRelease

  build-ios:
    needs: detect-changes
    if: needs.detect-changes.outputs.ios_affected == 'true'
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - run: bundle exec fastlane ios build_and_test
Prova a rispondere a questa domanda con un coach IA

20Come progetteresti un'architettura mobile multipiattaforma per un prodotto che deve condividere la maggior parte della logica di business tra iOS e Android, consentendo al contempo a UI (User Interface), navigazione e integrazioni native di evolversi in modo indipendente?

Progetterei l'applicazione attorno a un core condiviso responsabile del comportamento di business stabile: modelli di dominio, validazioni, casi d'uso, regole di business e contratti di accesso ai dati. La UI specifica della piattaforma, la navigazione, gli SDK nativi, la gestione dei permessi, il ciclo di vita e la shell dell'app devono rimanere all'esterno di questo core. Il codice condiviso non deve importare UIKit, SwiftUI, Jetpack, API del framework Android, navigazione di React Native, navigazione di Flutter o dettagli di SDK nativi. Al contrario, deve dipendere da interfacce circoscritte come AuthRepository, SecureStorage, Analytics, CameraService o PaymentProvider implementate da ciascuna piattaforma. Organizzerei il codice per feature il più possibile anziché creare un unico grande layer condiviso. Ad esempio checkout, ricerca, account e messaggistica possono avere ciascuno codice condiviso di dominio/casi d'uso, contratti di stato e interfacce dei repository. Le shell di iOS e Android gestiscono poi la composizione delle schermate, i pattern nativi di UI, gli stack di navigazione, i permessi e il cablaggio degli adapter. La logica di presentazione può essere condivisa quando è realmente indipendente dal framework UI, come nel caso di reducer, macchine a stati o contratti dei view-model che emettono stati e azioni semplici; tuttavia, le viste effettive e la navigazione devono restare di competenza della singola piattaforma affinché ciascuna possa evolversi autonomamente. Il compromesso principale consiste nel condividere in modo incisivo la logica di business comune e stabile, evitando però astrazioni che nascondano le reali differenze tra le piattaforme. Imporrei questi confini con regole di dipendenza tra moduli, dependency injection, API pubbliche, test contrattuali, controlli architetturali e ownership definite. Le integrazioni native dovrebbero utilizzare porte e adapter (ports/adapters), così che il codice condiviso della feature veda una funzionalità comune mentre ciascuna piattaforma gestisce il comportamento dell'SDK, il ciclo di vita, i permessi, gli errori e le differenze di UX nel proprio layer.

shared-core/
  checkout/
    domain/          # Money, Cart, CheckoutRules, PlaceOrderUseCase
    ports/           # PaymentGateway, TaxRepository, Analytics
    state/           # CheckoutStateMachine or ViewModel contract

ios-app/
  checkout-ui/       # SwiftUI/UIKit screens and iOS navigation
  adapters/          # ApplePayPaymentGateway, KeychainStorage

android-app/
  checkout-ui/       # Compose/XML screens and Android navigation
  adapters/          # GooglePayPaymentGateway, EncryptedSharedPrefsStorage
Prova a rispondere a questa domanda con un coach IA