Pourquoi nous avons choisi Kotlin comme l'une de nos langues cibles. Partie 2: Kotlin Multiplatform

Nous continuons la série d'articles sur l'introduction du langage Kotlin dans notre processus de développement. Recherchez la première partie ici .

En 2017, l'ambitieux projet de Jetbrains a vu le jour, offrant une nouvelle perspective sur le développement multiplateforme. Compilation du code kotlin en code natif de diverses plateformes! À notre tour, à Domklik, nous cherchons toujours des moyens d'optimiser le processus de développement. Quoi de mieux que de réutiliser du code, pensons-nous? C'est vrai - n'écrivez pas du code du tout. Et pour que tout fonctionne comme vous le souhaitez. Mais cela ne se produit pas. Et s'il existe une solution qui nous permettrait, sans trop d'efforts, d'utiliser une base de code unique pour différentes plateformes, pourquoi ne pas essayer?

Alors bonjour à tous! Je m'appelle Gennady Vasilkov, je suis développeur Android chez Domklik et aujourd'hui je veux partager avec vous notre expérience dans le développement de Kotlin Multiplatform pour les appareils mobiles, nous dire quelles difficultés nous avons rencontrées, comment nous avons résolu et avec quoi nous nous sommes retrouvés. Le sujet intéressera sûrement ceux qui veulent essayer Kotlin MPP (projets multiplateformes), ou qui l'ont déjà essayé, mais qui n'ont pas terminé la production. Ou amené, mais pas comme je le voudrais. Je vais essayer de transmettre notre vision de la façon dont le processus de développement et de livraison des bibliothèques développées doit être organisé (en utilisant l'une d'entre elles comme exemple, je vais vous dire le début de notre chemin de développement dans Kotlin MPP).

Souhaitez-vous des histoires sur la façon dont nous l'avons fait? Nous les avons!



En bref sur la technologie


Pour ceux qui n'ont pas encore entendu ou qui ne se sont pas plongés dans le sujet, il se bloque dans le monde Java et va simplement dans le monde Kotlin (ou n'y va pas, mais jette un œil): Kotlin MPP est une technologie qui vous permet d'utiliser du code une fois écrit sur de nombreuses plateformes à la fois.

Le développement est, sans surprise, dans le langage Kotlin dans IntelliJ IDEA ou Android Studio. Le plus est que tous les développeurs Android (au moins avec nous) connaissent et aiment à la fois le langage et ces merveilleux environnements de développement.

Également un gros avantage dans la compilation du code résultant dans les langues natives de chaque plate-forme (OBJ-C, JS, tout est clair avec JVM).

Autrement dit, tout est cool. À mon avis, Kotlin Multiplatform est idéal pour apporter la logique métier aux bibliothèques. Il est également possible d'écrire une application complètement, mais pour l'instant elle semble trop extrême, mais le développement de la bibliothèque convient à des projets aussi importants que le nôtre.

Un peu technique, pour comprendre comment fonctionne le projet sur le Kotlin MPP


  • Le système de construction est Gradle , il prend en charge la syntaxe dans les scripts groovy et kotlin (kts).
  • Kotlin MPP a le concept de cibles - plates-formes cibles. Dans ces blocs, les systèmes d'exploitation dont nous avons besoin sont configurés, dont le code natif compilera notre code sur Kotlin. Dans l'image ci-dessous, 2 cibles sont implémentées, jvm et js (l'image est partiellement tirée du site Web ): la

    prise en charge d'autres plates-formes est implémentée de la même manière.
  • Ensembles de sources - le nom lui-même est clair que les codes sources des plates-formes sont stockés ici. Il existe un ensemble de sources et une plate-forme communs (il y en a autant qu'il y a de cibles dans le projet, suivi par l'IDE). Il est impossible de ne pas mentionner ici le mécanisme attendu.



    Le mécanisme permet d'accéder au code spécifique à la plate-forme à partir du module commun. Nous déclarons la déclaration expect dans le module Common et l'implémentons dans ceux de la plateforme. Voici un exemple d'utilisation du mécanisme pour obtenir la date sur les appareils:

    Common:
    internal expect val timestamp: Long
    
    Android/JVM:
    internal actual val timestamp: Long 
        get() = java.lang.System.currentTimeMillis()
    
    iOS:
    internal actual val timestamp: Long 
        get() = platform.Foundation.NSDate().timeIntervalSince1970.toLong()
    

    Comme vous pouvez le voir pour les modules de plate-forme, les bibliothèques système Java et iOS Foundation sont disponibles, respectivement.

Ces points suffisent pour comprendre ce qui sera discuté plus tard.

Notre expérience


Donc, à un moment donné, nous avons décidé que tout était accepté par Kotlin MPP (alors il s'appelait aussi Kotlin / Native) comme standard et nous avons commencé à écrire des bibliothèques dans lesquelles nous partagions le code commun. Au début, il s'agissait uniquement de code pour les plates-formes mobiles, à un moment donné, ils ont ajouté la prise en charge du backend jvm. Sur Android, le développement et la publication des bibliothèques développées n'ont pas posé de problèmes, sur iOS, dans la pratique, ils ont rencontré des problèmes, mais ils ont été résolus avec succès et un modèle de travail pour développer et publier des cadres a été élaboré.

Il est temps que quelque chose traîne!


Nous avons réalisé différentes fonctionnalités dans des bibliothèques distinctes que nous utilisons dans le projet principal.

1) Analytique


Contexte de l'événement


Ce n'est un secret pour personne que toutes les applications mobiles collectent un tas d'analyses. Les événements sont suspendus avec tous les événements importants et peu nombreux (par exemple, dans notre application, plus de 600 mesures diverses sont collectées). Et qu'est-ce que la collection métrique? D'une manière simple, il s'agit d'un appel à une fonction qui envoie un événement avec une certaine clé dans les entrailles du moteur d'analyse. Et ensuite, il va dans une variété de systèmes d'analyse comme Firebase, Appmetrica et autres. Quels sont les problèmes avec cela? Duplication constante du même code (plus ou moins) sur deux plateformes! Oui, et le facteur humain ne peut être trouvé nulle part - les développeurs pourraient se tromper, à la fois dans le nom des clés et dans l'ensemble de métadonnées transmises avec l'événement. Cela doit clairement être écrit une fois et utilisé sur chaque plate-forme. Un candidat idéal pour faire de la logique générale et de la technologie de test (c'est notre stylo de test dans Kotlin Mpp).

Comment mettre en œuvre


Nous avons transféré les événements eux-mêmes à la bibliothèque (sous forme de fonctions avec une clé câblée et un ensemble de métadonnées requises dans les arguments) et écrit une nouvelle logique pour le traitement de ces événements. Le format de l'événement a été unifié et un moteur de gestion (bus) a été créé, dans lequel tous les événements se sont déversés. Et pour divers systèmes d'analyse, les auditeurs d'adaptateurs ont écrit (une solution très utile s'est avérée être, lors de l'ajout de nouvelles analyses, nous pouvons facilement tout rediriger, ou sélectivement, chaque événement vers une nouvelle analyse).



Intéressant dans le processus de mise en œuvre


En raison des fonctionnalités de l'implémentation de l'utilisation des flux dans Kotlin / Native pour iOS, vous ne pouvez pas simplement prendre et travailler avec l'objet Kotlin en tant que singleton et y écrire des données à tout moment. Tous les objets qui transfèrent leur état entre les threads doivent être gelés (il existe une fonction freeze () pour cela). Mais la bibliothèque, entre autres, stocke un état qui peut changer pendant la durée de vie de l'application. Il existe plusieurs façons de résoudre cette situation, nous avons opté pour la plus simple:

Common:

expect class AtomicReference<T>(value_: T) {
    var value: T
}

Android:

actual class AtomicReference<T> actual constructor(value_: T) {
    actual var value: T = value_
}

iOS:

actual typealias AtomicReference<V> = kotlin.native.concurrent.AtomicReference<V>

Cette option convient au stockage d'état simple. Dans notre cas, la configuration de la plateforme est stockée:

object Config {
    val platform = AtomicReference<String?>("")
    var manufacturer = AtomicReference<String?>("")
    var model = AtomicReference<String?>("")
    var deviceId = AtomicReference<String?>("")
    var appVersion = AtomicReference<String?>("")
    var sessionId = AtomicReference<String?>("")
    var debug = AtomicReference<Boolean>(false)
}

Pourquoi est-ce nécessaire? Nous n'obtenons pas d'informations au début de l'application, lorsque le module doit déjà être chargé et que ses objets sont déjà gelés. Afin d'ajouter ces informations à la configuration, elles pourraient ensuite être lues à partir de différents flux sur iOS et un tel déplacement est nécessaire.

Nous avons également été confrontés au problème de la publication de la bibliothèque. Et si Android n'a eu aucun problème avec cela, alors la méthode proposée par la documentation officielle à l'époque pour connecter directement le framework assemblé au projet iOS ne nous convenait pas pour un certain nombre de raisons, je voulais obtenir de nouvelles versions aussi simples et transparentes que possible. De plus, la gestion des versions est prise en charge.
La solution est de connecter le framework via un fichier pod. Pour ce faire, ils ont généré le fichier podspec et le framework lui-même, les ont placés dans le référentiel et la connexion de la bibliothèque au projet est devenue très simple et pratique pour les développeurs iOS. De plus, pour la commodité du développement, nous collectons un artefact unique pour toutes les architectures, le soi-disant framework fat (en fait un binaire en gras dans lequel toutes les architectures et les méta-fichiers ajoutés générés par le plugin kotlin s'entendent). La mise en œuvre de tout cela sous le spoiler:

Peu importe comment nous l'avons fait avant la décision officielle
Après que le plugin kotlin a collecté pour nous un tas de différents frameworks pour différentes architectures, nous créons manuellement un nouveau framework universel dans lequel nous copions les métadonnées de n'importe quel (dans notre cas, nous avons pris arm64):
//add meta files (headers, modules and plist) from arm64 platform
task copyMeta() {
    dependsOn build

    doLast {
        copy {
            from("$buildDir/bin/iphone/main/release/framework/${frameworkName}.framework")
            into "$buildDir/iphone_universal/${frameworkName}.framework"
        }
    }
}

, , , . lipo, :
//merge binary files into one
task lipo(type: Exec) {
    dependsOn copyMeta

    def frameworks = files(
            "$buildDir/bin/iphone32/main/release/framework/${frameworkName}.framework/$frameworkName",
            "$buildDir/bin/iphone/main/release/framework/${frameworkName}.framework/$frameworkName",
            "$buildDir/bin/iphoneSim/main/release/framework/${frameworkName}.framework/$frameworkName"
    )
    def output = file("$buildDir/iphone_universal/${frameworkName}.framework/$frameworkName")
    inputs.files frameworks
    outputs.file output
    executable = 'lipo'
    args = frameworks.files
    args += ['-create', '-output', output]
}

, - Kotlin MPP Info.plist UIRequiredDeviceCapabilities, ( ). PlistBuddy:
//workaround
//remove UIRequiredDeviceCapabilities key from plist file (because we copy this file from arm64, only arm64 architecture was available)
task editPlistFile(type: Exec) {
    dependsOn lipo

    executable = "/bin/sh"
    def script = './scripts/edit_plist_file.sh'
    def command = "Delete :UIRequiredDeviceCapabilities"
    def file = "$buildDir/iphone_universal/${frameworkName}.framework/Info.plist"

    args += [script, command, file]
}

edit_plist_file.sh:
/usr/libexec/PlistBuddy -c "$1" "$2"

, zip ( podspec ):
task zipIosFramework(type: Zip) {
    dependsOn editPlistFile
    from "$buildDir/iphone_universal/"
    include '**/*'
    archiveName = iosArtifactName
    destinationDir(file("$buildDir/iphone_universal_zipped/"))
}

podspec :
task generatePodspecFile(type: Exec) {
    dependsOn zipIosFramework

    executable = "/bin/sh"

    def script = './scripts/generate_podspec_file.sh'

    def templateStr = "version_name"
    def sourceFile = './podspec/template.podspec'
    def replaceStr = "$version"

    args += [script, templateStr, replaceStr, sourceFile, generatedPodspecFile]
}

generate_podscpec_file.sh :
sed -e s/"$1"/"$2"/g <"$3" >"$4"

curl :
task uploadIosFrameworkToNexus(type: Exec) {
    dependsOn generatePodspecFile

    executable = "/bin/sh"
    def body = "-s -k -v --user \'$userName:$password\' " +
            "--upload-file $buildDir/iphone_universal_zipped/$iosArtifactName $iosUrlRepoPath"
    args += ['-c', "curl $body"]
}

task uploadPodspecFileToNexus(type: Exec) {
    dependsOn uploadIosFrameworkToNexus

    executable = "/bin/sh"
    def body = "-s -k -v --user \'$userName:$password\' " +
            "--upload-file $generatedPodspecFile $iosUrlRepoPath"
    args += ['-c', "curl $body"]
}

:
task createAndUploadUniversalIosFramework() {
    dependsOn uploadPodspecFileToNexus
    doLast {
        println 'createAndUploadUniversalIosFramework complete'
    }
}


Profit!


Au moment de la rédaction, il existe des solutions officielles pour créer un cadre gras et générer un fichier podspec (et généralement l'intégration avec CocoaPods). Il nous suffit donc de télécharger le cadre créé et le fichier podspec dans le référentiel et de connecter le projet dans iOS de la même manière qu'auparavant:




Après avoir implémenté un tel schéma avec un emplacement unique pour le stockage des événements, un mécanisme de traitement et d'envoi des métriques, notre processus d'ajout de nouvelles métriques a été réduit au fait qu'un développeur ajoute une nouvelle fonction à la bibliothèque, qui décrit la clé d'événement et les métadonnées nécessaires que le développeur doit transmettre pour un événement particulier . De plus, le développeur d'une autre plateforme télécharge simplement une nouvelle version de la bibliothèque et utilise la nouvelle fonction au bon endroit. Il est devenu beaucoup plus facile de maintenir la cohérence de l'ensemble du système d'analyse; il est également beaucoup plus facile maintenant de modifier l'implémentation interne du bus d'événements. Par exemple, à un moment donné, pour l'un de nos analystes, il nous a été nécessaire d'implémenter notre propre tampon d'événements, ce qui a été fait immédiatement dans la bibliothèque, sans avoir besoin de changer le code sur les plateformes. Profit!

Nous continuons à transférer de plus en plus de code vers Kotlin MPP, dans les articles suivants, je continuerai l'histoire de notre introduction au monde du développement multiplateforme en utilisant deux autres bibliothèques comme exemples, dans lesquelles la sérialisation et la base de données ont été utilisées, en travaillant avec le temps. Je vais vous parler des restrictions qui ont été rencontrées lors de l'utilisation de plusieurs frameworks kotlin dans un même projet iOS et comment contourner cette limitation.

Et maintenant brièvement sur les résultats de nos recherches


+ Tout fonctionne de manière stable en production.
+ Base de code unifiée de la logique de service métier pour toutes les plateformes (moins d'erreurs et de divergences).
+ Coûts d'assistance réduits et temps de développement réduit pour les nouvelles exigences.
+ Nous augmentons l'expertise des développeurs.

Liens connexes


Site Web Kotlin Multiplatform: www.jetbrains.com/lp/mobilecrossplatform

ZY:
Je ne veux pas répéter sur le laconicisme du langage, et donc me concentrer sur la logique métier lors de l'écriture de code. Je peux conseiller plusieurs articles qui

couvrent ce sujet: «Kotlin sous le capot - voir bytecode décompilé» - un article décrivant le fonctionnement du compilateur Kotlin et couvrant la structure de base du langage.
habr.com/en/post/425077
Deux parties de l'article "Kotlin, compilation en bytecode et performance", complétant l'article précédent.
habr.com/ru/company/inforion/blog/330060
habr.com/ru/company/inforion/blog/330064
Rapport de Pasha Finkelstein «Kotlin - deux ans de production et pas un seul écart», basé sur l'expérience de l'utilisation et de la mise en œuvre de Kotlin dans notre entreprise.
www.youtube.com/watch?v=nCDWb7O1ZW4

All Articles