Viaggio in Kotlin Multiplatform Projects – Parte 1

Kotlin Multi Platform

Noi di MOLO17 siamo sempre alla ricerca di nuove tecnologie che possano aumentare la nostra produttività e consentirci di rilasciare il progetto nella sua forma migliore.
Dal 2017, abbiamo iniziato ad adottare Kotlin con successo come linguaggio per lo sviluppo di applicazioni Android. Dopo un enorme progetto interamente scritto con il linguaggio di JetBrains, ho iniziato personalmente a credere che questa nuova tecnologia porterà una ventata di innovazione nello sviluppo del software moderno. Ad oggi posso dire che è proprio quello che è successo. Il futuro è Kotlin Multiplatform.

Kotlin Multiplatform (abbreviato MPP, Multiplatform Project) è un potente strumento che combina le varie declinazioni del compilatore Kotlin. Kotlin/JVM (che produce bytecode JVM), Kotlin/JS (che viene transpilato in Javascript) e Kotlin/Native (che produce binari con istruzioni “macchina”, a basso livello) vengono uniti sotto un unico progetto. Ciò consente ad uno sviluppatore di avere una codebase comune interamente scritta in puro Kotlin e specializzata, poi, in moduli che contengono codice dipendente dalla piattaforma. Kotlin Multiplatform permette inoltre di compilare un software che gira su JVM (o Android), sul web e, perché no, su un iPhone.
E quando dico che funziona, intendo dire che funziona davvero bene!

Kotlin, funziona!

Alcuni di voi potrebbero sostenere che questo è solo un altro modo per scrivere un’app mobile multipiattaforma, come i framework Javascript Cordova o Ionic, o Xamarin, utilizzando C#. Ma questo non è ciò che il team dietro a Kotlin MPP intendeva quando questa tecnologia è nata.
Semplicemente osservando la struttura di progetto secondo le linee guida di JetBrains, possiamo affermare che il principio, alla base di questo concetto multipiattaforma, è la condivisione del codice. Ciò è reso possibile stabilendo in un modulo Kotlin comune le definizioni e le implementazioni di entità e classi. In questo modulo saranno presenti tutte le logiche di business, la logica di presentazione agnostica dal framework e, con le librerie giuste, la logica di accesso ai dati. La regola è, come ho detto, essere indipendenti dal framework.
L’applicazione Kotlin Academy Portal, sviluppata da Marcin Moskala, è solo una delle prove di fattibilità di queste intenzioni (https://github.com/MarcinMoskala/KtAcademyPortal).

Completamente entusiasti da queste caratteristiche, noi di MOLO17 abbiamo subito avviato un team di ricerca e sviluppo dedicato allo sviluppo di progetti multiplatform.
Come Software Engineer, specializzato su Android, ho iniziato avviato personalmente alcuni progetti per padroneggiare al meglio Kotlin Multiplatform.

Il progetto Kotlin Multiplatform

Il primo progetto che ho sviluppato è stato una semplice libreria mobile Bluetooth, per piattaforme Android e iOS. L’obiettivo era quello di scrivere una API comune per entrambi i target, per un accesso semplificato al framework Bluetooth.

Ho iniziato creando il progetto seguendo la documentazione sul sito web di Kotlin. Abbastanza facile da configurare e lanciare. La cosa interessante che ho davvero apprezzato è che possiamo creare un progetto Multiplatform direttamente da Android Studio. Quindi, non è necessario installare IntelliJ IDEA.Ciò che mi aspettavo dal primo passo era una API che consentisse allo sviluppatore di avviare la scansione delle periferiche BLE. Quindi ho iniziato a definire l’interfaccia comune per questa classe, qualcosa di questo tipo:

expect class BluetoothAdapter {
    fun discoverDevices(callback: (BluetoothDevice) -> Unit)
    fun stopScan()
}

Le keyword expect / actual in Kotlin Multiplatform

Notate la keyword expect all’inizio della dichiarazione di classe. Nel contesto di Kotlin Multiplatform, expect definisce un’entità che lo sviluppatore dichiara in un modulo comune. Questa definizione può essere una classe, un metodo, una proprietà o persino un costruttore, e deve essere presente nelle piattaforme target. “Aspettandosi” tale entità, lo sviluppatore deve anche “attualizzarla” nel modulo dipendente dalla piattaforma, utilizzando la keyword `actual`. Una sorta di paradigma di implementazione di un’interfaccia, ma più dinamico. E qui arriva il bello.

Kotlin Multiplatform – Target Android

Quando si usa il target Android su Kotlin Multiplatform, nulla differisce dalle implementazioni che faresti con un progetto standard. È necessario il nostro amato oggetto Context, da cui si ottiene un BluetoothManager. Quindi è possibile accedere all’istanza BluetoothAdapter.

Una potente funzionalità sul paradigma expect/actual è che pur “aspettandosi” una classe, non viene richiesto di “aspettarsi” anche il costruttore di quella classe. Quindi, nel mio caso, sono stato in grado di creare la definizione actual che richiede un’istanza di Context nel costruttore, che mi ha permesso di implementare la definizione come necessario.

actual class BluetoothAdapter(
    private val context: Context
) : ScanCallback() {
    // ...
    
    actual fun discoverDevices(callback: (BluetoothDevice) -> Unit) {
        this.callback = callback
        bluetoothAdapter.bluetoothLeScanner.startScan(this)
    }
    actual fun stopScan() {
        bluetoothAdapter.bluetoothLeScanner.stopScan(this)
        callback = null
    }
}

Kotlin Multiplatform – Target iOS

La faccenda diventa ancora più interessante quando usi come target la piattaforma iOS. Oltre ai vantaggi che ottieni non utilizzando Xcode (?), risultano molti altri benefici usando Kotlin come linguaggio di programmazione iOS. La standard library di Kotlin offre tutta una serie di classi bridge che traducono le API di Objective-C in classi Kotlin (come CoreBluetooth nel nostro caso). Ciò mi ha permesso di implementare la “attualizzazione” iOS della classe Bluetooth usando CoreBluetooth, scrivendo in Kotlin. Fantastico!

Ho istanziato CBCentralManager come al solito, e gli ho settato il delegate CBCentralManagerDelegate per ricevere le periferiche rilevate. Va sottolineato inoltre che durante la traduzione del protocol in Kotlin, il compilatore aggiunge il suffisso `Protocol` al nome di base.

All’inizio volevo che la mia classe actual chiamata BluetoothAdapter implementasse direttamente il protocol del delegate. Quindi, l’ho fatto, ma siccome CBCentralManagerDelegate implementa NSObjectProtocol, mi è stato richiesto di far implementare alla mia classe actual anche i metodi definiti da quest’ultimo. Per evitare di implementarli, ho fatto si che la classe erediti da NSObject, per avere già l’implementazione dei metodi richiesti. Tutto è andato bene, fino a quando ho provato a usare la mia definizione di classe in un progetto Swift. La compilazione è fallita con il seguente errore:

'BluetoothAdapter' is unavailable: Kotlin subclass of Objective-C class can’t be imported
Xcode compiler error due to unavailability of BluetoothAdapter
Errore del compilatore Xcode a causa dell’indisponibilità di BluetoothAdapter

Header Objective-C

Osservando gli header Obj-C generati, ho notato che la mia classe BluetoothAdapter era stata compilata correttamente, ma contrassegnata come non disponibile con il messaggio visto qui sopra. Quindi non ero in grado di creare un’istanza di una nuova o addirittura definirla come tipo di proprietà.
Dopo qualche ricerca su Google, ho scoperto che questo comportamento è una limitazione voluta, per evitare alberi di ereditarietà potenzialmente complessi di classi Foundation sul lato Kotlin. Per la serie: “fidarsi è bene, non fidarsi è meglio”.
Ottenere un’istanza della mia classe non era quindi possibile. Ho dichiarato in Kotlin una funzione che restituisce un’istanza del mio tipo, e una volta compilata, il tipo restituito è diventato NSObject. Quindi ho tratto le mie conclusioni.

In breve, possiamo usare una classe che eredita dagli oggetti Foundation solo dal lato Kotlin, e se dobbiamo crearne un’istanza dal codice Swift, dobbiamo creare una funzione builder in Kotlin, perché Kotlin sa come è definita e come costruirla. Qualcosa di questo tipo:

fun makeBluetoothAdapter() = BluetoothAdapter()
actual class BluetoothAdapter: NSObject() { ... }

And then in the Swift implementation:

private let myClass = ConsumerClass(
    bluetoothAdapter: BluetoothAdapterKt.makeBluetoothAdapter()
)

La nostra codebase Kotlin conoscerà il tipo di istanza che passiamo con la label bluetoothAdapter e tutto funzionerà correttamente.

Ora, torniamo al mio scopo originale. Ho lasciato la mia classe attuale senza alcuna estensione. Poi ho creato una proprietà in modo anonimo che estende NSObject, e che implementa il protocollo CBCentralManagerDelegate.

actual class BluetoothAdapter {
    // ... 
    private val delegateImpl = object : NSObject(), CBCentralManagerDelegateProtocol {
        override fun centralManager(
            central: CBCentralManager,
            didDiscoverPeripheral: CBPeripheral,
            advertisementData: Map<Any?, *>,
            RSSI: NSNumber
        ) {
            // ... 
        }
    }
    private val manager = CBCentralManager().apply { delegate = delegateImpl }
    actual fun discoverDevices(callback: (BluetoothDevice) -> Unit) {
        // ...
        manager.scanForPeripheralsWithServices(null, null)
        onDeviceReceived = callback
    }
    actual fun stopScan() {
        manager.stopScan()
        onDeviceReceived = null
    }
}

Conclusione

Ed ecco fatto! Una classe del modulo comune e le implementazioni specifiche per ogni piattaforma. Seguendo le linee guida di JetBrains, sono stato in grado di aggiungere come Build Phase in Xcode uno script che lancia il task Gradle per la creazione del file framework Cocoa. Questo poi può essere importato in un progetto Xcode.

Ho sviluppato due semplici app, Android con Kotlin e iOS con Swift, che mostrano un elenco dei dispositivi Bluetooth rilevati utilizzando il modulo comune. Il risultato è stato eccezionale. La scansione Bluetooth ha funzionato perfettamente come se fosse stata implementata in modo standard.

Il passo successivo è stato l’integrazione di una comunicazione standard con protocollo BLE … che ti descriverò nel prossimo articolo di questa blog! Grazie per la lettura!

Crediti per l’immagine: blog.jetbrains.com

conosciamo solo un quinto di ciò che vive in profondità. tutto il resto è da esplorare.

coderspace sta arrivando.