Kotlin Multiplatform Mobile: Native Apps entwickeln mit Multiplattform-Technik

Mit KMM lassen sich native Anwendungen für verschiedene Plattformen entwickeln, wobei die Businesslogik für die Cross-Plattform-Apps stets erhalten bleibt.

Lesezeit: 18 Min.
In Pocket speichern
vorlesen Druckansicht Kommentare lesen 1 Beitrag

(Bild: Tero Vesalainen / Shutterstock.com)

Von
  • Nils Kasseckert
Inhaltsverzeichnis

Im Alltag stellt sich oft die Aufgabe, eine neue Anwendung zu entwickeln, die auf verschiedenen Plattformen zur Verfügung stehen muss. Dabei besteht die Wahl zwischen einem Cross-Plattform-Framework und der nativen Entwicklung auf allen betreffenden Plattformen. Gegen die plattformübergreifende Entwicklung sprechen Gründe wie eine nicht optimale User Experience (UX) und eine schlechtere Akkulaufzeit. Die native Entwicklung des Produkts auf jeder einzelnen Zielplattform verursacht allerdings hohe Kosten und ist hinsichtlich der späteren Wartbarkeit nicht praktikabel. Eine Lösung für dieses Dilemma bietet das Software Development Kit (SDK) Kotlin Multiplatform Mobile (KMM). Es ermöglicht das native Erstellen von Apps unter der Nutzung eines gemeinsamen Businesscodes, der in Kotlin geschrieben ist. Die Businesslogik lässt sich anschließend als Bibliothek in Apps, ins Web und auf dem PC einbinden.

Young Professionals schreiben für Young Professionals

Dieser Beitrag ist Teil einer Artikelserie, zu der heise Developer junge Entwickler:innen einlädt – um über aktuelle Trends, Entwicklungen und persönliche Erfahrungen zu informieren. Die Reihe "Young Professionals" erscheint im monatlichen Rhythmus. Bist du selbst ein "Young Professional" und willst einen (ersten) Artikel schreiben? Schicke deinen Vorschlag an die Redaktion: developer@heise.de. Wir stehen dir beim Schreiben zur Seite.

Hinter KMM steht – wie auch hinter der Programmiersprache Kotlin selbst – das tschechische Softwareunternehmen JetBrains. Die Alpha-Version erschien im August 2020 und lässt sich als Android-Studio-Plug-in installieren. Im Unterschied zu anderen Cross-Plattform-Frameworks schreibt KMM nur die Businesslogik als geteilten Code in Kotlin und wandelt ihn anschließend nativ in die Programmiersprachen für die jeweilige Plattform um. Auch findet anders als bei anderen Cross-Plattform-Frameworks, bei denen das User Interface (UI) meist nur als Webansicht eingebunden ist, das Erstellen des UI nativ statt, was eine bessere Nutzererfahrung bedingt. Der Businesscode lässt sich als Abhängigkeit in das jeweilige plattformspezifische Projekt integrieren, es ist jedoch auch ohne diese Abhängigkeit funktionsfähig.

KMM soll dabei helfen, den Aufwand für das Erstellen der Businesslogik zu minimieren und dennoch eine native Erfahrung zu schaffen. Mit Kotlin Multiplatform Mobile lässt sich nativer Code für Android, iOS, watchOS, macOS, Windows sowie Linux erzeugen. In der App-Entwicklung ist dieses Vorgehen besonders attraktiv, denn Teams programmieren Android-Apps seit geraumer Zeit in Kotlin. Eine Umgewöhnung auf eine neue Sprache ist somit nicht nötig. Die starke Ähnlichkeit zwischen der von Apple entwickelten Programmiersprache Swift und Kotlin vereinfacht iOS-Entwicklern und -Entwicklerinnen die Umstellung.

Wie in der klassischen Android-Entwicklung ist für das Umsetzen eines Multiplattform-Projekts Android Studio notwendig. Zusätzlich ist das KMM-Plug-in in Android Studio zu installieren. Für einen praxisnahen Einstieg in die Multiplattform-Entwicklung gilt es im Folgenden eine kleine Beispiel-App zu entwickeln, die den Gerätenamen ausgibt. Technisch gesehen geschieht das mittels plattformspezifischer APIs, während die Businesslogik auf beiden Plattformen identisch ist. Um die iOS-App umzusetzen und zu kompilieren, ist ein Mac-Computer nötig – für die Android-App besteht diese Beschränkung nicht. Grundkenntnisse in der iOS- und Android-App-Entwicklung seien im Folgenden vorausgesetzt.

Zum Erstellen eines neuen Multiplattform-Projekts führt der Weg über den New Project-Button in Android Studio. Dort ist als Template zunächst Kotlin Multiplatform App auszuwählen. Im Anschluss besteht die Möglichkeit, verschiedene Parameter wie die Android-Version oder den Projektnamen zu konfigurieren. Für einen leichteren Einstieg ist im GitHub-Repository von AppSupporter ein bereits fertig konfiguriertes Projekt zu finden. Auf dem Branch solution steht die fertige App zur Ansicht bereit.

Nach erfolgreichem Öffnen des Projekts in Android Studio mit anschließender Gradle-Synchronisierung ist die Projektansicht von Android auf Project umzustellen, sonst wären einige Teile des Projekts nur schwer oder gar nicht auffindbar. Abbildung 1 stellt den initialen Projektaufbau dar.

Initialer Projektaufbau einer Kotlin-Multiplatform-Mobile-App (Abb. 1)

Das Projekt besteht aus den Ordnern androidApp, iosApp und shared. Zusätzlich sind in der Basisversion einige Gradle-Dateien zu finden. Im androidApp-Ordner ist die gesamte Android-App enthalten. Die Programmierung findet in der Sprache Kotlin statt, was auch für den Ordner iosApp gilt. Er enthält die gesamte in Swift verfasste iOS-App. Die geteilte Businesslogik befindet sich im shared-Ordner, der sich in die Unterordner androidMain, commonMain sowie iosMain aufteilt. Die Programmiersprache für diesen Ordner ist ebenfalls Kotlin. Im Unterordner commonMain sind alle Klassen enthalten, die als plattformunabhängig gelten. Typische Anwendungsfälle sind Algorithmen, Netzwerkanfragen oder zum großen Teil auch die Datenbanklogik.

Da für Netzwerkanfragen plattformspezifische APIs notwendig sind, bedarf es hierfür spezieller Multiplattform-Bibliotheken. Durchgesetzt haben sich dabei die quelloffene Kotlin-Bibliothek Ktor (für die Netzwerkanfragen) sowie SQLDelight für die plattformunabhängige Speicherung von Daten in einer Datenbank. Auf GitHub gibt es eine Liste bereits verfügbarer Multiplattform-Bibliotheken für weitere Anwendungsfälle in Kotlin. Zudem ist es möglich, eigene Libraries zu erstellen.

Die im Projekt vorhandenen Gradle-Dateien sind elementar. Daher muss man zunächst auch sie verstehen. Sowohl auf Root-Ebene als auch im androidApp- und shared-Ordner befindet sich eine Gradle-Datei namens "build.gradle.kts". Auf Root-Ebene werden allgemeine, projektweit geltende Einstellungen wie die URLs der Repositorys oder die Gradle-Version festgelegt. Die Datei Build-Gradle im Android Ordner enthält den von Android gewohnten Aufbau. Im Unterschied zu Android-Apps, die das KMM-SDK nicht nutzen, ist eine weitere Abhängigkeit enthalten (siehe Listing 1). Dadurch lässt sich jeglicher im shared-Ordner erstellte Code in das Android-Projekt einbinden und steht somit auch dort zur Verfügung.

implementation(project(":shared"))

Listing 1: shared-Abhängigkeit in der Android-Build-Gradle-Datei

Wichtiger ist die im shared-Ordner enthaltene Gradle-Datei. Listing 2 zeigt einen Ausschnitt daraus, die komplette Datei ist im Projekt unter dem Pfad shared/build.gradle.kts zu finden. Der Plug-in-Block bindet zuerst das Multiplattform- und das cocoapods-Plug-in ein. CocoaPods ist der standardmäßig verwendete Abhängigkeitsmanager unter iOS, da Gradle dort nicht zur Verfügung steht. Fast jedes Open-Source-Projekt in der iOS-Welt steht als CocoaPod-Abhängigkeit zur Verfügung. Dessen Schöpfer Eloy Durán hatte CocoaPods 2011 erstmals veröffentlicht. Inzwischen entwickelt es ein großes Entwicklerteam weiter. JetBrains nutzt in seinen SDKs ebenfalls CocoaPods. Auch in der Standardkonfiguration eines KMM-Projekts ist der shared-Code nach der Kompilierung als CocoaPods-Abhängigkeit in das iOS-Projekt eingebunden. Es besteht zwar die Möglichkeit, den shared-Code direkt als Framework einzubinden, was wegen einiger gravierender Nachteile wie dem Fehlen der Möglichkeit, andere Abhängigkeiten einzubinden, hier jedoch keine weitere Rolle spielt.

plugins {
    kotlin("multiplatform")
    kotlin("native.cocoapods")
    id("com.android.library")
}

version = "1.0"

kotlin {
    android()
    iosX64()
    ...

    cocoapods {
		 ...
        ios.deploymentTarget = "14.1"
        podfile = project.file("../iosApp/Podfile")
        framework {
            baseName = "shared"
        }
    }
    
    sourceSets {
        val commonMain by getting
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test"))
            }
        }
        val androidMain by getting
        ...
        val iosSimulatorArm64Main by getting
        val iosMain by creating {
            dependsOn(commonMain)
            iosX64Main.dependsOn(this)
            ...
        }
}
...

Listing 2: Ausschnitt aus der Shared-Build-Gradle-Datei

Nach dem einleitenden Plug-in-Block folgt in Listing 2 der Kotlin-Abschnitt. Darin findet die eigentliche Konfiguration des Multiplattform-Projekts statt. Neben allgemeinen Einstellungen zu CocoaPods lassen sich hier die zur Verfügung stehenden Plattformen (im Beispiel sind es Android und iOS) konfigurieren und unter sourceSets die Abhängigkeiten definieren. Soll eine Abhängigkeit für den gesamten shared-Code gelten, ist sie unter commonMain in den dependencies-Block zu schreiben. Abhängigkeiten, die nur im shared-Code auf iOS oder Android benötigt werden, sind im entsprechenden androidMain- beziehungsweise im iosMain-Dependency-Block zu definieren. Gleiches gilt für Testabhängigkeiten. Derzeit ist es nicht möglich, die dort definierten Abhängigkeiten auch im nativen Code zu verwenden. Um sie dennoch nutzen zu können, sind sie auf der jeweiligen Plattform erneut zu definieren. In der shared-Gradle-Datei definierte CocoaPods haben hingegen sowohl im geteilten als auch im nativen iOS-Teil Gültigkeit. Derzeit sind ausschließlich Objective-C-Abhängigkeiten im Kotlin-Code für iOS verwendbar. Eine Lösung für diese Beschränkung ist laut Roadmap und dazugehörigen YouTrack-Issues bereits in Arbeit. Auffällig ist die umständliche Definition der iOS-App: Während für Android nur eine Zeile notwendig ist, benötigt iOS drei. Das ist der Architekturvielfalt (ARM, x64, Simulator) der Apple-Plattform geschuldet.

Zum Start der eigentlichen Programmierung ist der commonMain-Ordner zu öffnen und man muss zur Datei Plattform.kt navigieren, die sich im Unterordner kotlin/de.heise/ befindet. Die IDE generiert während der Projekterstellung standardmäßig die Kotlin-Dateien Plattform.kt und Greeting.kt. Sie dienen für dieses Beispiel als Basis. Listing 3 zeigt den Inhalt der Datei Plattform.kt. Darin ist eine Expect-Klasse definiert, die eine Variable des Typs String enthält. Expect-Klassen sind ein Kernkonzept der Multiplattform-Entwicklung. Sie ermöglichen den Zugriff auf native, plattformspezifische APIs. Grundsätzlich ähneln Expect-Klassen Interfaces. Sie kommen ausschließlich im commonMain-Code vor und enthalten nur Methodenrümpfe oder Variablen-Deklarationen. Im Gegensatz zu einem Interface lassen sich Expect-Klassen initialisieren und wie normale Kotlin-Klassen verwenden. Die eigentliche Implementierung der Klasse findet plattformspezifisch statt. So existiert sowohl in androidMain als auch in iosMain eine entsprechende Implementierung der Plattform-Expect-Klasse. Die konkrete Implementierung wird mit actual anstelle von expect definiert. Die Auswahl der korrekten Klasse übernimmt der Compiler zur Build-Zeit in Abhängigkeit zur gerade verwendeten Plattform. Beim Kompilieren der iOS-Version verwendet iOS automatisch die zu iOS passende Actual-Klasse. Das gleiche Prinzip gilt für alle anderen Plattformen.

expect class Platform() {
    val platform: String
}

Listing 3: Inhalt der Expect-Klasse-Plattform

Zuerst wird die Platform-Expect-Klasse Platform.kt in commonMain um eine neue Methode erweitert. Sie soll den Gerätenamen als String ausgeben. Hierfür ist nach Zeile vier folgender Code einzufügen:

fun getDeviceName(): String. Eine Methode darf auch Parameter enthalten. Für das einfache Beispiel ist das allerdings entbehrlich. Im Anschluss daran ist die Methode jedes Mal in die jeweilige Plattform zu implementieren.