Hlavní navigace

Kotlin Multiplatform: jednotná platforma pro aplikace určené pro iOS a Android

2. 6. 2022
Doba čtení: 10 minut

Sdílet

 Autor: Depositphotos
Vývoj mobilních aplikací pro chytré telefony je obor mladý a stále prochází dynamickým vývojem. Na počátku bylo v této oblasti více hráčů a téměř každá vývojářská společnost měla své želízko v ohni.

Poměrně záhy se ale ukázalo, že dominantní platformou se nestává ta nejhezčí nebo ta co má nejvíce funkcí. Zásadní veličinou, která nabyla na důležitosti, je síla komunity lidí, kteří se o ni zajímají a vývojářů, kteří pro ni vytváří stále nové a neotřelé aplikace nebo hry.

Okruh kandidátů na trůn krále mobilních platforem se postupně omezil na tři nejsilnější hráče a po odstoupení Microsoftu a jeho Windows Phone (Windows Mobile) nám vykrystalizoval duel dvou titánů, platforem Android a iOS.

Zpočátku si obě platformy byly podobné asi jako italské polobotky a rybářské galoše s tím, že původ vašeho zařízení bylo možné rozeznat z jedoucího rychlíku. Postupem času se jejich design, funkce a principy ovládání začaly sbližovat a dnes není mnohdy na první pohled možné aplikace na jedné platformě od té druhé rozeznat.

Jedno z pravidel pro efektivní vývoj software má zkratku D.R.Y. neboli “Don't repeat yourself”. Jak naprogramovat byznys logiku jen jednou a použít ji ve všech cílových aplikacích?

Cross-platform řešení

Tak jak vznikaly a zanikaly operační systémy pro mobilní telefony, objevovala se a zase mizela i různá cross-platform řešení pro jejich vývoj. Na rozdíl od standardního nativního vývoje slibují vyšší efektivitu, rychlejší Time-To-Market a nižší náklady na správu a další rozvoj.

Mezi cross-platform frameworky můžeme zařadit enginy pro vývoj her (např. Unity, Unreal Engine).

Pro vývoj běžných aplikací existují řešení založená na webových technologiích umožňující komunikovat s hardwarem ve vašem zařízení, tzv. PWA (Progressive Web Application). Jiné frameworky nahrazují nativní vývoj aplikací pro Android a iOS využitím cizího ekosystému (např. React Native/JavaScript nebo Xamarin/C#) nebo vytváří ekosystém zcela nový (např. Flutter/Dart).

Jednu charakteristiku mají všechna tato řešení společnou. Vedle platformy Android a iOS je nutné spravovat ještě třetí cross-platform řešení. Komunikace mezi těmi platformami, je možná, ale rozhodně není snadná a přímočará. Celkové sladění všech technologií na větším projektu je tak pravou výzvou pro vývojový tým a v praxi se může stát, že tato komplexnost zastíní všechny výhody, které daný cross-platform framework přináší.

Kotlin Multiplatform

Technologie Kotlin Multiplatform se od těchto příkladů zásadně odlišuje. Kotlin jako programovací jazyk nemá žádný svůj vlastní domovský ekosystém, ve kterém by dominoval a na kterém by mohl stavět. Proto má ve svém DNA zabudovanou maximální možnou interoperabilitu s „hostujícím“ prostředím. Když byl uveden Kotlin/JVM jako alternativní jazyk pro JVM (Java Virtual Machine) prostředí, základ jeho úspěchu byl postaven právě na lehkosti, s jakou jej bylo možné použít ve stávajících projektech, které do té doby stavěly na programovacím jazyku Java. Nebylo nutné měnit architekturu, build systém nebo ještě snad programovat celý projekt od začátku.

Postupem času se Kotlin naučil kooperovat i s prostředím více vzdáleném. Postupně byl uveden Kotlin/JS a Kotlin/Native pro platformy, které žádnou VM (Virtual Machine) nenabízí. Stejný zdrojový kód napsaný v Kotlinu tak můžete použít v desktopové aplikaci na Windows, na Spring backendu, na JavaScript frontendu nebo Raspberry Pi, které ovládá vaše garážová vrata.

Kotlin compiler pak zajistí to, že výsledná binární knihovna bude zcela kompatibilní s ostatními nativními knihovnami, takže je už na vás, jestli v Kotlinu napíšete jen jednu funkci, sdílenou byznys logiku, nebo kompletně celou aplikaci. Volání této knihovny na cílové platformě se nebude nijak lišit od případu, kdy by tato knihovna byla implementována přímo v programovacích jazyce dané platformy.

Demo Time

Jak funguje Kotlin Multiplatform si nyní ukážeme na reálném projektu, kde vyvineme jednoduchou čtečku RSS kanálu portálu Root.cz. Jako cílovou platformu zvolme mobilní aplikaci pro iOS, abychom si ukázali integraci Kotlin sdíleného kódu do nativního kódu, v tomto případě v jazyce Swift.

Pokud by někoho zajímalo, jak by vypadala aplikace pro Android, desktop JVM nebo jako konzolová aplikace, je celý projekt dostupný na GitHubu. Struktura projektu je velmi zjednodušena tak, aby byla integrace technologie Kotlin Multiplatform co nejviditelnější.

Následující popis předpokládá, že máte dostupný počítač s macOS a nainstalovaným Xcode, což je je nezbytné pouze pro vytvoření iOS aplikace. Sdílený kód v Kotlinu je možné psát v libovolném IDE na jakémkoli stroji, my budeme používat Android Studio.

Šablona projektu

Začneme tím, že si v Android Studiu vytvoříme nový projekt dle šablony Kotlin Multiplatform App. Tím se vytvoří složky pro moduly androidApp, iosApp a shared, ve kterém bude sdílený kód.

Autor: Milan Mitošinka

Dále kde změníme distribuci iOS knihovny z Cocoapods na standardní framework. Toto zjednoduší konfiguraci iOS aplikace, kterou si detailně vysvětlíme níže.

Autor: Milan Mitošinka

Sestavení projektu prostřednictvím nástroje Gradle

Gradle je nástroj zodpovědný za sestavení projektu.

Soubor settings.gradle.kts definuje základní parametry projektu a pro jeho zápis je také použit jazyk Kotlin:

// Definice repozitářů odkud budeme stahovat jednotlivé pluginy
pluginManagement {
    repositories {
        google()
        gradlePluginPortal()
        mavenCentral()
    }
}
// Název projektu
rootProject.name = "Rootcz_Reader"
// Seznam modulů, se kterými bude gradle pracovat.
include(":androidApp")
include(":shared")

Pečlivý čtenář si jistě povšiml, že modul iosApp v této konfiguraci chybí.

Technologie Kotlin Multiplatform a nástroj Gradle jsou zodpovědné pouze za sestavení sdíleného kódu, avšak finální aplikace musí být sestavena nástrojem specifickým pro danou platformu. Android aplikace standardně také používají nástroj Gradle, a tak oba tyto moduly mohou být definovány v jedné Gradle konfiguraci.

Aplikace pro iOS je nutné sestavit v nástroji Xcode, takže modul iosApp v Gradle konfiguraci zcela chybí.

Sdílený modul

Modul shared obsahuje veškerý kód, který budeme sdílet. Jeho produktem budou knihovny, které použijeme ve finálních aplikacích. Zdrojový kód je členěn na dvě části:

  • src/commonMain  – Čistý Kotlin kód kompilovaný pro všechny platformy.
  • src/iosMain, src/androidMain  – Platformní Kotlin kód kompilovaný pouze pro konkrétní platformu. Umožňuje volání API dané platformy, např. iOS Foundation nebo Android Context.

Soubor shared/build.gradle.kts obsahuje build konfiguraci pro daný modul.

Obsahuje seznam platforem, pro které bude vytvořena knihovna se sdílenou funkcionalitou. Pro platformu iOS, která pro běh nevyužívá žádnou VM a kód je kompilován podle architektury procesoru, je nutné tyto specifikovat konkrétně.

kotlin {
    android()
    iosX64()
    iosArm64()
    iosSimulatorArm64()
}

Pro každou architekturu je vytvořena sada zdrojových kódů (Gradle sourceSet), aby nebylo nutné kód pro jednotlivé iOS architektury duplikovat.

sourceSets {

val commonMain by getting

sourceSets {
    val commonMain by getting
    val androidMain by getting
    val iosX64Main by getting
    val iosArm64Main by getting
    val iosSimulatorArm64Main by getting
    val iosMain by creating {
        dependsOn(commonMain)
        iosX64Main.dependsOn(this)
        iosArm64Main.dependsOn(this)
        iosSimulatorArm64Main.dependsOn(this)
    }
}

Šablona multiplatformní aplikace obsahuje také zdrojové soubory Platform.kt a Greeting.kt, které ale můžeme smazat a pustit se do implementace RSS čtečky.

Příprava rozhraní

Naše aplikace bude provádět následující kroky:

  1. Načte data ze služby www.root.cz/rss/clanky.
  2. Zpracuje získané XML.
  3. Zobrazí jednotlivé položky z načtených dat.

Krok 3. musí respektovat UI a UX principy jednotlivých platforem, proto si jej každá platforma bude implementovat samostatně.

Kroky 1. a 2. jsou obecné a vhodné k implementaci ve sdíleném modulu. Jeho veřejným rozhraním bude třída FeedService a model pro příspěvky:

class FeedService {
    suspend fun loadItems(): List
}
data class FeedItem(
    val id: Id,
    val title: String,
    val description: String,
    val author: String,
    val image: Image?,
)

Stažení a parsování dat

Pro stažení a parsování XML dat budeme využívat externí Kotlin Multiplatform knihovny. Upravíme definici závislostí v souboru  shared/build.gradle.kts:

sourceSets {
    val commonMain by getting {
        dependencies {
            dependencies {
                // Na stažení dat využijeme knihovnu Ktor
                implementation("io.ktor:ktor-client-core:2.0.0")
                // Na práci s XML daty využijeme knihovnu XmlUtil
                implementation("io.github.pdvrieze.xmlutil:core:0.84.2")
                implementation("io.github.pdvrieze.xmlutil:serialization:0.84.2")
            }
        }
    }
    val androidMain by getting {
        dependencies {
            // Android bude používat OkHttp engine
            implementation("io.ktor:ktor-client-okhttp:2.0.0")
        }
    }
    val iosMain by creating {
        dependencies {
            // iOS bude používat Darwin engine
            implementation("io.ktor:ktor-client-darwin:2.0.0")
        }
    }
}

Nyní můžeme implementovat službu pro získání položek z RSS zdroje:

expect val httpEngine: HttpClientEngine
class FeedService {
    // HTTP klient na stažení dat
    private val httpClient = HttpClient(httpEngine)
    // XML dekodér
    private val xml = XML(SerializersModule {}) {
        // Při dekódování chceme ignorovat data, která nepotřebujeme
        unknownChildHandler = UnknownChildHandler { _,_,_,_,_ -> emptyList() }
    }
}

Používáme obecný multiplatformní HttpClient, který pro svoji funkci potřebuje objekt HttpEngine. Ten definuje jak provolávat webové služby na dané platformě.

Proměnná val httpEngine: HttpClientEngine je tak deklarována jako expect. Toto klíčové slovo indikuje, že reálná hodnota není nastavena ve sdíleném kódu commonMain ale je definovaná zvlášť pro každou platformu v iosMain  a  androidMain:

// androidMain/AndroidClientEngine.kt
internal actual val httpEngine: HttpClientEngine = OkHttp.create {}
// iosMain/IosClientEngine.kt
internal actual val httpEngine: HttpClientEngine = Darwin.create {}

Klíčové slovo actual znamená, že jde o definici hodnoty deklarované ve sdíleném kódu.

Nyní stačí pouze definovat strukturu stahovaných dat o příspěvcích prostřednictví anotaci v objektu RssDto a máme vše pro implementaci sdílené funkcionality:

class FeedService {
    suspend fun loadItems(): List {
        // Stáhneme XML s daty o příspěvcích
        val xmlString = httpClient.get("https://www.root.cz/rss/clanky").bodyAsText()
        // Dekodujeme XML do Kotlin struktury
        val rssDto = xml.decodeFromString(serializer(), xmlString)
        // Převedeme Kotlin XML strukturu do modelu pro zobrazení
        return rssDto.channel.items.map(::toDomain)
    }
}

Aplikace pro platformu iOS

iOS aplikaci budeme stavět na moderním UI frameworku SwiftUI, který – podobně jako Jetpack Compose – umožňuje deklarativní definici designu. Dosud byl všechen kod psán v jazyce Kotlin, který ale (prozatím) nemá pro SwiftUI podporu. iOS aplikaci tedy budeme psát v jazyce Swift, který je Kotlinu po stránce syntaxe poměrně blízký.

Pojďme si ale nejprve ukázat, jak je sdílený kód integrován do iOS aplikace.

iOS aplikace je sestavena nástrojem Xcode, který využívá vlastní build systém. Detailní popis tohoto systému je nad rámec článku, soustředíme se tedy jen na specifika multiplatformního projektu.

Prvním krokem je speciální build fáze, která volá Gradle s úkolem na vytvoření Apple knihovny:

cd "$SRCROOT/.."
./gradlew :shared:embedAndSignAppleFrameworkForXcode

Druhým krokem je nalinkování této knihovny do iOS aplikace:

Autor: Milan Mitošinka

Nyní je vše připraveno a můžeme s naším sdíleným modulem pracovat stejně jako s libovolnou nativní iOS knihovnou.

Vytvoříme si strukturu, která bude zastřešovat vykreslení seznamu položek:

struct FeedView: View {
    // Pracujeme se sdílenou službou na dotažení dat
    let service = FeedService()
    // Držíme si posledně dotažené položky
    @State var items = [FeedItem]()

    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(items, id: \.id, content: makeItemView)
            }
        }
        .task {
            // Při zobrazení obrazovky chceme načítat data
            items = try! await service.loadItems()
        }
    }
}

Definujeme design zobrazení jedné položky:

func makeItemView(for item: FeedItem) -> some View {
    VStack(alignment: .leading) {
        GeometryReader { geometry in
            AsyncImage(url: item.image.flatMap { URL(string: $0.url) })
                .frame(width: geometry.size.width, height: geometry.size.height)
                .clipped()
        }
        .frame(height: 100)

        Text(item.title).font(.title)
        Text(item.author).font(.caption)
        Text(item.description_).font(.body).padding(.top, 4)
    }
    .padding()
}

Na závěr definujeme, že chceme mít FeedView jakou součást aplikace:

struct iOSApp: App {
    var body: some Scene {
        WindowGroup {
            FeedView()
        }
    }
}

Hotovo. Po spuštění aplikace se nám zobrazí data z RSS kanálu:

Autor: Milan Mitošinka

Kotlin Multiplatform v praxi

Kotlin Multiplatform byl představen v roce 2018 a mnoho týmů jej začalo ihned používat, nejprve však jen na menších částech svých projektů. Stejně jsme postupovali v mobilním týmu společnosti Cleverlance, kde jako první použití této technologie byla vyvinuta knihovna pro práci s formátem bankovních QR plateb.

Zlomovým pro širší nasazení v produkčních aplikacích bylo vydání Kotlin 1.4 v srpnu 2020, které obrousilo spoustu ostrých hran zejména při sestavování výsledných aplikací, a tak bylo možné začít mobilní aplikace stavět od začátku jako multiplatformní.

Od této doby jsme vyvinuli a v produkci provozujeme několik mobilních aplikací. Ze zkušenosti vidíme, že při jejich vývoji dokážeme sdílet cca 60–70 % kódu na platformu. V případě vývoje na dvě platformy to znamená, že v součtu dokážeme ušetřit minimálně třetinu nákladů na vývoj, plus s tím související další náklady na věci typu testování (v tomto případě snížení až na polovinu).

Autor: Milan Mitošinka

Vedlejším efektem je automatická synchronizace tempa vývoje a parity funkcionalit mezi Androidem a iOSem.

Týmy se nedělí podle platforem. Každý tým má jednoho Android a jednoho iOS leadera, složení zbytku týmu je poměrně volné. Vývojáři jsou schopni se částečně zastupovat i napříč platformami, takže plánovaná i neplánovaná nepřítomnost člena týmu má v této konfiguraci mnohem menší vliv na výkon, a celková velikost týmu je menší než součet velikostí dvou nezávislých platformních týmů.

skolení ELK

Dlouhodobě udržitelné

V týmu mobilního vývoje u nás v Cleverlance jsme vždy kladli velký důraz na udržitelnost námi vyvíjených aplikací. Různé cross-platformní řešení existují již delší dobu, nicméně v praxi se u nás nikdy nedostala z fáze experimentů do každodenního produkčního užití.

Technologie Kotlin Multiplatform se v posledních letech vyvinula do podoby, kdy jsme ji přes počáteční opatrnost začali postupně používat na větším množství aplikací. Inherentně řeší řadu standardních bolístek vývoje mobilních aplikací, jako je parita funkcí aplikací, sestavování vhodných týmů a zejména pak snižuje celkové náklady na vývoj. Považujeme jej za první opravdu široce použitelnou technologii při vývoji na obě platformy mobilních telefonů, která má potenciál snížit cenu vývoje bez obětování kvality.

Autor článku

Expert společnosti Cleverlance v oblasti světa mobilních technologií, wearables, IoT a VR / AR. Jeho motivací ke stálému zlepšování softwarových řešení a produktů je použití právě těchto technologií.