Upgrade to Pro — share decks privately, control downloads, hide ads and more …

"Kotlin Multiplatform dans le monde réel", Pari...

"Kotlin Multiplatform dans le monde réel", Paris Android User Group, 2023-05-23

Avatar for Alexandre Gressier

Alexandre Gressier

May 23, 2023
Tweet

Transcript

  1. Kotlin Multiplatform • Un module de code Kotlin partagé entre

    plusieurs plateformes • Compilé avec Kotlin/JVM pour Android et Kotlin/Native pour iOS • Génération d’un header Objective-C pour iOS • Permet d’utiliser les APIs de la plateforme via le mécanisme expect/actual • KMM en beta (mais stable fi n 2023)
  2. Structure du projet de base • Une application Android Jetpack

    Compose (mais peut utiliser les Views/XML) • Une application SwiftUI (mais peut utiliser UIKit) • Un module shared qui montre comment Kotlin peut appeler les APIs iOS • Un projet Gradle
  3. Solution kotlin { sourceSets { val commonMain by getting {

    dependencies { api(project(":core:di")) api(project(":core:ui:resources")) } } } // .. . core/build.gradle.kts
  4. Solution / / . .. listOf( iosArm64(), iosX64(), iosSimulatorArm64(), ).forEach

    { it.binaries.framework { export(project(":core:di")) export(project(":core:ui:resources")) // + Dépendances transitives à exposer export(project(":core:ui:state")) export(project(":core:domain:primitives")) export(project(":core:domain:model")) } } core/build.gradle.kts
  5. Gradle • Gère : • L’application Android • Les autres

    modules (KMP ou non) • Ne gère pas : • L’application iOS
  6. Convention Plugins android { namespace = MODULE_PACKAGES compileSdk = TARGET_ANDROID_SDK_VERSION

    sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") defaultConfig { minSdk = MIN_ANDROID_SDK_VERSION targetSdk = TARGET_ANDROID_SDK_VERSION } compileOptions { sourceCompatibility = JVM_BYTECODE_VERSION targetCompatibility = sourceCompatibility } packagingOptions { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } } gradle/plugins/src/main/…/plugins/lib/android-multiplatform.gradle.kts
  7. Tuist • Comme Gradle, décrire la con fi guration d’un

    projet avec du code • Génère un projet Xcode • Con fi guration écrite en Swift tuist.io
  8. Tuist let project = Project( name: name, targets: [ Target(

    name: name, platform: .iOS, product: .app, bundleId: "\(group).\(name)", infoPlist: .extendingDefault(with: [ "CFBundleShortVersionString": "1.0", "CFBundleVersion": "1", "UIMainStoryboardFile": "", "UILaunchStoryboardName": "LaunchScreen" ]), sources: ["src/main/swift / ** "], resources: ["src/resources /** "], dependencies: [ .project(target: "AmigoFootballData", path: " .. / .. /libs/data"), .project(target: "AmigoFootballUIComponents", path: " .. / .. /libs/ui/components"), .external(name: "Stinsen"), ] ), ] )
  9. Tuist $ tuist edit # Éditer les fichiers de build

    via Xcode $ tuist generate # Génère le projet et workspace Xcode $ tuist build # Build le projet $ tuist test # Lance les tests
  10. Plugins Tuist extension MainCommand { struct SwiftLintCommand: ParsableCommand { static

    var configuration = CommandConfiguration( commandName: "swiftlint", abstract: "Lints the code of your projects using SwiftLint." ) @Option( name: .shortAndLong, help: "The path to the directory that contains the workspace or project whose code will be linted.", completion: .directory ) var path: String? @Argument( help: "The target to be linted. When not specified all the targets of the graph are linted." ) var target: String? // ... func run() throws { try SwiftLintService() .run( path: path, targetName: target, strict: strict ) } } }
  11. Coroutines VS Async/Await suspend fun fetchData(): List<Int> { // TODO:

    fetch data } suspend fun compute(records: List<Int>): Int { // TODO: compute } suspend fun saveData(result: Int): Boolean { // TODO: save data } suspend fun doWork() { val records = fetchData() val result = compute(records) val response = saveData(result) println(“Response: $response") } func fetchData() async -> [Int] { // TODO: fetch data } func compute(_ records: [Int]) async -> Int { // TODO: compute } func saveData(_ result: Int) async -> Bool { // TODO: save data } func doWork() async { let records = await fetchData() let result = await compute(records) let response = await saveData(result) print("Response: \(response)") }
  12. Coroutines VS Async/Await suspend fun main() { doWork() } scope.launch

    { doWork() } LaunchedEffect(key) { doWork() } static func main() async { await doWork() } Task { await doWork() } .task { await doWork() }
  13. Limitations Kotlin → Objective-C • Les fonctions suspend sont transpilées

    en rajoutant un handler dans le cas où la fonction se termine • Mais : • Pas de support pour la cancellation • Les Flows perdent leur type générique (Objective-C ne supporte pas les génériques pour les interfaces)
  14. KMP-NativeCoroutines • Permet de palier ces deux problèmes • Plugin

    compilateur (KSP) utilisé via Gradle • Modi fi e le header généré pour le module partagé • Supporte Async/Await, Combine, ou RxSwift github.com/rickclephas/KMP-NativeCoroutines
  15. KMP-NativeCoroutines class RandomLettersGenerator { @NativeCoroutines suspend fun getRandomLetters(): String {

    // TODO: générer des lettres aléatoires } } Kotlin - Fonction suspend
  16. KMP-NativeCoroutines let handle = Task { do { let letters

    = try await asyncFunction( for: randomLettersGenerator.getRandomLetters() ) print("Got random letters: \(letters)") } catch { print("Failed with error: \(error)") } } handle.cancel() Swift - Fonction suspend
  17. KMP-NativeCoroutines let handle = Task { do { let sequence

    = asyncSequence( for: randomLettersGenerator.getRandomLettersFlow() ) for try await letters in sequence { print("Got random letters: \(letters)") } } catch { print("Failed with error: \(error)") } } handle.cancel() Swift - Flow
  18. SQLDelight • Génère du code Kotlin à partir de requêtes

    SQL • Multiplatforme : utilise l’implémentation respective d’SQLite pour Android ou iOS • Fichiers .sq (pas .sql) : SQL + labels • Supporte les migrations github.com/cashapp/sqldelight
  19. SQLDelight plugins { id("app.cash.sqldelight") version "2.0.0-alpha05" } sqldelight { databases

    { create("Database") { packageName.set(“com.multiplatforge") } } } Con fi guration
  20. SQLDelight CREATE TABLE hockeyPlayer ( player_number INTEGER NOT NULL, full_name

    TEXT NOT NULL ); CREATE INDEX hockeyPlayer_full_name ON hockeyPlayer(full_name); INSERT INTO hockeyPlayer (player_number, full_name) VALUES (15, 'Ryan Getzlaf'); Schéma src/commonMain/sqldelight/com/multiplatforge/hockey/data/Player.sq
  21. SQLDelight selectAll: SELECT * FROM hockeyPlayer; insert: INSERT INTO hockeyPlayer(player_number,

    full_name) VALUES (?, ?); insertFullPlayerObject: INSERT INTO hockeyPlayer(player_number, full_name) VALUES ?; Requêtes src/commonMain/sqldelight/com/multiplatforge/hockey/queries/Player.sq
  22. SQLDelight val database = Database(driver) val playerQueries: PlayerQueries = database.playerQueries

    println(playerQueries.selectAll().executeAsList()) // -> [HockeyPlayer(15, "Ryan Getzlaf")] playerQueries.insert(player_number = 10, full_name = "Corey Perry") println(playerQueries.selectAll().executeAsList()) // -> [HockeyPlayer(15, "Ryan Getzlaf"), HockeyPlayer(10, "Corey Perry")] val player = HockeyPlayer(10, "Ronald McDonald") playerQueries.insertFullPlayerObject(player) Requêtes - Kotlin
  23. ViewModels • Conteneur d’état dont le périmètre est généralement un

    seul écran • Exécute de la logique métier • Android : • A un cycle de vie • Persiste l’état à malgré les changements de con fi guration • Fourni un scope pour lancer des coroutines • Principe similaire sous iOS avec SwiftUI
  24. @Composable fun LoginScreen() { var username by remember { mutableStateOf("")

    } var password by remember { mutableStateOf("") } Column { TextField(value = username, onValueChange = { username = it }) TextField(value = password, onValueChange = { password = it }) Button( onClick = { println("Username: $username, Password: $password") } ) { Text("Sign In") } } } Sans ViewModel Android
  25. struct LoginView: View { @State private var username: String =

    "" @State private var password: String = "" var body: some View { VStack { TextField("Username", text: $username) SecureField("Password", text: $password) Button("Sign In") { print("Username: \(username), Password: \(password)") } } } } Sans ViewModel iOS
  26. class LoginViewModel : ViewModel() { var username by mutableStateOf("") var

    password by mutableStateOf("") } Avec ViewModel Android
  27. @Composable fun LoginScreen( vm: LoginViewModel ) { Column { TextField(value

    = vm.username, onValueChange = { vm.username = it }) TextField(value = vm.password, onValueChange = { vm.password = it }) Button( onClick = { println("Username: ${vm.username}, Password: ${vm.password}") } ) { Text("Sign In") } } } Avec ViewModel Android
  28. extension LoginView { @MainActor class ViewModel: ObservableObject { @Published var

    username = "" @Published var password = "" } } Avec ViewModel iOS
  29. struct LoginView: View { @StateObject private var vm = ViewModel()

    var body: some View { VStack { TextField("Username", text: $vm.username) SecureField("Password", text: $vm.password) Button("Sign In") { print("Username: \(vm.username), Password: \(vm.password)") } } } } Avec ViewModel iOS
  30. KMM-ViewModel github.com/rickclephas/KMM-ViewModel • Par le même auteur que KMP-NativeCoroutines (Rick

    Clephas) • Utilise le vrai AAC ViewModel pour Android • Dépendance Gradle pour le code Kotlin commun dependencies { api("com.rickclephas.kmm:kmm-viewmodel-core:1.0.0-ALPHA-8") }
  31. KMM-ViewModel Code Commun open class LoginViewModel: KMMViewModel() { private val

    _username = MutableStateFlow<String>(viewModelScope, null) val username = _username.asStateFlow() private val _password = MutableStateFlow<String>(viewModelScope, null) val password = _password.asStateFlow() }
  32. @Composable fun LoginScreen( vm: LoginViewModel ) { Column { TextField(value

    = vm.username, onValueChange = { vm.username = it }) TextField(value = vm.password, onValueChange = { vm.password = it }) Button( onClick = { println("Username: ${vm.username}, Password: ${vm.password}") } ) { Text("Sign In") } } } KMM-ViewModel Utilisation côté Android
  33. KMM-ViewModel Utilisation côté iOS import SwiftUI import KMMViewModelSwiftUI import shared

    struct ContentView: View { @StateViewModel var vm = LogInViewModel() // . .. }
  34. MOKO Resources • Convertit des resources dé fi nies dans

    un format similaire à Android vers iOS • Plugin Gradle • Supporte entre autres : • Strings/Traductions • Polices • Couleurs • Images • Fichiers Textes github.com/icerockdev/moko-resources
  35. MOKO Resources resources/MR/base/strings.xml <resources> <string name="helloWorld">Hello, world! < / string>

    <string name="mainHeadline">Find your next match </ string> </ resources> Généré (iOS) : en.lproj/Localizable.strings "helloWorld" = "Hello, world!"; "mainHeadline" = "Find your next match";
  36. MOKO Resources Utilisation côté Android en Kotlin: Utilisation côté iOS

    en Swift : let string = MR.strings().helloWorld.desc().localized() val string = MR.strings.helloWorld.desc().toString(context = this)
  37. xcode-kotlin • Ajoute un support pour debugger du code Kotlin/Native

    dans Xcode • Installation via CLI : • $ brew install xcode-kotlin • $ xcode-kotlin install github.com/touchlab/xcode-kotlin
  38. KMM Tooling Experiment • Plugin IntelliJ IDEA • Support Swift/Objective-C

    (comme AppCode) • Cross Kotlin/Swift/Objective-C : • Auto-complétion • Navigation symboles • Refactoring • Debugging • Run con fi gurations pour toutes les plateformes (dont iOS)
  39. Encore plus de Kotlin Multiplatform • Navigation • Réseau (HTTP

    avec Ktor Client, GraphQL avec Apollo-Kotlin) • Génération de code Swift à partir de Kotlin (KSP) • Utilisation de librairies Swift depuis Kotlin (pour iOS uniquement) • CI/CD : ne builder/tester/deployer que les modules pour lesquels il y a des changements • UI partagée : Compose Multiplatform, Flutter… • …