Cos'è Kotlin e perché ha sostituito Java su Android
Cos'è Kotlin: interoperabilità totale con Java, null safety nel sistema di tipi, coroutine, i limiti reali di Multiplatform e il mercato italiano.
Ti sei messo a studiare per sviluppare app Android e trovi due risposte contraddittorie: metà dei tutorial è in Java, metà in Kotlin, e nessuno spiega se devi impararli entrambi o se uno ha davvero sostituito l'altro. La risposta è più interessante di un semplice "usa Kotlin". In questo articolo vedi perché la sostituzione è avvenuta senza riscrivere niente, e dove Kotlin invece non conviene.
Cos'è Kotlin
Kotlin è un linguaggio creato da JetBrains che gira sulla Java Virtual Machine e che può essere mescolato a Java nello stesso progetto, file per file, mantenendo accesso completo a tutte le librerie dell'ecosistema Java.
È nato attorno al 2011 da un'esigenza pratica: JetBrains scriveva i suoi strumenti in Java e ne sentiva i limiti — verbosità, nessuna protezione contro i valori nulli. Non volevano abbandonare la JVM e vent'anni di librerie, volevano un linguaggio migliore che ci girasse sopra. Nel 2017 Google lo ha dichiarato ufficialmente supportato per Android, e qualche anno dopo lo ha indicato come linguaggio preferito per la piattaforma.
L'interoperabilità è il punto, non la sintassi
Quasi tutti gli articoli su Kotlin partono dal confronto sintattico: guarda quante righe in meno. È vero ma è secondario. La caratteristica che ha reso possibile la sostituzione di Java su Android è un'altra: Kotlin e Java coesistono nello stesso progetto senza attriti.
Un file .kt può usare una classe scritta in un file .java che sta nella cartella accanto. E viceversa. Nessun livello di traduzione, nessuna interfaccia di collegamento, nessuna serializzazione tra i due mondi: compilano entrambi nello stesso bytecode.
// Kotlin che usa una classe Java esistente, senza cerimonie
val cliente = ClienteLegacy("Rossi")
cliente.aggiornaSaldo(120.0)
Le conseguenze pratiche sono enormi:
- Non serve riscrivere niente. Un'app Android di duecentomila righe in Java può adottare Kotlin partendo da un singolo file nuovo.
- La migrazione è graduale e reversibile. Converti quello che tocchi, lasci stare il resto.
- Tutte le librerie Java restano disponibili. Vent'anni di ecosistema, immediatamente utilizzabili.
Questo è il motivo per cui la transizione è avvenuta in pochi anni. Se Kotlin avesse richiesto un progetto separato o una riscrittura, sarebbe rimasto un linguaggio di nicchia come tanti altri nati sulla JVM.
L'attrito che esiste va comunque detto: Java non conosce gli optional di Kotlin, quindi qualunque valore che arriva da codice Java è un "platform type" che il compilatore non può controllare. Ai confini col codice Java la garanzia di null safety si indebolisce, e sta a te verificare.
Null safety: il NullPointerException diventa un errore di compilazione
È la caratteristica più citata e merita l'attenzione. In Java qualunque oggetto può essere null, e scoprirlo succede a runtime, in produzione, spesso di notte. È lo stesso approccio adottato da Swift con gli optional e da Dart: i tre linguaggi mobile moderni convergono qui.
In Kotlin i tipi si dividono in due:
var nome: String = "Edoardo"
nome = null // errore di compilazione
var soprannome: String? = null // questo va bene
String e String? sono tipi diversi. Sul secondo non puoi chiamare metodi senza gestire il caso vuoto:
val lunghezza = soprannome?.length // Int?, nullo se soprannome è nullo
val mostrato = soprannome ?: "anonimo" // valore di riserva
if (soprannome != null) {
println(soprannome.length) // qui il compilatore sa che non è nullo
}
Quell'ultimo blocco si chiama smart cast: dopo il controllo, il compilatore tratta la variabile come non nulla senza che tu debba convertirla. È una comodità piccola che si sente molto nell'uso quotidiano.
Esiste anche !!, che dice "fidati, non è nullo" e lancia un'eccezione se lo è. È l'equivalente del force unwrap di altri linguaggi e vale la stessa regola: usato per far tacere il compilatore, riporta esattamente i crash che stavi cercando di eliminare.
Le coroutine: asincronia leggibile
Un'app mobile passa il tempo ad aspettare: risposte di rete, letture da database, elaborazioni di immagini. Tutto questo deve avvenire senza bloccare l'interfaccia, e storicamente su Android si faceva con i callback — funzioni che ne chiamano altre quando il risultato arriva.
Il problema dei callback è che si annidano: tre operazioni in sequenza producono tre livelli di rientranza, e la gestione degli errori si sparpaglia ovunque. Le coroutine riscrivono lo stesso codice in forma sequenziale:
suspend fun caricaProfilo(id: String): Profilo {
val utente = api.getUtente(id) // sospende, non blocca
val ordini = api.getOrdini(utente.id) // sospende, non blocca
return Profilo(utente, ordini)
}
La parola suspend segna una funzione che può mettersi in pausa e riprendere dopo. Mentre è in pausa il thread è libero e fa altro: non c'è nessun blocco, nonostante il codice si legga dall'alto in basso come se fosse sincrono.
Quando ti servono operazioni in parallelo:
suspend fun dashboard() = coroutineScope {
val meteo = async { api.getMeteo() }
val news = async { api.getNews() }
Dashboard(meteo.await(), news.await()) // partono insieme
}
Il tempo totale è quello della chiamata più lenta, non la somma. Il concetto è affine a quello che trovi in JavaScript con async/await, ma con una differenza importante: le coroutine hanno lo structured concurrency, cioè vivono dentro uno scope e quando lo scope muore vengono cancellate automaticamente. Su Android significa che se l'utente chiude la schermata, le richieste in corso si fermano invece di continuare a girare e tentare di aggiornare una vista che non esiste più.
Non è gratis: la gestione dei dispatcher, lo scope giusto e la cancellazione corretta sono argomenti che richiedono studio. Ma il punto di partenza è molto più accessibile dei callback.
Il resto della sintassi, in breve
Le cose che noti nei primi giorni, senza dilungarsi:
data class Utente(val nome: String, val eta: Int)
// genera equals, hashCode, toString, copy: niente boilerplate
val adulti = utenti.filter { it.eta >= 18 }.map { it.nome }
val descrizione = when {
punteggio > 90 -> "ottimo"
punteggio > 60 -> "sufficiente"
else -> "da rivedere"
}
fun String.primaMaiuscola() = replaceFirstChar { it.uppercase() }
// funzione di estensione: aggiunge un metodo a un tipo esistente
Le data class da sole eliminano una quantità di codice ripetitivo che in Java era la norma; le funzioni di estensione permettono di arricchire tipi che non controlli senza ereditarietà.
Kotlin Multiplatform: cosa promette e cosa no
Qui serve chiarezza, perché è l'argomento su cui circolano più fraintendimenti.
Kotlin Multiplatform (KMP) permette di condividere la logica tra Android, iOS, desktop e web: modelli di dati, chiamate di rete, regole di business, accesso al database locale, validazioni. Quel codice si scrive una volta e compila per ogni piattaforma — su iOS diventa un framework nativo che il codice Swift usa normalmente.
Quello che non condivide, nella sua forma classica, è l'interfaccia: su Android scrivi Compose, su iOS scrivi SwiftUI.
Questo è l'approccio opposto a Flutter. Flutter condivide tutto, interfaccia inclusa, e disegna i propri componenti su ogni piattaforma. KMP condivide il motore e lascia che ogni piattaforma abbia la sua interfaccia nativa.
| Kotlin Multiplatform | Flutter | |
|---|---|---|
| Cosa condividi | Logica e dati | Tutto, interfaccia inclusa |
| Interfaccia | Nativa per piattaforma | Disegnata dal framework |
| Aspetto | Identico a un'app nativa | Coerente ma proprio |
| Adozione graduale | Sì, su app esistenti | No, di solito progetto nuovo |
| Codice condiviso | Meno | Molto di più |
I limiti reali di KMP, senza addolcirli: serve comunque competenza su entrambe le piattaforme, perché l'interfaccia la scrivi due volte; l'ecosistema di librerie multipiattaforma è più giovane di quello Android puro; e per compilare la parte iOS ti serve comunque un Mac. Esiste Compose Multiplatform per condividere anche l'interfaccia, ma è una scelta ulteriore con i suoi compromessi, non lo scenario base.
Per scegliere in modo informato tra questi approcci, la lettura dedicata è scegliere la tecnologia per un'app mobile e il confronto diretto in React Native o Flutter.
Dove si usa Kotlin (e l'onestà sul mercato)
| Ambito | Situazione |
|---|---|
| App Android | Standard di fatto, è lì che vive |
| Backend JVM (Ktor, Spring) | Funziona benissimo, ma è minoranza rispetto a Java |
| Multiplatform mobile | In crescita, ecosistema ancora giovane |
| Desktop con Compose | Possibile, di nicchia |
| Kotlin/JS per il web | Esiste, praticamente nessuno lo usa in produzione |
| Script di build Gradle | Lo incontri anche senza volerlo |
Qui va detta la cosa scomoda. Fuori da Android, Kotlin è di nicchia rispetto a Java. Sul backend JVM funziona molto bene e Spring lo supporta pienamente, ma le aziende con sistemi consolidati hanno codice Java, team Java e processi Java: un progetto nuovo in Kotlin è una scelta, non l'impostazione predefinita.
E sul mercato italiano, in particolare: le offerte esplicitamente Kotlin sono meno di quelle Java, e sono concentrate sullo sviluppo Android. Se il tuo obiettivo è massimizzare le possibilità di trovare lavoro sulla JVM, Java resta la base più ampia; se il tuo obiettivo è fare app Android, Kotlin non è un'opzione ma il punto di partenza. La domanda "cosa imparo per lavorare" trova una risposta più ragionata in quale linguaggio imparare nel 2026, e il dato sulle retribuzioni mobile in quanto guadagna un mobile developer in Italia.
Errori comuni
Usare !! per andare avanti. È la doppia negazione della null safety: hai scelto un linguaggio che protegge dai valori nulli e poi disattivi la protezione riga per riga. Se ti serve spesso, il problema è il modello dei dati.
Dare per scontata la null safety ai confini con Java. I valori che arrivano da librerie Java sono platform type: il compilatore non li controlla. Aggiungi tu le annotazioni o i controlli espliciti.
Scrivere Kotlin come se fosse Java. Getter e setter scritti a mano, classi piene di codice che una data class genera da sola, cicli for dove basta map o filter. Compila tutto, ma non stai guadagnando niente rispetto a prima.
Lanciare coroutine nello scope sbagliato. Usare GlobalScope su Android significa che l'operazione sopravvive alla schermata che l'ha avviata: aggiornamenti su viste distrutte, perdite di memoria, crash. Usa lo scope legato al ciclo di vita del componente.
Bloccare un thread dentro una coroutine. Thread.sleep() o una chiamata di rete sincrona dentro una suspend fun annullano tutto il vantaggio: la coroutine non si sospende, occupa il thread. Usa delay() e librerie che supportano la sospensione.
In sintesi
Kotlin ha sostituito Java su Android grazie all'interoperabilità, non alla sintassi. Poter mescolare i due linguaggi nello stesso progetto, file per file, senza riscrivere nulla e mantenendo tutte le librerie esistenti, è ciò che ha reso la migrazione possibile per progetti reali già in produzione.
Le due caratteristiche che si sentono ogni giorno sono la null safety nel sistema dei tipi, che sposta il NullPointerException dalla produzione al compilatore, e le coroutine, che rendono l'asincronia leggibile dall'alto in basso e la cancellano automaticamente quando lo scope muore.
Kotlin Multiplatform condivide la logica, non l'interfaccia. È l'approccio opposto a Flutter: meno codice condiviso, ma risultato nativo su ogni piattaforma e adozione graduale su app esistenti. Ha senso se hai già team su entrambe le piattaforme; meno se cerchi di fare tutto con una persona sola.
Il limite onesto è l'ampiezza. Su Android è la scelta ovvia. Fuori di lì rimane un ottimo linguaggio con un'adozione minoritaria, e sul mercato italiano le posizioni sono meno numerose e quasi tutte legate al mobile. Per collocarlo rispetto agli altri c'è la mappa completa in tutti i linguaggi di programmazione.
Se vuoi affrontare Android con un percorso strutturato invece che saltando tra tutorial, guarda i corsi.