Par Valentin Brosseau / @c4software
Vos applications Compose tournent sur Android.
Question : et sur iOS ? Sur un ordinateur ?
Trois fois le même travail 🚨
Du code Kotlin commun à Android, iOS, Desktop, Web.
Mais uniquement la logique, pas l'interface.
L'étape suivante, par JetBrains (en lien avec Google).
L'interface aussi devient commune.
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("J'ai été cliqué $count fois")
}
}Même Kotlin, mêmes composants, même Material Design.
Même promesse, mais avec un autre langage (Dart).
Ici, nous restons en Kotlin.
composeApp/src
├── commonMain → le code commun
├── androidMain → le spécifique Android
├── iosMain → le spécifique iOS
└── desktopMain → le spécifique Desktop (JVM)Dans quel dossier doit se trouver la majorité de votre code ?
Le plus possible.
Les autres dossiers ne contiennent que ce qui ne peut pas être commun.
« Comment faire pour que mon code soit le plus commun possible ? »
Coder « comme avant » est l'erreur à ne pas commettre.
Vous devez afficher la caméra.
Elle fonctionne différemment sur chaque plateforme. Vous réécrivez tout l'écran trois fois ?
@Composable
expect fun CameraView()
@Composable
fun ScanScreen() {
Box(modifier = Modifier.fillMaxSize()) {
CameraView()
// Le reste de l'écran est commun
}
}expect : « chaque plateforme doit fournir ceci » (dans commonMain).actual : l'implémentation, dans chaque plateforme.Fonctions, classes, propriétés : tout y passe.
D'abord le code commun, sans expect.
Ensuite seulement, ce qui doit vraiment être spécifique.
val leDrawable = Res.drawable.NOM_DE_LA_RESSOURCE
val leTexte = stringResource(Res.string.app_name)Communes elles aussi : commonMain/composeResources.
Les traductions suivent la langue de l'utilisateur.
gradle/libs.versions.toml
Toutes les versions des dépendances, au même endroit.
Jetpack Navigation n'existe que sur Android.
Comment naviguer entre nos écrans ?
enum class Route(val path: String) {
Default(getDefaultRoute()),
Main("/"),
Hello("/hello"),
Scan("/scan")
}
expect fun getDefaultRoute(): StringUn NavHost et des routes, très proche de Jetpack.
L'injection de dépendances : séparer la création d'un objet de son utilisation.
fun setupKoin() = startKoin {
modules(platformSpecificModule())
modules(networkModule)
}class NetworkOperation(private val client: HttpClient) {
suspend fun getHello(): String? {
return try {
client.get("https://…/hello.md").body<String>()
} catch (e: Exception) {
null
}
}
}Le client HTTP, fourni par Koin.
class MainViewModel(private val networkOperation: NetworkOperation) : ViewModel(), IMainViewModel {
override val isLoading: MutableStateFlow<Boolean> = MutableStateFlow(false)
}L'état de l'écran, observé par la vue.
Pourquoi une interface IMainViewModel ?
Une interface se remplace facilement par un faux ViewModel.
Les tests aussi sont communs : commonTest.
expect open class LocalStorage : ILocalStorage {
override fun save(key: String, value: String)
override fun get(key: String): String
}Le contrat est commun, l'implémentation est par plateforme.
commonMain.expect / actual pour ce qui diffère vraiment.commonTest.Place au TP 🚀