Skip to content
    All posts
    DIFFICULTY RATING:
    Intermediate
    LANGUAGE:
    Italiano

    Kotlin e Compose Multiplatform (KMP e CMP)

    Da sempre aziende e sviluppatori sono alla ricerca di una tecnologia per scrivere codice una volta sola e usarlo su più piattaforme. Sul mobile lo sviluppo cross platform, oggi dominato da Flutter e React Native, si sta aggiungendo una terza opzione: Kotlin Multiplatform con Compose Multiplatform.

    Neosperience Tech Blog 26

    Si è svolta a maggio 2024 la Kotlinconf 2024, una conferenza annuale su Kotlin dove decine di speaker annunciano le ultime novità e la visione per il futuro di Kotlin e tutto ciò che ruota intorno alla sua community.

    Kotlin è una terza, e molto interessante, opzione per lo sviluppo cross-platform; prima di approfondire, è utile una breve overview delle due tecnologie cross-platform che attualmente dominano il mercato.

    Cross-Platform: Flutter e React Native

    Flutter e React Native sono framework che permettono lo sviluppo cross-platform. Entrambe le soluzioni permettono di creare applicazioni per Android e iOS: con Flutter, con i dovuti accorgimenti, si possono creare anche applicazioni Desktop e Web.

    Flutter è stato creato da Google; il team Flutter è completamente distinto da quello che porta avanti la piattaforma Android e sono in diretta concorrenza tra loro. React Native è stato creato da Facebook (ora Meta) ed è sviluppato e mantenuto dalla community.

    Flutter utilizza Dart, un linguaggio tipizzato creato da Google, mentre per React Native si utilizzano tecnologie tipiche dello sviluppo web, quindi Typescript o Javascript.

    Flutter include un motore di rendering 2D[1]: uno UI kit dichiarativo genera l’albero di primitive grafiche che poi è inviato alla GPU e disegnato su schermo. RN, invece, implementa delle View native nelle due piattaforme che vengono usate dalla tecnologie web per creare la UI, include inoltre un engine Javascript (Hermes) scritto in C/C++ (da qui il “Native” nel nome). Durante la fase di build dell'app, il codice JavaScript è compilato in un bytecode.

    L’interazione con le API native delle due piattaforme avviene tramite plugin: sia Flutter che React Native implementano questa interazione con un Bridge che su cui è possibile mandare messaggi asincroni nelle due direzioni; su React Native c'è anche l'opzione di usare JSI per una comunicazione diretta. Il plugin è poi scritto nelle tecnologie native di Android e iOS.

    Esistono molti plugin per le funzionalità più comuni. La maggior parte dei plugin Flutter sono forniti da Google stesso e sono di alta qualità, quelli RN sono gestiti dalla community.

    Le origini di Kotlin

    Kotlin è un linguaggio di programmazione creato da JetBrain e promosso dalla Kotlin Foundation, fondata da Google e JetBrain.

    JetBrain è la società che ha sviluppato famosi IDE usati da molti programmatori (Intellij IDEA, WebStorm, PyCharm, …) e su cui è basato Android Studio. Recentemente stanno sviluppando un nuovo IDE chiamato Fleet, un IDE poliglotta più simile a Visual Studio Code di Microsoft.

    Kotlin è nato nel 2011 all’interno di JetBrain, la prima release stable nel 2016 è stata seguita rapidamente da un supporto ufficiale per lo sviluppo Android nel Maggio del 2017, solo due anni dopo nel Maggio 2019 Google ha annunciato Android Kotlin-First, rendendo Kotlin il linguaggio ufficiale per lo sviluppo Android.

    È un linguaggio moderno, tipizzato, pragmatico, e in continua evoluzione.

    Kotlin 2.0 è appena stato rilasciato: si tratta di un rilascio senza alcuna nuova funzionalità ma una completa riscrittura del compilatore (K2) che ne raddoppia la velocità di compilazione.

    Origine di Kotlin e evoluzione a Multi-platform

    Kotlin è stato creato per essere interoperabile con Java in un momento storico in cui Java, come linguaggio, cominciava a far sentire la sua età.

    Fin da subito Kotlin è stato progettato per cross-compilare verso la piattaforma JDK producendo Java Bytecode.

    Il compilatore Kotlin è composto di due parti:

    • Frontend: compila il codice kotlin in un formato intermedio
    • Backend: uno per piattaforma completa la compilazione con diversi target

    Nel 2018 JetBrain ha iniziato a lavorare a Kotlin Multiplatform, è stata rilasciata la prima versione stabile nel 2020 e oggi è usata da diverse aziende, tra cui Netflix, Baidu e Autodesk, per scrivere codice che può essere compilato per JVM, Android, iOS, Web (JS e WASM), Desktop e Server.

    Il termine Cross-Platform e Multi-Platform sembrano simili ma sono fondamentalmente diversi. Con Kotlin Multiplatform lo stesso codice viene compilato su molteplici target per diverse piattaforme, il prodotto della compilazione si comporta come una libreria nativa per il target per cui è compilato: bytecode jars in ambiente JVM, librerie AAR su Android, librerie Cocoapods o Swift Package Manager ObjectveC o Swift (con un plugin) su iOS, etc...

    Operativamente i sorgenti di un progetto in Kotlin Multiplatform hanno una directory sorgente per ogni target mentre una directory common contenente il codice Kotlin comune e agnostico alle piattaforme. Nella directory specifica di ogni piattaforma che si vuole supportare si può accedere ad API native disponibili solo su quella piattaforma.

    // commonMain
    expect fun getAppVersion(): String
    
    // iosMain
    actual fun getAppVersion(): String {
      return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as String? ?: ""
    }
    
    // androidMain
    actual fun getAppVersion(): String {
      return BuildConfig.VERSION_NAME
    }

    Nel codice comune si possono creare dichiarazioni di funzioni e classi usando la keyword expect e nel codice di piattaforma si può usare la keyword actual per fornire l’implementazione specifica per la piattaforma; quest’ultima ha accesso alle API della piattaforma nativa: come vedete nell’esempio di codice viene utilizzato BuildConfig su Android e si accede alla info plist su iOS per ottenere la versione dell’app.

    val appVersion = getAppVersion()
    println("Versione app: $appVersion")

    Dal codice comune si può usare l’API dichiarata direttamente. E, fondamentale in tutto questo, una libreria KMP può essere utilizzata direttamente dal codice di piattaforma (ad esempio iOS) come fosse una libreria nativa per la piattaforma. Questo significa che si può mischiare codice nativo con codice multiplatform.

    Supponete di aver scritto una classe Kotlin per la gestione dei messaggi di una Chatroom, ad esempio usando uno stream osservabile di messaggi per esporre lo stato della conversazione:

    class ChatRoom {
      private val _messages = MutableStateFlow(emptyList())
      val messages: StateFlow<list> = _messages.asStateFlow()
      // ....
    }

    Da iOS può essere utilizzata l’API scritta in Kotlin come fosse una libreria nativa iOS:

    class ChatRoomViewModel: ObservableObject {
        let chatRoom = ChatRoom()
    
        @Published
        private(set) var messages: [String] = []
    
        @MainActor
        func activate() async {
            for await messages in chatRoom.messages {
                // No type cast (eg `it as! [String]`) is needed
                //because the generic type is preserved.
                self.messages = messages
            }
        }
    }

    Questa è la fondamentale differenza tra CrossPlatform e Multiplatform. Non ci sono bridge di comunicazione asincrona, non servono plugin: il codice prodotto è nativo per la piattaforma ed è possibile sempre scrivere parti di codice native all’occorrenza, condividendo però il codice per tutto ciò che si ritiene condividere.

    Google, alle recenti conferenze, ha preso una chiara posizione su Kotlin Multiplatform indicandolo come la scelta consigliata per condividere codice tra piattaforme Android, iOS, Web, Server e Desktop. Non solo a parole ma con i fatti.

    Nell’immagine qui sopra potete vedere una timeline di adozione di Kotlin Multiplatform all’interno di Google, oggi utilizzato in produzione per condividere codice server / client mobile / client web su diversi prodotti Workspace quali Gmail, Drive e Docs e ha cominciato a fare il porting a multiplatform di molte delle librerie Jetpack di Android a Kotlin.

    Il che ci porta a parlare di…

    Librerie Kotlin Multiplatform

    Sebbene KMP sia una tecnologia relativamente nuova, il numero di librerie disponibili sta rapidamente aumentando.

    Eccone una carrellata tra quelle più usate per lo sviluppo Android - iOS

    • Persistenza e Storage: Jetpack Room, Jetpack DataStore, SQLDelight, Realm, Multiplatform Settings
    • Networking: Ktor, Apollo Kotlin
    • ViewModel / UI State Sharing: Jetpack ViewModel, KMM-ViewModel, Decompose
    • Dependency Injection: Koin
    • UI: Jetpack Compose, Coil, KoalaPlot, Markdown Renderer, Window Size Class
    • Navigation: Jetpack Navigation, Voyager

    Molte di queste librerie sono comunemente utilizzate per lo sviluppo Android, le librerie Jetpack in particolare sono le librerie ufficiali di Google usate comunemente per sviluppare app Android. Room, ad esempio, è usata per gestire database SQLite locali; è stata una delle più recenti librerie migrate a Kotlin Multiplatform e altre arriveranno in seguito ora che il team Android in google sta ufficialmente investendo in quest’area.

    L’abbiamo accennata tra le librerie, ma merita una sezione a sé stante: è tempo di parlare di…

    (Jetpack) Compose Multiplatform

    Jetpack compose è il toolkit UI ufficiale per lo sviluppo Android. È sviluppato da Google e Jetbrain per renderlo multiplatform e utilizzabile, oltre che su Android, su iOS (Beta), Desktop e Web tramite WASM (Alpha).

    Sotto il cofano Compose può utilizzare diversi motori di rendering su schermo. Su Android disegna su un View Canvas usando quindi il sistema nativo, su iOS e Desktop include Skia (Skiko) e usa un diverso backend skia a seconda della piattaforma per l'accelerazione hardware: metal su iOS, Vulkan o OpenGL su Desktop, su Web il rendering è lasciato al funzionamento standard di WebAssembly.

    In questa immagine potete vedere un’app che gira su MacOs e su Web. E potete vedere la stessa applicazione in questo video dell’anno scorso, con una live demo, alla presentazione di Compose iOS Alpha.

    Compose UI è un UI Kit moderno e dichiarativo (come SwiftUI, React e Flutter da questo punto di vista). Come ogni altro UI kit dichiarativo funziona al meglio quando i componenti UI si mettono in ascolto di uno stato che cambia e aggiornano la UI di conseguenza.

    Questo esempio è preso dal sito ufficiale di Jetpack Compose per Android e riadattato per Compose Multiplatform.

    @Composable
    fun JetpackCompose() {
      Card {
        var expanded by remember { mutableStateOf(false) }
        Column(
          modifier = Modifier
            .clickable { expanded = !expanded }
        ) {
          Image(painterResource(Res.drawable.compose))
          AnimatedVisibility(expanded) {
            Text(
              text = "Jetpack Compose",
              style = MaterialTheme.typography.bodyLarge
          }
        }
      }
    }

    Un widget in Compose è una funzione Kotlin annotata con @Composable. Questa annotazione informa il plugin compose del compilatore kotlin che va trattata diversamente dalle normali funzioni Kotlin.

    Non entrerò nel dettaglio in questo articolo di cosa faccia il compilatore ma spiegherò velocemente i suoi effetti.

    Ciò che distingue Compose da altri UI Kit dichiarativi è che il dev non ha bisogno di informare manualmente il framework quando lo stato cambia, il compilatore sa dove viene letta una variabile nel codice o passata ad un altro widget e permette a compose di monitorare automaticamente i cambi di stato.

    Un cambio di stato scatena una ricomposizione: riesegue il codice del vostro widget, e lo fa in maniera molto efficiente saltando (skip) le parti che non sono cambiate.

    Nell’esempio qui sopra l’unico stato a cambiare è la variabile expanded: questa variabile è ricordata con remember tra una composizione e l’altra ed è inizializzata a false; il by al posto del classico = per assegnare la variabile è una funzionalità di Kotlin che ci permette di usare expanded come una variabile anche se si tratta di un oggetto MutableState<Boolean> invece che dover chiamare un metodo per fare set e un altro per fare get.

    Il remember in compose ha la stessa funzione di @StateObject in SwiftUi, ChangeNotifierProvider in Flutter e useState in React.

    Card, Column, Image, AnimatedVisibility e Text sono tutti Composable, widget – Card e Text forniti dalla libreria Material3, gli altri core di compose UI. Card rappresenta un contenitore, nella UI è il contorno bianco; Column è un componente di layout che mostra tutti i widget al suo interno incolonnati verticalmente; Image renderizza un’immagine a schermo; AnimatedVisibility è un componente che anima automaticamente la visibilità di quello che contiene; Text come potete immaginare è semplice testo.

    ComposeUI utilizza i Modifier per applicare delle variazioni ai widget, I modifier si possono usare per moltissime cose come aggiungere padding, uno sfondo, impostazioni di accessibilità, gestire eventi touch e molto altro ancora.

    Nell’esempio qui sopra un modifier clickable è applicato al widget Column e rende l’intero widget cliccabile: al click viene eseguita la lambda che modifica il nostro stato expanded. Questo cambio di stato scatena automaticamente la ricomposizione e la UI cambia per adeguarsi mostrando il testo.

    Infine Res.drawable.compose è il modo in cui Compose Multiplatform permette di fare riferimento ad assets (risorse) esterni, in questo caso un’immagine vettoriale del logo di compose. Res si può usare anche per testi in lingua in modo analogo al funzionamento su Android della classe R.

    Da iOS si può usare un widget Compose dentro a SwiftUi o UIKit e viceversa si può includere un widget SwiftUi o UiKit all’interno di un composable lasciando piena libertà allo sviluppatore di decidere dove usare codice multiplatform e dove usare codice nativo. Le keyword expect e actual si possono usare anche sulle funzioni Compose e questo permette all’occorrenza di accedere ad API native o passare al framework di UI nativo. Multiplatform!

    Qual’è lo stato attuale di KMP e CMP?

    Per rispondere occorre distinguere tra Kotlin Multiplatform e Compose Multiplatform: Compose è costruito sopra a Kotlin Multiplatform, è stato realizzato prima per Android e Desktop ed in seguito è stato aggiunto il supporto ad iOS e Web.

    Sia Kotlin Multiplatform che Compose su Android sono tecnologie stabili e pronte per la produzione mentre Compose iOS è in Beta, Compose per Web è in Alpha.

    Quando si fa riferimento a software in Alpha o Beta il significato può cambiare enormemente da progetto a progetto: in questo caso Jetbrain e Google fanno riferimento principalmente alla stabilità delle API, qualcuna potrebbe cambiare prima della release stable ed essere quindi marcata come sperimentale con delle annotazioni.

    È possibile già usare compose iOS e Web in produzione accettando la possibilità di doverlo modificare in piccole parti in una successiva release; Compose è un insieme di librerie, di cui la maggior parte è indipendente dalla piattaforma ed è già stabile.

    Il sistema di build per Kotlin Multiplatform è Gradle, lo stesso utilizzato per applicazioni Android e nel mondo JVM. Applicando un plugin diventa possibile sviluppare un modulo gradle (libreria) per Kotlin Multiplatform. Le librerie KMP si possono pubblicare su repository maven e usano un formato ad-hoc chiamato klib.

    Per iOS consigliamo di configurare e utilizzare fin da subito SKIE di TouchLab: Kotlin Multiplatform per iOS produce un binario per ObjectiveC: SKIE è un plugin di gradle che aggiunge il supporto a Swift; TouchLab fa parte della Kotlin Foundation e lavora a stretto contatto con gli sviluppatori di Kotlin.

    Con l’introduzione del nuovo compilatore K2 e Kotlin 2.0 JetBrain ha cominciato a lavorare sul supporto a Swift senza bisogno di plugin, nell’attesa che sia completato si può tranquillamente usare SKIE che supporta fin dalle prime preview release il nuovo Kotlin 2.0.

    Lato iOS esistono diversi dependency manager, KMP supporta Cocoapods e SPM.

    Per lo sviluppo Kotlin si può utilizzare in modo intercambiabile Android Studio o Fleet. Fleet supporta anche swift: permette di lanciare l’app iOS direttamente senza lasciare l’IDE e di andare in debug indistintamente su codice Kotlin o Swift. Android Studio, ad oggi, è più completo nelle funzionalità di Preview di Compose. Esiste anche un plugin per XCode che permette di modificare il codice kotlin. La preview da XCode in swift-ui funziona anche per il codice scritto in Compose.

    Compose supporta performance da 120Hz sia su Android che su iOS.

    Non si trova ancora la quantità di librerie e documentazione disponibile su Flutter ma la community Kotlin è molto attiva. Vi consigliamo di entrare nello Slack di Kotlin e partecipare alla discussione: sia JetBrain, che Google, che gli sviluppatori di librerie KMP sono raggiungibili e attivi al suo interno e su altri social.

    Sul mercato del lavoro le posizioni per Kotlin Multiplatform sono ancora rare, ma uno sviluppatore nativo ha già quasi tutte le conoscenze necessarie a diventare uno sviluppatore KMP.

    Adozione di KMP / CMP per team mobile

    Se avete un app nativa Android state già probabilmente utilizzando librerie KMP.

    Se avete team mobile nativi Android e iOS è più semplice adottare KMP perché l'adozione può essere graduale, anche grazie alla possibilità di riutilizzo del codice Android già esistente. Se invece avete un team cross platform l’adozione è più complicata e non è approfondita in questo articolo.

    Ecco i miei suggerimenti:

    Il mio suggerimento personale ai team Android è iniziare valutando, tra le librerie utilizzate, quali sono già compatibili con multiplatform e quali non lo sono. Se, ad esempio, utilizzate Retrofit per le chiamate di rete, migrate a Ktor al suo posto prima di aggiungere il supporto multiplatform.

    Quando il team Android ha maturato dimestichezza con uno stack tecnologico multiplatform-friendly sarà un buon momento per coinvolgere il team iOS: anticipare è possibile ma forza il team a imparare nuove tecnologie contemporaneamente.

    Scegliete un progetto pilota, può essere qualcosa di nuovo o una nuova feature non collegata a feature precedenti. Rendete i due team, iOS e Android, un unico team nativo multiplatform composto da un mix di sviluppatori Android e iOS: l’ideale è avere almeno uno sviluppatore Android esperto.

    Esiste un comodo wizard per creare un progetto Kotlin multiplatform da zero scegliendo quali piattaforme si vuole utilizzare o partendo da un template predefinito. È una buona idea sperimentare un po’ prima di partire. Esistono anche moltissimi esempi di progetti su GitHub (qui una selezione).

    Invitate gli sviluppatori iOS a provare a scrivere anche codice Kotlin e quelli Android a prendere dimestichezza con Swift. Kotlin è molto simile come sintassi a Swift. Le code review e il pair programming sono un buon strumento per creare conoscenza condivisa.

    Valutate un mono-repo per il codice android e iOS. Non è essenziale ma rende la collaborazione più semplice.

    Decidete quali parti sviluppare in modo condiviso. Inizialmente condividere solo la business logic e il layer dati; posticipare la scrittura della UI in Compose in un secondo tempo può facilitare le cose, ma se il vostro team è motivato potete provare a sperimentare fin da subito anche con la UI in Compose.

    Definite una deadline e chiari requisiti anche se si tratta di un proof of concept: avere una scadenza aiuta a focalizzare il lavoro e requisiti chiari permettono di concentrarsi sull' operatività.

    Siate empatici con il team; evolvere lo stack tecnologico può essere difficile. Ritengo KMP sia un’occasione di crescita per sviluppatori iOS come per sviluppatori Android: i primi impareranno lo sviluppo Android e i secondi potrebbero imparare nozioni di sviluppo iOS.

    Per iniziare: seguite i codelab e le guide di Android - quasi tutti i concetti sono trasferibili a Compose Multi-platform. Valutate l’utilizzo della documentazione fornita da TouchLab, un’azienda della Kotlin Foundation che fornisce, oltre a guide e librerie per KMP, consulenza a pagamento per aiutare le aziende nell’adozione di KMP.

    Conclusioni

    La forza di Kotlin Multiplatform rispetto a framework come Flutter e React Native è che l’interazione tra codice nativo e cross-platform non richiede layer di astrazione e/o serializzazione di messaggi.

    Il prodotto della compilazione è una libreria nativa per le diverse piattaforme; in qualsiasi momento passare da codice Kotlin condiviso a codice specifico per la piattaforma è possibile e semplice: si può scegliere, funzionalità per funzionalità se svilupparla nativa, con codice solo multiplatform o con un mix delle due che permetta di ottenere il massimo riutilizzo di codice senza rinunciare a funzionalità specifiche della piattaforma.

    KMP permette inoltre di scegliere quanto e cosa condividere. Nella stessa app possono convivere una funzionalità interamente nativa, una interamente multipiattaforma e una ibrida con parti condivise e parti native; lasciando la libertà ai team di scegliere la tecnologia migliore di volta in volta.

    KMP può essere utilizzato anche lato server abilitando la condivisione di logica o domain model tra client e server.

    In poche parole, KMP non è un Framework, come Flutter o React Native, ma un linguaggio di programmazione con un compilatore poliglotta e una pletora di librerie multi-piattaforma.

    È una tecnologia relativamente nuova rispetto a Flutter o RN: la strada è meno segnata. Le guide e documentazioni di KMP talvolta sono rivolte a sviluppatori Android nativi. Anche se la documentazione sta velocemente migliorando e le linee guida si stanno delineando rapidamente, adottare KMP oggi è ancora più complesso, rispetto all’adozione, ad esempio, di Flutter e React Native.

    Si tratta di una piccola barriera d’ingresso che, personalmente, considero un investimento: ritengo che KMP abbia un futuro davanti molto promettente e possa gradualmente sostituire gli altri framework cross-platform.

    Lo ritengono anche Google, Meta e Amazon. Meta è Kotlin First (articolo) dal 2022 e lo ha ribadito, recentemente al keynote di Kotlinconf 2024 (minuto 10:33) specificando che la maggior parte dei loro sviluppatori preferiscono Kotlin. Nello stesso keynote Amazon (minuto 46:00) ha annunciato che nell’azienda sta aumentando l’adozione di kotlin e il supporto SDK a kotlin per le librerie AWS. E non sono gli unici.

    Riferimenti


    [1] Il motore Skia è stato usato per lungo tempo, ma il team di Flutter ha scritto un nuovo motore chiamato Impeller che dovrebbe migliorare problemi di performance, al momento è ancora un work in progress su Android.

    NEWSLETTER

    Never miss the latest tech news