Obsah
Compose Multiplatform
Compose Multiplatform (CM) je deklaratívný framework. Slúži na tvorbu UI pre viacero platforiem, teda pre Android, iOS, desktop (Linux, Windows), web. Vývoj sa uskutočňuje v jazyku Kotlin. Compose Multiplatform je produkt firmy JetBrains a rozširuje sadu nástrojov Jetpack Compose od spoločnosti Google pre Android a zároveň zdieľa kompilátor a runtime Compose s Jetpack Compose a používa rovnaké API. Nové projekty je vhodné písať už pod knižnicami CM, čo zabezpečí bezproblémové zdieľanie kódu pre MP a ich dokonalejšiu vzájomnú podobnosť.
Deklarácia sa realizuje funkciam označenými @Composable. Mená funkcií začínajú veľkým písmenom (ako triedy). Užívateľské Rozhranie sa kombinuje z týchto funkcií. a ďalších komponentov pre rozloženie. Základné komponenty sú napríklad: Row, Column, Text, Button a mnoho ďalších. Prispôsobenie komponentov je možné pomocou modifikátorov. Spolu všetky komponnety zabezpečia ich rozloženie, vstup od používateľa, animácie a zmeny stavov.
V IntelliJ IDEA aj Android Studio sa vizualizuje rozloženie kompozovateľnéhych prvkov označených ako @Preview.
Kotlin : Material3
Android Developers: Jetpack Compose
Klibs.io : KMP knižnice
GitHub : Material Theme Builder
Material.io : Material Design 3 (Android, Web, Flutter)
Ak ste pracovali s JavaFX, potom Compose je podobne štrukturovaný. Pridanú hodnotu v Compose tvorí Modifier, ktorý ovplyvňuje vzhľad.
Štruktúra projektu
Každá platforma potrebuje určitý vstupný bod, teda funkciu či notifikáciu:
- Android →
Activity - iOS →
@main - JVM →
main() - Kotlin/JS, Kotlin/Wasm →
main()
commonMain je zdrojová zložka s kódom pre Kotlin Multiplatform. Pri kompilácii sa odtiaľ vytvára štruktúra binárnych súborov pre distribúciu. Tu sa použijú len multiplatformové knižnice. Tu definované štruktúry budú dostupné pre všetky platformy projektu.
Ceielené platformy sa definujú zväčša pri vytvorení projektu, ale je to možné i neskôr (composeApp.build.gradle.kts).
Zdrojové sady pre cielené platformy sú vedľa zložky commonMain a sú vo vlastných zložkách (androidMain, iosMain, jvmMain, …). Každý zdroj obsahuje definíciu cieľa kompilácie, aj dostupné závislosti.
Ovládanie UI
- Softvérové klávesnice - pre iOS napodobňuje podľa Android čo najverenjšie
- Dotyk a Myš - emulujú sa gestá na pohyb myši, ale nepodpořujú sa viacdotykové gestá
- Kopírovanie, Preklad a podobne - zabezpečuje platforma
- Rolovanie - pre Android a iOS je v platforme, pre ostatné je to koliesko myši
- Interakcia - sa riadí podľa manuálov SwiftUI a UIKit a pre počítače podľa Swing
- Späť - pre Android je to v platforme, pre iOS sa emuluje Android, pre PC klávesou ESC
- Text - podľa platformy
Kompozície komponentov
Kompozície svojim rozložením komponetov vytvárajú používateľské rozhrania (UI). Takéto funkcie sú označené náveštím @Composable. Zväčša môžu byť aj v IDE vizualizované označením @Preview, teda ak neprijímajú v parametre stavy.
Základné štrukturovanie kompozície je prvkami:
- Column, FlowColumn - prvky pod sebou
- Row, FlowRow - prvky vedľa seba
- Box - prvky v oblasti
Modifikátory
umožňujú nastaviť rozmery, zarovnanie, odsadenie, interakcie a iné…
Reťazenie modifikátorov
parametre modifikátora je možné zreťaziť: Modifier.padding(24.dp).fillMaxWidth(). Význam má postupnosť volaní, pretože následujúci parameter je viazaný na predchádzajúci.
Vstavané modifikátory
niektoré parametre (napríklad: size, padding a offset) môžu mať viac parametrov oddelených čiarkami.
requiredSize() prepisuje obmädzenie
paddingFromBaseline() odsadzuje voči zákldnej čiare
offset() relatívne posúva prvok
Modifikátory s obmedzeným rozsahom
matchParentSize() oznamuje dieťaťu parametee rodiča
weight() v rámci Row alebo Column sa vzťahuje k svojim súrodencom
definícia modifikátorov
opätovné použitie inštancií modifikátorov zlepšuje čitateľnosť kódu a môže zvýšiť výkon
Vlastné modifikátory
je môžené si tiež vytvoriť vlastné modifikátory nad rámec vstavaných
Android Developers : Chain existing modifiers
Adaptívné rozloženia
Pre konzistentný vzhľad na všetkých platformách je vhodné dodržať pokyny:
- Preferovť kanonické vzory rozloženia (detaily zoznamu, informačný kanál a podporný panel)
- Zachovať konzistenciu opätovným použitím zdieľaných štýlov pre odsadenie, typografiu a ďalšie dizajnové prvky.
- Rozdeiť zložité rozloženia na opakovane použiteľné.
- Rozmiestňovať podľa hustoty a orientácie obrazovky.
Android Developers : Kanonické vzory rozloženia
Používanie tried veľkostí okien
Triedy veľkosti okien, teda body zlomu kategorizujú dizajny rozloženia na kompaktné, stredné a rozšírené. Preto je vhodné využiť WindowSizeClass pridaním závosilosti:
// libs.version.toml: [versions] adaptive = "1.2.0-alpha06" [libraries] adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive", version.ref = "adaptive" } // build.gradle.kts:composeApp kotlin { sourceSets { commonMain.dependencies { implementation(libs.adaptive) }}}
Multiplatformové zdroje
CM poskytuje knižnicu compose-multiplatform-resources (Gradle) pre prístup k zdrojom v kóde pre podporované platformy. Zdroje sú statický obsah: obrázky, písma a reťazce, použitie vo vlastnej aplikácii. Zdroje sa čítajú synchrónne vo vlákne volajúceho, okrem asynchrónneho čítania pre nespracované súbory a webové zdroje.
JetBrains KMP : Prehľad zdrojov
Pôvodná zložka zdrojov pre Android res.* so zložkami drawable, mipmap, values.strings.xml je použiteľná len v composeApp.kotlin.<prj>.MainActvity.kt, teda v MP je treba použiť vlastné zložky a zdroje. Absolutna cesta: /<prj>/composeApp/src/androidMain/res/values/strings.xml.
V Android Studio nie je viditeľná (25sept) zložka zdrojov, avšak je možné ju editovať (Finder). Absolútna cesta: /<prj>/composeApp/src/commonMain/composeResources/values/strings.xml, kde values/strings.xml je manuálne vytvorené.
Tento XML je treba editovať v externom editore a po spustení sa jeho obsahom generuje Class →
Od CM 1.6.10 a Kotlin 2.0.0 už vygenerovaný projekt zahŕňa potrebné závislosti a zdroje a importy, takže po vytvorení …/strings.xml sa pri spustení projektu vygeneruje composeApp.commonMain.kotlin.<prj>.composeapp.generated.resources.String0.commonMain.kt, ktoré umožňí import v Composable funkciách importovať napríklad vlastné stringy : import <prj>.composeapp.generated.resources.my_string. Absolutna cesta: /<prj>/composeApp/build/generated/compose/resourceGenerator/kotlin/commonMainResourceAccessors/<prj>/composeapp/generated/resources/.
Samotný kód potom môže vyzerať takto: Text(stringResource(Res.string.my_string)), kedy tag my_string je možné používať až po builde projektu: Build → Assemble project.
Každý string bude mať vlatný import: import <prj>.composeapp.generated.resources.my_string
! Zložky vlastných zdrojov musia mať názvy:drawable,files,font,values
Trieda Res.kt je umiestnená: /<prj>/composeApp.commonMain.kotlin.<prj>.composeapp.generated.resources.
Lokalizácia
jazyková lokalizácia je prípona za pomlčkou podľa normy ISO 639-1 alebo ISO 639-2, prípadne regionálna prípona podľa normy ISO 3166-1-alpha-2 má za ďalšou pomlčkou r, následujúce XX označenie regiónu (values-spa-rMX)
- vytvoriť zložku
values-skv/<prj>/composeApp/src/commonMain/composeResources/ - skopírovať do nej
values.xmla preložiť - po
Build → Assemble projectbude vassets.composeResourcesvytvorená zložka s lokalizáciou
Ďaší postup je v odstavci Lokalizácia reťazcov.
Životný cyklus
Životný cyklus komponentov je prevzatý z konceptu životného cyklu Jetpack Compose. Zároveň poskytuje spoločnú LifecycleOwner implementáciu, ktorá rozširuje pôvodnú funkcionalitu Jetpack Compose na iné platformy a pomáha sledovať stavy životného cyklu v spoločnom kóde.
Závislosť bude v novom projekte už vygenerovaná: implementation("org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.9.4")
Pro použití korutín je Lifecycle.coroutineScope hodnota viazaná na Dispatchers.Main.immediate
(initial state) (dead state)
┏━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━┓ ┏━━━━━━━━━┓ ┏━━━━━━━━━┓ ┏━━━━━━━━━┓
STAVY ┃ INITIALISED ┃ ┃ DESTROYED ┃ ┃ CREATED ┃ ┃ STARTED ┃ ┃ RESUMED ┃
┗━━┳━━━━━━━━━━┛ ┗━━┳━━━━━━━━┛ ┗━━┳━━━━━━┛ ┗━━━━┳━━━━┛ ┗━━━━━━┳━━┛
┃ ON_CREATE ┃ ┃ ┃ ┃
┣━━━━━━━━━━━━━━━╋━━━━━━━━━━━━▶┃ ON_START ┃ ┃
┃ ┃ ┃━ ━ ━ ━ ━ ━ ▶┃ ON_RESUME ┃
┃ ┃ ┃ ┃━ ━ ━ ━ ━ ━ ▶┃
┃ ┃ ┃ ┃ ┃
UDALOSTI ┃ ┃ ┃ ┃ ON_PAUSE ┃
┃ ┃ ┃ ON_STOP ┃◄ ━ ━ ━ ━ ━ ▶┃
┃ ┃ ON_DESTROY ┃◄ ━ ━ ━ ━ ━ ━┃ ┃
┃ ┃◄ ━ ━ ━ ━ ━ ━┃ ┃ ┃
┏━━┻━━━━━━━━━━┓ ┏━━┻━━━━━━━━┓ ┏━━┻━━━━━━┓ ┏━━━━┻━━━━┓ ┏━━━━━━┻━━┓
STAVY ┃ INITIALISED ┃ ┃ DESTROYED ┃ ┃ CREATED ┃ ┃ STARTED ┃ ┃ RESUMED ┃
┗━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━┛ ┗━━━━━━━━━┛ ┗━━━━━━━━━┛ ┗━━━━━━━━━┛
JetBrains KMP : Životný cyklus
ViewModel
Bežný ViewModel z Android je aj pre KMP.
Závislosť bude v novom projekte už vygenerovaná: implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.9.4")
Po deklarácii triedy reprezentujúcej ViewModel je možné triedu použiť v Compose funkcii.
Pri použití korutín je ViewModel.viewModelScope hodnota viazaná na Dispatchers.Main.immediate
JetBrains KMP : Common ViewModel
Navigácia v aplikácii KMP
Koncept navigácie vyjadruje Navigačný graf s destináciami a prepojeniami medzi nimi. Za cieľ sa považuje uzol, ktorý reprezentuje iný uzol, vnorený graf, či dialóg. Prejdenie do cieľa vykoná jeho zobrazenie. Trasa určuje cieľ a cestu k nemu.
Aplikácia reprezentuje cestu a zásobník a spätný zásobník cieľov. Po kroku do cieľa sa tento presunie na začiatok zásobníka, aby bol možný krok späť. Cieľ je možné priradiť URI (priamy odkaz).
Pri potrebe využiť navigačnú knižnicu je treba pridať závislosť: implementation("org.jetbrains.androidx.navigation:navigation-compose:2.9.0") :
// libs.version.toml: [versions] navigationCompose = "2.9.0" [libraries] navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigationCompose" } // build.gradle.kts:composeApp kotlin { sourceSets { commonMain.dependencies { implementation(libs.navigation.compose) }}}
Deep links
Umožňuje presmerovať na konkrétnu destináciu v príslušnej aplikácii. Môžu byť užitočné aj na získanie externého vstupu do aplikácie (napríklad OAuth). Implementujú sa tak, že najprv sa zaregistrujú, potom definujú cesty k cieľom a nakoniec ich aplikácia spravuje.
JetBrains KMP : Navigácia v aplikácii Compose
JetBrains KMP : Navigácia a smerovanie
JetBrains KMP : Priame odkazy - Deep links
JetBrains KMP : Operácie drag-and-drop
Testovanie KMP
Rozhranie KMP UI pre test volá API runComposeUiTestfunkciu a testovacie fExterný odkaz]]unkcie sa volajú na ComposeUiTest prijímači. Pre desktop API je využité JUnit.
Pre testovanie sa použije postup:
- pridajť zdrojový kód pre testy a potrebné závislosti
- napísať a spustite vzorový test
- prispôsobenie testu
Bežné testy sa tvoria a spúšťajú v composeApp.commonMain.commonTest.kotlin a príkazom v terminály :
- Android -
./gradlew :composeApp:connectedAndroidTest - iOS -
./gradlew :composeApp:iosSimulatorArm64Test - desktop -
./gradlew :composeApp:desktopTest - Wasm -
./gradlew :composeApp:wasmJsTest
Lokalizácia reťazcov
KMP ponúka spoločnú knižnicu na správu zdrojov a generovanie kódu pre jednoduchý prístup k prekladom. Pripraviť zdroje treba tak, ako to je vysvetlnie v tomto článku je aj v odstavci Multiplatformové zdroje → Lokalizácia.
Po zostavení KMP spracuje strings.xml súbory zdrojov composeResources a vytvorí statické vlastnosti prístupu pre každý reťazcový zdroj: Res.strings, kedy pri použití @Composable bude j vizualizovaný v IDE.
V strings.xml je možné použiť šablóny pre String a Double: <string name="welcome_message">Welcome, %s!</string> a v kóde následne volať vložennú premennú ako parameter: Text(stringResource(Res.strings.welcome_message, "User"))
JetBrains KMP : Lokalizácia reťazcov
Regionálne formáty
Formátovanie je momentálne implementované pomocou kotlinx-datetime knižnice. Reprezentujú textové zobrazenia veličín či ich výber (dátum, čas, mena).
JetBrains KMP : Regionálne formáty
Projekt
Po vytvorení kompletného projektu budú v module composeApp nasledujúce zdrojové sady:
- androidMain - (Kotlin/JVM)
- commonMain - zdrojové kódy (Kotlin)
- iosMain - (Kotlin/Native)
- jsMain - pre web (Kotlin/JS)
- jvmMain - pre desktop (Kotlin/JVM)
- wasmJsMain - pre web (Kotlin/Wasm)
- webMain - zdroje pre jsMain a wasmJsMain
- commonTest - ak sú zahrnuté aj testy
Odporúča sa kód špecifický pre platformu písať priamo do jeho zdrojovej sady (nie duplikovať v zdroji).
Zdrojový kód začína v: composeApp/src/commonMain/kotlin/App.kt, vo funkcii App() { … }
JetBrains KMP : Spustenie aplikácie na Android, iOS, desktop(jvm),
Generovanie artefaktov pre web distribúciu : View | Tool Windows | Gradle → kotlin browser → jsBrowserDistribution & wasmJsBrowserDistribution ⇒ composeApp/build/dist/wasmJs[js]/productionExecutable :
JetBrains Kotlin : Generate artifacts pre Js a Wasm
hotový príklad Js: https://regalems.restapi.sk/js
hotový príklad Wasm: https://regalems.restapi.sk/wasmJs (wasmJs nie je funkčné…)
Generovanie desktop distribúcie : View | Tool Windows | Gradle → compose desktop → createReleaseDistributable ⇒ ~/IdeaProjects/<projectName>/composeApp/build/compose/binaries/main-release/app pre macOS je možné skopírovať priamo do Finder | Aplikácie,
Generovanie iných projektov
Projekt mobilnej platformy so zdieľanou logikou a natívným UI sa vytvorí definíciou pri vytváraní projektu, napríklad v IJI:
- Generators → Kotlin Multiplatform
- [+] Android
- [+] iOS [Do not share UI]
Po tom je v zložkách následujúce obsadeie:
- shared - spoločná logika pre Android aj iOS
commonMain- Interface + Class:shared.src.commonMain.kotlin.<prjName>.<Class>.ktandroidMain- Android Class:shared.src.androidMain.kotlin.<prjName>.<Class>.android.ktiosMain- iOS Class:shared.src.iosMain.kotlin.<prjName>.<Class>.ios.kt
- composeApp - projekt Android:
composeApp.src.androidMain.kotlin.<prjName>.App.kt, ale aj Android projekt - iosApp - projekt Xcode:
iosApp.iosApp.Preview Content.iOSApp.swift+ContentView.swift
JetBrains KMP : zdieľaná logika a natívné používateľské rozhranie
Pridanie závislostí
Závislosti môžu byť Multiplatformové, alebo Natívne. Pre oba typy závislostí je možné použiť lokálne aj externé repozitáre.
Pre ostré projekty je vhodné zjednodušené cesty v implementáciách rozvinúť do <projectName>.gradle.libs.versions.toml
kotlinx-datetime
Zabezpečuje operácie s dátumom a časom, pre typmy ako Instant, LocalDateTime a TimeZone. Po implementácii do composeApp/build.gradle.kts je nutné synchronizovať Gradle a v terminály IDE spustiť:
./gradlew kotlinUpgradeYarnLock kotlinWasmUpgradeYarnLock
KMP závislosti
kotlinx-datetime
kotlinx.corutines
kotlinx.serialization
Ktor
sqlDelight
- možné a odporúčané je inštalovať plugin SQLDelight na prácu so
.sqsúbormi v IDE
Pridanie závislosti do .shared.build.gradle.kts (IJIdea) :
plugins { // json serialization kotlin("plugin.serialization") version "2.2.21" } kotlin { // ktor actual version val ktorVersion = "3.3.1" sourceSets { all { languageSettings.optIn("kotlin.time.ExperimentalTime") } commonMain.dependencies { // date-time implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1") // coroutines implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") // ktor implementation("io.ktor:ktor-client-core:${ktorVersion}") implementation("io.ktor:ktor-client-content-negotiation:${ktorVersion}") // serialization and deserialization implementation("io.ktor:ktor-serialization-kotlinx-json:${ktorVersion}") // set JSON for serialization/deserialization } commonTest.dependencies { implementation(libs.kotlin.test) } androidMain.dependencies { // ktor implementation("io.ktor:ktor-client-android:$ktorVersion") } iosMain.dependencies { // ktor implementation("io.ktor:ktor-client-darwin:$ktorVersion") } } }
podrobné vloženie závislostí
- koin
- sqlDelight
- lifecycleViewmodelCompose
- material3
JetBrains KMP: MP + Ktor + SQLDelight, Pridávanie závislostí
gradle/libs.versions.toml
[versions] agp = "8.11.2" #// JetBrains say "8.7.3" #// ... coroutinesVersion = "1.10.2" dateTimeVersion = "0.7.1" koin = "4.1.0" ktor = "3.3.1" sqlDelight = "2.1.0" #// lifecycleViewmodelCompose = "2.9.1" # no need (see: androidx-lifecycle = "2.9.5") material3 = "1.3.2" [libraries] #// ... android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqlDelight" } koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutinesVersion" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "dateTimeVersion" } ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } native-driver = { module = "app.cash.sqldelight:native-driver", version.ref = "sqlDelight" } runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqlDelight" } #// androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref="material3" } [plugins] #// ... kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqlDelight" }
shared/build.gradle.kts
plugins { // ... alias(libs.plugins.kotlinxSerialization) alias(libs.plugins.sqldelight) } kotlin { // ... sourceSets { all { languageSettings.optIn("kotlin.time.ExperimentalTime") } iosMain.dependencies { implementation(libs.ktor.client.darwin) implementation(libs.native.driver) } androidMain.dependencies { // ... implementation(libs.ktor.client.android) implementation(libs.android.driver) } commonMain.dependencies { // ... implementation(libs.kotlinx.coroutines.core) implementation(libs.ktor.client.core) implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.runtime) implementation(libs.kotlinx.datetime) implementation(libs.koin.core) } } } // ... sqldelight { databases { create("AppDatabase") { packageName.set("<projectName>.cache") } } }
Iné závislosti
vyhľadávacia služba KMP knižníc od JetBrains : Klibs.io
Kotlin & KMP Dependency Injection Framework : Koin
Kompletný ekosystém pre vývoj moderných API : Apollo , docs Apollo
výkonný HTTP klient (pôvodne OkHTTP) : Okio , Multiplatform
omitted:
* Android => Android(AndroidStudio) + iOS(Xcode), add shared module commonMain & iosMain
SQLDelight
Je vhodné nainštalovať plugin SQLDelight do IDE, ako je vyššie uvedené. Potom budú ponúkané automaticky rôzne časti štruktúry projektu a kódu. V odkaze na JB Konfigurácia SQLDelight je presný postup ako vytvoriť štruktúru zložiek a pridať súbory pre operácie s DB. Taktiež v ľavom rámci IDE je dostupný doplnok SQLDelight,
Po vytvorení súborov sa v terminále spustí: (prípadne použiť panel: Gradle→<prjName>→Tasks→sqldelight→generateCommonMainAppDatabaseInterface)
./gradlew generateCommonMainAppDatabaseInterface
čo vygeneruje štruktúru a kódy do : shared/build/generated/sqldelight
JetBrains KMP : Konfigurácia SQLDelight
JetBrains Marketplace : SQLDelight
JetBrains KMP : Vytvorenie aplikácie pre iOS - sqlite3
