# Compose Multiplatform - Concurrent de solutions multiplateforme ?

# Contexte

Basé sur du [KMM](https://kotlinlang.org/docs/multiplatform.html) (Kotlin Multiplatform) le projet [Compose Multiplatform](https://www.jetbrains.com/fr-fr/lp/compose-multiplatform/) a été annoncé par [JetBrains](https://www.jetbrains.com/fr-fr/) en 2020, en tant que nouvelle extension du framework [Jetpack Compose](https://developer.android.com/jetpack/compose?gclid=CjwKCAiA3aeqBhBzEiwAxFiOBsh8X1DMEcEcun8bDoKUJP05EPRONyYej5hPv3tqf0kEbfv_hn4jxBoCiGQQAvD_BwE&gclsrc=aw.ds&hl=fr) pour Android. Compose est un framework déclaratif pour la création d'interfaces utilisateur dynamiques et performantes.

À l'origine, Compose était uniquement disponible pour Android. Cependant, JetBrains a rapidement compris le potentiel de Compose pour les autres plateformes et a commencé à travailler sur une version multiplateforme.

La première version bêta de Compose Multiplatform a été publiée en 2021 et permettait déjà de créer des interfaces utilisateur communes pour Android, iOS, le web et le desktop.

Depuis lors, JetBrains a poursuivi le développement de Compose Multiplatform et a ajouté de nouvelles fonctionnalités et améliorations à chaque version. La dernière version (au moment de la rédaction de cet article), [Compose Multiplatform 1.5.10](https://blog.jetbrains.com/kotlin/2023/11/compose-multiplatform-1-5-10-release/), a été publiée en novembre 2023.

Le framework évolue aussi de release en release ce qui explique certains des points d'amélioration. Voici le statut des plateformes prises en charge :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699525716215/90719ad9-80d7-4fc9-882a-df190d8090b7.png align="center")

Cet article aura pour objectif de présenter les outils, les concepts et l'architecture d'un projet Compose Multiplatform. On utilisera comme base de présentation de code [un projet GitHub développé en POC](https://github.com/GuillaumeMuret/claudio-app) (contenant des points d'amélioration).

Ce POC est une application de boite à média récréative, ayant pour principale fonctionnalité de directement lancer des sons ou vidéos sur le téléphone d'une cible. Cette cible peut être un collègue / un ami / un grand patron ou toute personne équipée de l'application et connectée à internet. On pourra donc l'utiliser pour faire rire ou agacer ses voisins. Tout dépendra du niveau de fun de son voisinage...

(N'hésitez pas à regarder la section Démo en bas de cet article)

Voici les différentes actions possibles depuis la page principale de l'application :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1701358323740/2c73d2a7-052d-4a4c-918b-0251a2b6ab1a.png align="center")

Voici un bref aperçu de l'application et de ses fonctionnalités entre iOS (à gauche) et Android (à droite). Les vidéos au format original sont disponibles à la fin de l'article.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1701354716577/185d04dd-09ee-4c7e-93be-21901c552873.gif align="center")

De même, un bref aperçu entre le Desktop et Android (vidéos de démos à la fin de l'article) :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1701355332422/9b42bcbe-d0f9-4a77-a3d0-ad053c78df8d.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1701355624532/f1defa1f-85ba-4c39-9d23-56a6c1707609.gif align="center")

# Par où commencer ?

Le plus simple est de commencer par la découverte de projets déjà existants et de se rendre compte de la simplicité de développement !

Le [GitHub de Compose Multiplatform](https://github.com/JetBrains/compose-multiplatform) contient de nombreux projets [exemples](https://github.com/JetBrains/compose-multiplatform/tree/master/examples) sur lesquels se baser pour commencer.

## Outils

Les outils nécessaires pour commencer sont les suivants :

* Mac OS - pour le développement iOS
    
* [Git](https://git-scm.com/) pour le suivi de version
    
* [XCode](https://apps.apple.com/fr/app/xcode/id497799835?mt=12) (depuis le [App Store](https://www.apple.com/fr/app-store/)) avec l'activation de la [CLI](https://mac.install.guide/commandlinetools/index.html)
    
* [CocoaPods](https://cocoapods.org/) - avec [brew](https://brew.sh/) - `brew install cocoapods`
    
* [Android Studio](https://developer.android.com/studio) - l'IDE ultime pour le dev Android développé par Google et basé sur IntelliJ IDEA (IDE Java populaire et très puissant développé par JetBrains)
    
* Android SDK - à installer avec Android Studio
    

Laissez vous ensuite guider par Android Studio et les plugins puissants comme [Kotlin Multiplatform Mobile](https://plugins.jetbrains.com/plugin/14936-kotlin-multiplatform-mobile) pour le développement et la compilation.

# Architecture d'un projet Compose Multiplatform

*Mais qu'est ce que c'est que tous ces fichiers et dossiers dans ce projet !*

Cette section expliquera les différents fichiers et leur utilité en utilisant un projet développé chez Niji.

Avant de commencer petite explication d'un code couleur important utilisé par l'IDE :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699371114224/5625a850-8ec7-4ea9-a41d-a00efe004272.png align="left")

Lorsqu'un fichier ou dossier est écrit avec cette couleur de police c'est qu'il est ignoré de Git et donc cela correspond (à 99% des cas) à un fichier ou dossier généré par l'IDE et / ou [gradle](https://gradle.org/).

## Fichier à la racine

Les projets Android et Compose Multiplatform, utilisent [gradle](https://gradle.org/) pour la compilation. Ces projets doivent donc contenir des fichiers nécessaires pour la configuration de l'IDE et de l'outil de compilation gradle.

![Fichier racine projet Compose Multiplatform](https://cdn.hashnode.com/res/hashnode/image/upload/v1699352979332/494a185c-679a-4238-b012-273e62ec23b4.png align="left")

Voici un exemple de fichiers à la racine d'un projet Compose Multiplatform développé chez Niji :

`gradle.properties` : contient des instructions gradle pour la compilation et les numéros de version pour [Kotlin](https://kotlinlang.org/docs/releases.html), [Android Gradle Plugin](https://developer.android.com/build/releases/gradle-plugin) (AGP) et Compose :

```bash
kotlin.code.style=official
android.useAndroidX=true
org.gradle.jvmargs=-Xmx3g
xcodeproj=./iosApp

kotlin.native.cocoapods.generate.wrapper=true
kotlin.native.useEmbeddableCompilerJar=true
kotlin.mpp.androidSourceSetLayoutVersion=2

org.jetbrains.compose.experimental.macos.enabled=true
org.jetbrains.compose.experimental.jscanvas.enabled=true

# Enable kotlin/native experimental memory model
kotlin.native.binary.memoryModel=experimental

kotlin.version=1.9.20
agp.version=8.0.2
compose.version=1.5.10
```

`settings.gradle.kts` : contient la configuration des repo qui vont être utilisés pour la récupération des librairies (maven / google / gradle) et la déclaration des plugins avec leurs versions associées :

```kotlin
pluginManagement {
    repositories {
        google()
        gradlePluginPortal()
        mavenCentral()
        maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
    }

    plugins {
        val kotlinVersion = extra["kotlin.version"] as String
        val agpVersion = extra["agp.version"] as String
        val composeVersion = extra["compose.version"] as String

        kotlin("jvm").version(kotlinVersion)
        kotlin("multiplatform").version(kotlinVersion)
        kotlin("plugin.serialization").version(kotlinVersion)
        kotlin("android").version(kotlinVersion)
        id("com.android.base").version(agpVersion)
        id("com.android.application").version(agpVersion)
        id("com.android.library").version(agpVersion)
        id("org.jetbrains.compose").version(composeVersion)
    }
}

rootProject.name = "Claudio"

include(":common")
include(":appAndroid")
include(":appDesktop")
include(":appJs")
```

Il contient aussi le nom du projet en question et les différents modules que l'IDE et gradle pourront interpréter et analyser. Voici un exemple pour le projet Claudio. A noter que les noms ne respectent pas aujourd'hui le template Compose Multiplatform (cf [exemples](https://github.com/JetBrains/compose-multiplatform/tree/master/examples/imageviewer)).

`build.gradle.kts` : permet de définir les plugins avec leurs identifiants qui seront utilisés pour le projet. La configuration est très similaire entre chaque projet (cf [exemples](https://github.com/JetBrains/compose-multiplatform/blob/master/examples/imageviewer/build.gradle.kts))

`local.properties` : contient les configurations propres à la machine sur laquelle se trouve le projet. Ce fichier contient au minimum le sdk.dir définissant la localisation du SDK Android sur la machine de l'utilisateur.

`gradlew` / `gradlew.bat` sont les fichiers de script qui permettent d'exécuter les tâches gradle. Ils sont générés automatiquement. Le fichier [`gradlew`](https://github.com/JetBrains/compose-multiplatform/blob/master/examples/imageviewer/gradlew) est un script shell pour Linux et Mac OS, tandis que [`gradlew.bat`](https://github.com/JetBrains/compose-multiplatform/blob/master/examples/imageviewer/gradlew.bat) est un fichier batch pour Windows.

`.gitignore` : permet d'ignorer des fichiers de git

`README.md` : fichier qui décrit comment installer les outils et les différentes configurations pour compiler l'app

## Dossier à la racine

Voici un exemple des dossiers qui sont utilisés pour un projet Niji :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699367099219/fa485661-95dc-480f-9c79-8bc54d44140c.png align="left")

`.github` est le dossier de configuration pour les [GitHub Actions](https://github.com/features/actions) qui seront utilisées pour la [CI/CD](https://github.blog/2022-02-02-build-ci-cd-pipeline-github-actions-four-steps/).

`.gradle` contient l'ensemble des fichiers générés par gradle pour sa propre configuration.

`.idea` contient l'ensemble des fichiers de configuration pour Android Studio.

`.run` contient des scripts de run qui sont ajoutés automatiquement à l'IDE pour pouvoir lancer des tâches depuis ce dernier.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699368260240/ed36468c-8d08-425f-aea8-aed6c57a6878.png align="left")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699368319632/78d241a6-ad96-41fe-9d45-1ce46d3f4b91.png align="left")

`build` contient des fichiers générés par l'IDE et gradle pour la compilation. Il y a un dossier de build sous chaque module (ici `appAndroid` / `appDesktop` / `appJs` / `buildSrc` / `common`)

`appAndroid` / `appDesktop` / `appJs` / `iosApp` sont les dossiers pour la compilation finale des artéfacts. Ces modules utilisent le module `common` que nous verrons dans les sections suivantes.

`buildSrc` est un module permettant d'utiliser des fichiers et constantes qui pourront être utilisés dans les fichiers de `build.gradle.kts`. Cette approche est aujourd'hui obsolète et pourrait être remplacée par un [Gradle Version Catalogs](https://developer.android.com/build/migrate-to-catalogs).

`gradle` contient le wrapper gradle (binaire `.jar`) qui est appelé pour la compilation gradle par les fichiers `gradlew` et `gradle.bat`.

`kotlin-js-store` contient le `yarn.lock` utilisé pour la compilation de l'`appJs`.

`rpi` et `scripts` sont créés de toutes pièces et ne servent que pour du développement propre au projet.

# Module commun

Le module commun `common` contient, comme son nom l'indique, la configuration et le code commun aux autres modules de compilation finale d'artéfact.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699372596036/9ef6594c-a2dd-4055-a24f-c7c8b4c67b32.png align="left")

On retrouve le dossier `build`, qui est généré lors de la compilation, et un fichier `common.podspec` généré par la compilation iOS.

Le fichier `build.gradle.kts` contient la configuration pour la compilation dans les différents variants `androidMain` / `dektopMain` / `iosMain` / `jsMain` / `jvm`. Le module jvm est implémenté par le `androidMain` et `desktopMain` car la gestion de fichiers se fera dans la JVM.

## Concept expect/actual

Le concept expect/actual de Compose Multiplatform (basé sur KMM) est un mécanisme permettant de définir une API abstraite pour une fonctionnalité qui est ensuite implémentée de manière spécifique à chaque plateforme. Cela permet aux développeurs de partager du code entre plusieurs plateformes sans avoir à dupliquer le code ou à écrire des branches de code spécifiques à chaque plateforme.

Prenons pour exemple cet objet `LogUtils` :

```kotlin
package com.niji.claudio.common.tool

expect object LogUtils {
    fun d(tag: String, message: String)
    fun e(tag: String, message: String)
    fun e(tag: String, message: String, throwable: Throwable)
}
```

On voit ici le préfix `expect` qui permet de définir que cette classe `LogUtils` devra être implémentée pour l'ensemble des plateformes. A l'aide de l'IDE, on peut voir les modules dans lesquels sont implémentées les fonctions.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699432916571/7504dcdf-645e-4bb9-9f14-872b3818ad5b.png align="left")

Si on regarde dans le module `desktopMain`, on a ceci :

```kotlin
package com.niji.claudio.common.tool

actual object LogUtils {
    actual fun d(tag: String, message: String) {
        println("$tag - debug - $message")
    }

    actual fun e(tag: String, message: String) {
        println("$tag - error - $message")
    }

    actual fun e(tag: String, message: String, throwable: Throwable) {
        println("$tag - error - $message - $throwable")
    }
}
```

La classe `LogUtils` est préfixée de `actual`. La fonction `d` (pour afficher les logs de debug) est préfixée aussi de `actual` ce qui permet de réaliser l'implémentation voulue pour la plateforme desktop avec cette fonction.

Si on regarde dans le module `jsMain`, on a ceci :

```kotlin
package com.niji.claudio.common.tool

actual object LogUtils {
    actual fun d(tag: String, message: String) {
        console.log("$tag - debug - $message")
    }

    actual fun e(tag: String, message: String) {
        console.log("$tag - error - $message")
    }

    actual fun e(tag: String, message: String, throwable: Throwable) {
        console.log("$tag - error - $message - $throwable")
    }
}
```

L'implémentation est spécifique pour le JavaScript.

La plupart des classes et fonctions se trouvant dans les modules hors de common (`androidMain` / `desktopMain` / `iosMain` / `jsMain` / `jvm`) auront des fonctions qui seront préfixées de `actual`.

## Module jvm dans common

Voici ce qu'on a dans le module `jvm` :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699376498283/6f07a0b1-6705-4e0f-9926-fed118a7a47e.png align="left")

On retrouve uniquement dans ce module les `model` et fonctions utilisées dans la JVM pour la gestion des fichiers (média et configuration de l'app en JSON servant de base de données) et des dates. Ainsi, pour la base de données, ce seront des fichiers qui seront générés.

Ce module permet de ne pas dupliquer le code qui aurait été présent à la fois dans `androidMain` et à la fois dans `desktopMain`.

Exemple :

```kotlin
package com.niji.claudio.common.data.model

@Suppress("NO_ACTUAL_FOR_EXPECT")
expect class MediaFile {
    fun appendBytes(bytes: ByteArray)
    fun getLength(): Long
    fun getPath(): String
}
```

La classe `MediaFile` est une classe qui sera présente dans le module `iosMain` / `jsMain` / `jvm` :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699434874309/68ed635b-864b-4e52-96e5-17c4215bb7f1.png align="left")

En effet, comme ce `MediaFile` utilise un fichier qui sera utilisé dans la JVM, il est logique que ce fichier se trouve dans le module `jvm` plutôt qu'il soit dupliqué dans `androidMain` et `desktopMain` comme suit :

```kotlin
package com.niji.claudio.common.data.model

import java.io.File

actual class MediaFile(filePath: String) {

    private var mFile: File? = null

    init {
        mFile = File(filePath)
    }

    actual fun appendBytes(bytes: ByteArray) {
        mFile?.appendBytes(bytes)
    }

    actual fun getLength(): Long {
        return mFile?.length() ?: 0
    }

    actual fun getPath() = mFile?.path ?: ""
}
```

Cette implémentation pourra compiler grâce aux instructions dans le fichier `common/build.gradle.kts` :

```kotlin
val jvm by creating {
    dependsOn(commonMain)
}
val androidMain by getting {
    dependsOn(jvm)
    dependencies {
        implementation(Libs.appcompat)
        implementation(Libs.activityCompose)
        implementation(Libs.coreKtx)
        implementation(Libs.firebaseMessaging)
        implementation(Libs.ktorClientAndroid)
        implementation(Libs.media3ExoPlayer)
        implementation(Libs.media3Ui)
    }
}
val desktopMain by getting {
    dependsOn(jvm)
    dependencies {
        implementation(compose.desktop.common)
        implementation(Libs.ktorClientJava)
    }
}
```

On voit ici la création d'un module nommé `jvm` (avec l'utilisation de "by creating") qui est celui présenté dans cette section et qui dépend du module `commonMain`. Les modules `androidMain` et `desktopMain` qui dépendent du module `jvm` (avec l'utilisation de "dependsOn(jvm)"). Avec cette implémentation, on peut utiliser le module `jvm` pour les implémentations communes qui seront exécutées dans la JVM.

## Module jsMain dans common

Ce module contient toute la configuration pour la partie JavaScript :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699377063475/09a95823-8367-481f-8f3b-e727340bc7d7.png align="left")

On voit ici l'ensemble des classes et fonctions qui seront spécifiques pour le JavaScript. La plupart des fonctions ici sont vides avec des "TODO" car pour le moment pas d'implémentation simple possible avec Compose Multiplatform.

## Module iosMain dans common

Ce module contient toute la configuration pour la partie iOS :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699440021157/711fa514-1a90-494a-8828-0102d42895be.png align="left")

On remarque les mêmes fichiers que pour le module `jsMain`, à la différence du `main.ios.kt` qui va construire un [`UIViewController`](https://developer.apple.com/documentation/uikit/uiviewcontroller) permettant d'avoir l'application qui se lance dans ce [View Controller](https://developer.apple.com/documentation/uikit/view_controllers) :

```kotlin
import androidx.compose.ui.window.ComposeUIViewController
import com.niji.claudio.common.ui.ClaudioApp
import com.niji.claudio.common.ui.MediasViewModel
import platform.UIKit.UIViewController

@Suppress("FunctionName", "unused")
fun MainViewController(): UIViewController =
    ComposeUIViewController {
        ClaudioApp(MediasViewModel())
    }
```

Cette déclaration permettra ensuite au module `iosApp` d'appeler cette fonction qui sera le point d'entrée de cette application.

Compose Multiplatform utilise une bibliothèque de conversion appelée "Compose for iOS" pour traduire les objets Compose en objets natifs [UIKit](https://developer.apple.com/documentation/uikit). C'est une des grosses différences avec [Flutter](https://flutter.dev/) où tout le code est compilé en code natif. Ce qui signifie que la bibliothèque [Flutter](https://flutter.dev/) complète doit être incluse dans l'application. Elle comprend un moteur de rendu, un ensemble de widgets, composants d'interface utilisateur et bibliothèques de support. Ce qui explique sa taille beaucoup plus importante par rapport à celle de Compose Multiplatform.

## Module desktopMain dans common

Ce module contient toute la configuration pour la partie desktop.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699440549767/38d20133-b316-45da-abba-adc5728dbb1a.png align="left")

On peut voir ici les mêmes fichiers présents dans le module `jsMain` à l'exception de ceux présents dans le module `jvm`.

## Module androidMain dans common

Ce module est le plus avancé car la cible primaire de ce projet concerne le mobile (et sûrement parce que je suis développeur Android 😛) avec :

* [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging?hl=fr) (FCM) pour effectuer des push notifications
    
* Player vidéo natif avec [ExoPlayer](https://github.com/google/ExoPlayer)
    
* Session audio avec le [MediaPlayer](https://developer.android.com/reference/android/media/MediaPlayer)
    
* Gestion de notifications de l'appareil
    
* Gestion du microphone de l'appareil
    
* Gestion du volume de l'appareil
    
* Gestion des permissions
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699450408689/c5c894f5-12fe-46eb-8208-e5821fa142a7.png align="left")

On peut constater que ce module est un peu plus founi dont je ne rentrerai pas dans le détail mais principalement des services pour la gestion de [FCM](https://firebase.google.com/docs/cloud-messaging?hl=fr), le player de média (vidéo et son) et autres.

## Module commonMain dans common

Ce module est LE module le plus important de ce projet car c'est ici que tout le coeur de l'app est implémenté. L'architecture est basée sur les concepts de la [Clean Architecture](https://www.freecodecamp.org/news/a-quick-introduction-to-clean-architecture-990c014448d2/) :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699451112959/0e2006d7-6ef5-4b07-aabe-591127794346.png align="left")

### Package data

Ce package se décompose comme suit :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699452140874/630f0f7e-b8c5-40eb-9db0-0f8e60970840.png align="left")

On peut y voir en sa racine les classes abstraites et interfaces qui seront implémentées dans les classes du package `internal` que nous verrons plus bas.

Le package `api` contient les interfaces pour les appels aux [API REST](https://www.redhat.com/fr/topics/api/what-is-a-rest-api) principalement :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699452670260/5b4e9d7d-4dff-4af2-9acf-98bde21971a9.png align="left")

Le package `feature` contient l'ensemble des [Use Cases](https://nanosoft.co.za/blog/post/clean-architecture-use-cases) qui font eux appels aux interfaces des [Repository](https://nanosoft.co.za/blog/post/clean-architecture-repository). Par exemple pour les fonctionnalités associées au `player`, on peut voir cet ensemble de [Use Cases](https://nanosoft.co.za/blog/post/clean-architecture-use-cases) :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699453436559/fe9636e8-f3d7-47e7-a5f3-3f248cf02298.png align="left")

Pour le `PlayMediaOnRemoteUseCase` :

```kotlin
package com.niji.claudio.common.data.feature.player.usecase

import com.niji.claudio.common.data.model.Media
import com.niji.claudio.common.data.model.User
import com.niji.claudio.common.internal.RepositoryLocator


class PlayMediaOnRemoteUseCase(val media: Media, val user: User) {
    suspend fun execute() = RepositoryLocator.playerRepository.playMedia(media, user)
}
```

On utilise le `RepositoryLocator` pour récupérer le [Repository](https://nanosoft.co.za/blog/post/clean-architecture-repository) souhaité. On aurait pu utiliser de l'[injection de dépendance](https://fr.wikipedia.org/wiki/Injection_de_d%C3%A9pendances) directement dans le constructeur du [Use Case](https://nanosoft.co.za/blog/post/clean-architecture-use-cases) mais pour le moment il n'existe pas de librairie capable de le faire dans l'ensemble des plateformes souhaitées (cf cet [exemple](https://github.com/android/nowinandroid/blob/5f5bd91a686a414007c9ce3c7acb26d2b0bd293a/core/domain/src/main/kotlin/com/google/samples/apps/nowinandroid/core/domain/GetSearchContentsUseCase.kt#L4) avec de l'injection de dépendance dans le [Use Case](https://nanosoft.co.za/blog/post/clean-architecture-use-cases) avec [Hilt](https://developer.android.com/training/dependency-injection/hilt-android?hl=fr)). Voici le `RepositoryLocator` :

```kotlin
package com.niji.claudio.common.internal

import com.niji.claudio.BuildKonfig
import com.niji.claudio.common.data.IRepositoryLocator
import com.niji.claudio.common.data.api.IClaudioApi
import com.niji.claudio.common.data.api.IHookApi
import com.niji.claudio.common.data.api.IPlayerApi
import com.niji.claudio.common.data.feature.device.IDeviceRepository
import com.niji.claudio.common.data.feature.hook.IHookRepository
import com.niji.claudio.common.data.feature.log.IDataLogRepository
import com.niji.claudio.common.data.feature.media.IMediaRepository
import com.niji.claudio.common.data.feature.player.IPlayerRepository
import com.niji.claudio.common.data.feature.user.IUserRepository
import com.niji.claudio.common.data.save.IClaudioDatabase
import com.niji.claudio.common.internal.repo.DataLogRepository
import com.niji.claudio.common.internal.repo.DeviceRepository
import com.niji.claudio.common.internal.repo.HookRepository
import com.niji.claudio.common.internal.repo.MediaRepository
import com.niji.claudio.common.internal.repo.PlayerRepository
import com.niji.claudio.common.internal.repo.PlayerRepositoryMqtt
import com.niji.claudio.common.internal.repo.UserRepository
import com.niji.claudio.common.internal.repo.api.ClaudioApi
import com.niji.claudio.common.internal.repo.api.FcmApi
import com.niji.claudio.common.internal.repo.api.SlackApi
import com.niji.claudio.common.internal.repo.save.ClaudioDatabase

object RepositoryLocator : IRepositoryLocator {

    private val claudioApi: IClaudioApi = ClaudioApi().api
    private val playerApi: IPlayerApi = FcmApi().api
    private val hookApi: IHookApi = SlackApi().api
    private val database: IClaudioDatabase = ClaudioDatabase()

    override val deviceRepository: IDeviceRepository = DeviceRepository(claudioApi, database)
    override val mediaRepository: IMediaRepository = MediaRepository(claudioApi, database)
    override val userRepository: IUserRepository =
        UserRepository(claudioApi, database, deviceRepository)
    override val playerRepository: IPlayerRepository = if (BuildKonfig.IS_USING_FCM) {
        PlayerRepository(playerApi, userRepository)
    } else {
        PlayerRepositoryMqtt(userRepository)
    }
    override val dataLogRepository: IDataLogRepository = DataLogRepository(database)
    override val hookRepository: IHookRepository = HookRepository(hookApi)
}
```

On peut voir dans cette classe l'ensemble des [Repositories](https://nanosoft.co.za/blog/post/clean-architecture-repository) utilisés avec leurs interfaces associées.

Le package `model` regroupe l'ensemble des objets qui transitent (Mélange avec les DTO 😱) et qui sont utilisés dans l'app :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699454591729/b726b991-b7cf-4a3d-8f73-4fc931fb7ba9.png align="left")

On retrouve les classes `Media` et `MediaFile` qui sont implémentées avec le pattern expect / actual dans les fichiers spécifiques aux plateformes (vu précédemment).

On a par exemple la classe `Device` :

```kotlin
package com.niji.claudio.common.data.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
class Device(
    var bddId: Long? = null,
    var name: String? = null,
    @SerialName("push_token")
    var pushToken: String? = null,
    @SerialName("id")
    var serverId: String? = null,
) {

    fun postDevicesBody(): Device {
        return Device(name = name, pushToken = pushToken)
    }

    companion object {
        const val NO_SERVER_ID = "no_server_id"
        const val NO_PUSH_TOKEN = "no_push_token"
        fun noTokenCurrentDevice(name: String): Device {
            return Device(
                name = name,
                pushToken = NO_PUSH_TOKEN,
                serverId = NO_SERVER_ID
            )
        }
    }
}
```

On utilise la [sérialisation Kotlin de la librairie Ktor](https://ktor.io/docs/serialization-client.html) pour les conversions des objets Kotlin en Json et inversement.

Le package `save` contient l'ensemble des interfaces pour la gestion de la base de données comme expliqué plus bas :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699455073490/9e0a5a2a-d8ef-4aab-a36e-6d701445cf75.png align="left")

L'ensemble des interfaces est rassemblé dans le `IClaudioDatabase` qui est implémenté dans les classes `ClaudioDatabase` en expect / actual.

### Package internal

Ce package contient les classes implémentant les interfaces définies dans le package `data`.

Voici ce qu'on y trouve :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699456882327/7625742c-0e93-4acb-830b-4cc01b2c9184.png align="left")

On retrouve le `RepositoryLocator` (vu précédemment) et également l'ensemble des [Repositories](https://nanosoft.co.za/blog/post/clean-architecture-repository) utilisés.

Dans le package `save` on utilise `ClaudioDatabase` pour la sauvegarde des données utiles de l'app sous forme de fichier Json. Dans un avenir proche, l'objectif sera de migrer toute la gestion de la base de données en utilisant [SQLDelight](https://github.com/cashapp/sqldelight) qui semble aujourd'hui mature et disponible sur l'ensemble des plateformes.

Dans le package `api` on a l'ensemble des appels aux API qui sont réalisés. On utilise [Ktor](https://ktor.io/docs/getting-started-ktor-client-multiplatform-mobile.html#android-activity) pour les appels réseaux et pour la [sérialisation Json](https://ktor.io/docs/serialization-client.html).

### Package tool

Ce package présenté comme suit :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699457853877/218157dd-3236-4018-86ca-2cb4bbd44433.png align="left")

Contient uniquement des objets Kotlin. Ces objets implémentent pour la plupart le concept de expect / actual et pour d'autres sont des regroupements de fonctions utiles pour le développement. Ici un exemple de `UiUtils` :

```kotlin
package com.niji.claudio.common.tool

import com.niji.claudio.common.ui.state.AppViewState

object UiUtils {
    private const val KYLO_BYTE = 1024
    private const val MEDIA_DISPLAY_COLUMN = "MediaDisplayColumn"
    private const val MEDIA_DISPLAY_GRID = "MediaDisplayGrid"

    fun getFriendlySize(size: Int): String {
        return if (size / KYLO_BYTE > 1000) {
            if (size / (KYLO_BYTE * KYLO_BYTE) > 1000) {
                "" + (size / (KYLO_BYTE * KYLO_BYTE * KYLO_BYTE)) + "Gb"
            } else {
                "" + (size / (KYLO_BYTE * KYLO_BYTE)) + "Mb"
            }
        } else {
            "" + (size / KYLO_BYTE) + "Kb"
        }
    }

    fun getMediaStateClass(state: String): AppViewState {
        return if (state == MEDIA_DISPLAY_COLUMN) {
            AppViewState.MediaDisplayColumn
        } else {
            AppViewState.MediaDisplayGrid
        }
    }

    fun getMediaStateString(state: AppViewState): String {
        return if (state is AppViewState.MediaDisplayColumn) {
            MEDIA_DISPLAY_COLUMN
        } else {
            MEDIA_DISPLAY_GRID
        }
    }

    fun isCorrectAddMediaField(str: String): Boolean {
        return str.length >= 2
    }
}
```

### Package ui

Ce package contient les classes qui seront utilisées pour le thème, les couleurs, les [viewModel](https://developer.android.com/jetpack/compose/libraries?hl=fr#viewmodel), les états de vues et surtout les composables (composants graphiques) ! Voici ce qu'il contient :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699458680773/1e7aea4a-a7df-420f-8bb4-557e0797c20c.png align="left")

On voit le `ClaudioApp` qui correspond au point d'entrée de l'application. Nous verrons plus bas les points d'entrée des apps dans les modules correspondants.

Le package state contient la classe `AppViewState` permettant de gérer la navigation et l'état UI de l'application.

```kotlin
package com.niji.claudio.common.ui.state

sealed class AppViewState {
    data object MediaDisplayColumn : AppViewState()
    data object MediaDisplayGrid : AppViewState()
    data object DeviceColumn : AppViewState()
    data object DataLog : AppViewState()
}
```

Le package `theme` contient les classes permettant de paramétrer le thème [Material](https://developer.android.com/jetpack/compose/themes/material?hl=fr) de Compose.

Le package widget contient les Composables utilisés dans l'app :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699459981409/a5d29368-68c8-45f7-b932-2192071cf274.png align="left")

### Module resources

Ce module contient les ressources graphiques communes utilisées dans l'application :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699460590977/dcf71996-de06-4226-b401-7dae22a63132.png align="left")

On utilise les classes `ClaudioPainterResource`, utilisant le pattern expect / actual, pour la conversion des resources en [painterResource](https://developer.android.com/jetpack/compose/resources?hl=fr#vector-assets) :

Pour Android :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699460902380/ffb5dd98-3d71-4ae8-abb0-ffc1e67a13a7.png align="left")

Pour Desktop :

```kotlin
package com.niji.claudio.common.ui.widget

import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource

actual object ClaudioPainterResource {

    private const val DRAWABLE_PATH = "drawable"

    @Composable
    actual fun get(claudioImageResource: ClaudioImageResource): Painter = painterResource(
        "${DRAWABLE_PATH}/${claudioImageResource.title}.${claudioImageResource.extension}"
    )
}
```

Ce pattern devrait être aussi utilisé pour les conversions des chaines de caractères en utilisant [stringResource](https://developer.android.com/jetpack/compose/resources?hl=fr#strings) (à faire de manière optimale dans une prochaine version 🙃 )

# Et pour les artéfacts finaux ?

Vous l'attendiez sûrement avec impatience ! Comment avec tous ces modules on arrive à réaliser un artéfact par plateforme ? Voici ce qu'on a :

## Application Android

Dans le module `appAndroid`, on retrouve ceci :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699526021321/c8a9aa50-f410-4391-b581-cb9ed12cc7bf.png align="left")

C'est tout ce dont l'app a besoin. Le dossier `keystore` (pas obligatoire) est utilisé ici pour éviter de devoir désinstaller et réinstaller l'application et ne pas perdre le token de push [FCM](https://firebase.google.com/docs/cloud-messaging?hl=fr) car les signatures diffèrent si elles sont compilées sur des machines différentes sans [keystore spécifique](https://developer.android.com/studio/publish/app-signing?hl=fr). On voit aussi le `google-services.json` qui est utilisé pour la [configuration de FCM](https://firebase.google.com/docs/android/setup?hl=fr).

Dans le fichier `build.gradle.kts`, on va retrouver la configuration de compilation uniquement pour Android qui est très simpliste où on peut voir l'implémentation du projet `common` comme étant la seule dépendance du projet :

```kotlin
plugins {
    id("org.jetbrains.compose")
    id("com.android.application")
    id("com.google.gms.google-services")
    kotlin("android")
}

dependencies {
    implementation(project(":common"))
}
```

En ce qui concerne la partie Android, on retrouve le standard de définition :

* la version de compilation
    
* la version cible
    
* la version minimale du SDK Android
    
* l'applicationId
    
* le versionCode
    
* le versionName
    
* la version de Java utilisée pour la compilation
    
* et la définition de la signature (uniquement en debug car pour le moment pas de release).
    

```kotlin
android {
    compileSdk = ProjectVersions.COMPILE_SDK
    namespace = ProjectVersions.PACKAGE_NAME_ANDROID
    defaultConfig {
        applicationId = ProjectVersions.PACKAGE_NAME_ANDROID
        minSdk = ProjectVersions.MIN_SDK
        targetSdk = ProjectVersions.TARGET_SDK
        versionCode = ProjectVersions.getAppVersionCode()
        versionName = ProjectVersions.getAppVersionName()
        setProperty("archivesBaseName", "${ProjectVersions.APP_NAME}-$versionName-$versionCode")
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    signingConfigs {
        getByName("debug") {
            keyAlias = "debug"
            keyPassword = "cl@udioDebugAlias"
            storeFile = file("./keystore/debug-keystore.jks")
            storePassword = "cl@udioDebug"
        }
    }
    buildTypes {
        getByName("release") {
            isMinifyEnabled = false
        }
        getByName("debug") {
            signingConfig = signingConfigs.getByName("debug")
            isDebuggable = true
        }
    }
}
```

Dans le `AndroidManifest.xml` on ne va retrouver que la définition de l'application puisque dans le module `common` avec le module androidMain, on a déjà toute la définition des [Activity](https://developer.android.com/reference/android/app/Activity) et [Services](https://developer.android.com/guide/components/services) (pour [FCM](https://firebase.google.com/docs/cloud-messaging?hl=fr)) utilisés dans l'app.

Si on regarde la partie GitHub Actions (vu précédemment dans le dossier `.github`), on a un job de compilation pour Android en debug comme suit :

```yaml
jobs:
  build_android:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3
      - name: Setup java 17
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '17'
          cache: 'gradle'
      - name: Build bundle debug
        id: build_bundle_debug
        run: ./gradlew clean :appAndroid:bundleDebug
      - name: Archive aab bundle debug
        uses: actions/upload-artifact@v3
        with:
          name: aab-bundle
          path: appAndroid/build/outputs/bundle/debug/*.aab
```

## Application iOS

Dans le module iosApp, on retrouve tout ce qui sera nécessaire pour la construction d'une application iOS. Beaucoup de fichiers sont générés par XCode et CocoaPods et certains fichiers doivent être configurés manuellement pour une compilation correcte de l'app. Ce dossier se base exclusivement sur ce qui existe dans les [exemples](https://github.com/JetBrains/compose-multiplatform/tree/master/examples/imageviewer/iosApp) disponibles de JetBrains.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699528486266/5381b53a-a7f6-4750-ab19-fd4b6fc609a3.png align="left")

## Application Desktop

Cette application est très simple à générer dont voici les fichiers :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699528905919/aa106860-e1fe-43b8-be80-98467f1c4c8d.png align="left")

On y retrouve un dossier icons permettant de définir une icône pour l'application (uniquement sur Mac Os).

Un `Main.kt` on ne peut plus simple :

```kotlin
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPlacement
import androidx.compose.ui.window.WindowState
import androidx.compose.ui.window.application
import com.niji.claudio.common.ui.ClaudioApp
import com.niji.claudio.common.ui.MediasViewModel
import com.niji.claudio.common.ui.theme.ClaudioTheme


fun main() = application {
    Window(
        onCloseRequest = ::exitApplication,
        title = "Claudio",
        state = WindowState(WindowPlacement.Maximized),
    ) {
        ClaudioTheme {
            ClaudioApp(MediasViewModel(), this.window)
        }
    }
}
```

Et un `build.gradle.kts` très simple aussi et compréhensible facilement :

```kotlin
import org.jetbrains.compose.desktop.application.dsl.TargetFormat

plugins {
    kotlin("multiplatform")
    id("org.jetbrains.compose")
}

kotlin {
    jvm {
        withJava()
    }
    sourceSets {
        val jvmMain by getting {
            dependencies {
                implementation(compose.desktop.currentOs)
                implementation(project(":common"))
            }
        }
    }
}

compose.desktop {
    application {
        mainClass = "MainKt"
        nativeDistributions {
            targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
            packageName = ProjectVersions.APP_NAME
            packageVersion = ProjectVersions.getAppVersionName()
            macOS {
                iconFile.set(project.file("icons/icon.icns"))
            }
            windows {
                iconFile.set(project.file("icon.ico"))
            }
            linux {
                iconFile.set(project.file("icon.png"))
            }
        }
    }
}
```

La commande `./gradlew packageUberJarForCurrentOS` permettra de générer un [Uber-JAR](https://imagej.net/develop/uber-jars) qui contiendra tout le nécessaire pour lancer l'application sur un OS similaire à celui ayant généré le Jar.

Si on regarde la partie GitHub Actions (expliqué précédemment), on a un job de compilation pour Desktop comme suit :

```yaml
  build_desktop:
    strategy:
      matrix:
        os: [ macos-latest, windows-latest, ubuntu-latest ]
    runs-on: ${{ matrix.os }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3
      - name: Setup java 17
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '17'
          cache: 'gradle'
      - name: Build desktop app ${{ matrix.os }}
        id: build_desktop
        run: ./gradlew packageUberJarForCurrentOS
      - name: Archive jar
        uses: actions/upload-artifact@v3
        with:
          name: desktop-${{ matrix.os }}
          path: appDesktop/build/compose/jars/*.jar
```

On aura donc trois JAR générés lors de l'execution de ces instructions : un pour Windows, un pour Linux et un pour MacOs. Ces trois JAR seront compilés sur les dernières versions de VMs GitHub Actions disponibles en architecture x64.

## Application JavaScript

Cette application est aussi très simple à générer dont voici les fichiers :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699530595179/06965da8-d930-4ff8-9f60-02befe05bd96.png align="left")

On y retrouve le `main.js.kt` :

```kotlin
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.Window
import com.niji.claudio.common.ui.ClaudioApp
import com.niji.claudio.common.ui.MediasViewModel
import org.jetbrains.skiko.wasm.onWasmReady

fun main() {
    onWasmReady {
        Window("Claudio") {
            Column(modifier = Modifier.fillMaxSize()) {
                ClaudioApp(MediasViewModel(), this)
            }
        }
    }
}
```

Ce main est appelé par le JavaScript qu'on retrouve dans l'`index.html` :

```xml
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Claudio</title>
    <script src="skiko.js"> </script>
    <link type="text/css" rel="stylesheet" href="styles.css" />
</head>
<body>
    <div>
        <canvas id="ComposeTarget" width="1800" height="900"></canvas>
    </div>
    <script src="appJs.js"> </script>
</body>
</html>
```

On y retrouve le script `appJs.js` et le `canvas` dans lequel sera dessiné l'application. Un peu de style dans le `styles.css` :

```css
#root {
    width: 100%;
    height: 100vh;
}

#root > .compose-web-column > div {
    position: relative;
}
```

Pour la compilation on aura ce `build.gradle.kts` :

```kotlin
plugins {
    kotlin("multiplatform")
    id("org.jetbrains.compose")
}

val copyJsResources = tasks.create("copyJsResourcesWorkaround", Copy::class.java) {
    from(project(":common").file("src/commonMain/resources"))
    into("build/processedResources/js/main")
}

kotlin {
    js(IR) {
        browser()
        binaries.executable()
    }
    sourceSets {
        val jsMain by getting  {
            dependencies {
                implementation(project(":common"))
                implementation(compose.ui)
                implementation(compose.foundation)
            }
        }
    }
}

compose.experimental {
    web.application {}
}
```

La création de la tâche `copyJsResourcesWorkaround` est un patch pour l'ajout des ressources graphiques (les icônes) à l'application.

# Démo

Voici le rendu tant attendu ! Pour les versions iOS et Android :

%[https://youtu.be/lXzl4C-wwz0] 

Pour Desktop :

%[https://youtu.be/HWrvUPPXJwE] 

Pour les navigateurs :

%[https://youtu.be/I5_k4S3OWzM] 

# Conclusion

J'ai pris beaucoup de plaisir à découvrir Compose Multiplatform et l'implémenter dans ce projet burlesque. J'ai été impressionné par la rapidité de mise à jour de ce framework puissant et performant.

La techno est bien mature pour les projets Android et Desktop et le devient sur iOS. Il y a de nombreuses différences entre Compose Multiplatform et les autres frameworks multiplateformes. Il est difficile de dire avec certitude que Compose Multiplatform va remplacer [Flutter](https://flutter.dev/) par exemple. Cependant, il existe plusieurs facteurs qui suggèrent que Compose Multiplatform a le potentiel de devenir une alternative viable. Google a annoncé qu'il prévoyait de sortir une version bêta de Compose Multiplatform pour iOS en 2024... Hâte de découvrir ça !

Merci pour la lecture 🖖
