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

Android Makers 2019 - Kotlin end to end: du cli...

Android Makers 2019 - Kotlin end to end: du client au serveur

Grâce à Kotlin/Native, disponible en version 1.0 depuis octobre, il est désormais possible d’utiliser Kotlin pour créer nos applications back et mobile natives en utilisant un même langage. Mais, en 2019, est-ce déjà une solution réellement viable ? Dans ce talk, nous allons vous présenter un retour d’expérience sur la création de notre application : de la création des Web Services avec Ktor aux clients iOS et Android, en passant par l'industrialisation et les tests.

Le futur du développeur mobile serait-il full-stack ?

Avatar for Julien Datour

Julien Datour

April 24, 2019
Tweet

More Decks by Julien Datour

Other Decks in Programming

Transcript

  1. BESOIN FONCTIONNEL Un serveur REST simple à appeler : >

    /people > /picture/{id} > /categories > /game/{category} > /leaderboard/{category} > /score
  2. UN PEU DE CONFIG (GRADLE) apply plugin: 'java' apply plugin:

    'kotlin' apply plugin: 'application' apply plugin: 'war' apply plugin: 'com.google.cloud.tools.appengine'
  3. UN PEU DE CONFIG (GRADLE) dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" implementation

    "io.ktor:ktor-server-servlet:$ktor_version" providedCompile "com.google.appengine:appengine:$appengine_version" }
  4. UN PEU DE CONFIG (GRADLE) dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" implementation

    "io.ktor:ktor-server-servlet:$ktor_version" providedCompile "com.google.appengine:appengine:$appengine_version" }
  5. UN PEU DE CONFIG (GRADLE) dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" implementation

    "io.ktor:ktor-server-servlet:$ktor_version" providedCompile "com.google.appengine:appengine:$appengine_version" }
  6. ROUTE fun Route.postScore() { post("/score") { val score: ScoreJson =

    call.receive(ScoreJson::class) call.respondText { "Roger that" } } }
  7. AU FINAL > La logique metier côté serveur... > ...

    exportable vers un autre framework.
  8. AU FINAL > La logique metier côté serveur... > ...

    exportable vers un autre framework.
  9. AU FINAL > La logique metier côté serveur... > ...

    exportable vers un autre framework. > Les DTO exportables vers un autre framework.
  10. REX

  11. > 100% Kotlin > Facile à mettre en place >

    Peu de lignes de code (~400)
  12. > 100% Kotlin > Facile à mettre en place >

    Peu de lignes de code (~400) > Facilité de déploiement
  13. > 100% Kotlin > Facile à mettre en place >

    Peu de lignes de code (~400) > Facilité de déploiement > Moderne (Coroutines, Kotlinx serialization)
  14. TARGETS, PRESETS, COMPILATIONS, SOURCESETS > Target = la plateforme visée

    (android, iOS, linux …) > Preset = une configuration prédéfinie pour la target > Compilation = le type d’output visé à l’issu de la compilation > SourceSets = Un ensemble de sources pour une plateforme
  15. UN PEU DE CONFIG (UNE FOIS DE PLUS ) kotlin

    { targets { fromPreset(presets.iosX64, 'iOS') { compilations.main.outputKinds('FRAMEWORK') } fromPreset(presets.jvm, 'android') }
  16. UN PEU DE CONFIG (UNE FOIS DE PLUS ) sourceSets

    { commonMain.dependencies { implementation 'org.jetbrains.kotlin:kotlin-stdlib-common' implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$coroutine_version" implementation "io.ktor:ktor-client-core:$ktor_version" }
  17. UN PEU DE CONFIG (UNE FOIS DE PLUS ) androidMain.dependencies

    { implementation 'org.jetbrains.kotlin:kotlin-stdlib-common' implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serialization_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version" implementation "io.ktor:ktor-client-android:$ktor_version" }
  18. UN PEU DE CONFIG (UNE FOIS DE PLUS ) androidMain.dependencies

    { implementation 'org.jetbrains.kotlin:kotlin-stdlib-common' implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serialization_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version" implementation "io.ktor:ktor-client-android:$ktor_version" }
  19. UN PEU DE CONFIG (UNE FOIS DE PLUS ) iOSMain.dependencies

    { implementation 'org.jetbrains.kotlin:kotlin-stdlib-common' implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:$serialization_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-native:$coroutine_version" implementation "io.ktor:ktor-client-ios:$ktor_version" } }
  20. UN PEU DE CONFIG (UNE FOIS DE PLUS ) iOSMain.dependencies

    { implementation 'org.jetbrains.kotlin:kotlin-stdlib-common' implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:$serialization_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-native:$coroutine_version" implementation "io.ktor:ktor-client-ios:$ktor_version" } }
  21. PRESENTER class QuizzPresenterImpl(private val view: QuizzView) : QuizzPresenter { override

    fun presentAnswer(indexAnswered: Int, indexOfCorrectAnswer: Int?) { val indexedValues = mutableListOf<Pair<Int, Boolean>>() if (indexOfCorrectAnswer == null) { indexedValues.add(Pair(indexAnswered, true)) } else { indexedValues.add(Pair(indexAnswered, false)) indexedValues.add(Pair(indexOfCorrectAnswer, true)) } Logger.log("Indexed values: $indexedValues") view.displayHiglightedAnswer(indexedValues) } override fun presentQuizz(entity: List<QuizzEntity>) { view.displayQuizz(entity.map { quizzEntity -> QuizzViewModel(quizzEntity.picture, quizzEntity.answers.map { AnswerViewModel(it.name) }) }) } override fun presentEndGame() { view.displayEndGame() } }
  22. SERVICE override suspend fun getCategories(): List<CategoryJson>? { return try {

    val categoriesString = client.call(Endpoint.get(Request.CATEGORIES)).response.readText() JSON.parse(CategoryJson.serializer().list, categoriesString) } catch (e: Exception) { null } }
  23. FILEWORKER interface IFileWorker { fun fileData(folder: String, name: String): ByteArray?

    fun fileExists(folder: String, name: String): Boolean fun numberOfFiles(folder: String): Int fun saveFile(data: ByteArray, folder: String, name: String) }
  24. FILEWORKER / EXPECT expect class FileWorker: IFileWorker { override fun

    fileData(folder: String, name: String): ByteArray? override fun fileExists(folder: String, name: String): Boolean override fun numberOfFiles(folder: String): Int override fun saveFile(data: ByteArray, folder: String, name: String) }
  25. FILEWORKER / ACTUAL IOS actual class FileWorker: IFileWorker { actual

    override fun fileExists(folder: String, name: String): Boolean { val path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true) val documentFolder = path.first() as NSString val targetFolder = documentFolder.stringByAppendingPathComponent(folder) val filePath = "$targetFolder/$name" return NSFileManager.defaultManager().fileExistsAtPath(filePath) } }
  26. FILEWORKER / ACTUAL ANDROID actual class FileWorker(private val cacheDir: File)

    : IFileWorker { actual override fun fileExists(folder: String, name: String): Boolean { val cacheDirectory = File(cacheDir, folder) val file = File(cacheDirectory, name) return cacheDirectory.exists() && cacheDirectory.isDirectory && file.exists() && file.isFile } }
  27. REX

  28. buildscript { ext { kotlin_version = '1.3.11' ktor_version = '1.1.1'

    serialization_version = '0.9.1' coroutine_version = '1.1.0' junit_version = '4.12' } } (Gradle 4.7)
  29. ET SUR ANDROID ? > Le framework compilé -> sharedcode.jar

    > implementation files("libs/sharedcode-android-1.0.jar")
  30. ET SUR ANDROID ? > le module du framework ->

    dependance gradle > implementation project(':sharedcode')
  31. ET SUR ANDROID ? UN IDE POUR TOUT > Android

    > Multiplatform > Serveur Ktor
  32. LES ENUM ( ! ) enum class GameCategory(val entryNumber: Int)

    { EASY(20), MEDIUM(40), HARD(80), EXPERT(CATEGORY_ALL_ENTRY_NUMBER); }
  33. LES ENUM ( ! ) __attribute__((objc_subclassing_restricted)) __attribute__((swift_name("GameCategory"))) @interface SharedcodeGameCategory :

    SharedcodeKotlinEnum + (instancetype)alloc __attribute__((unavailable)); + (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable)); @property (class, readonly) SharedcodeGameCategory *easy; @property (class, readonly) SharedcodeGameCategory *medium; @property (class, readonly) SharedcodeGameCategory *hard; @property (class, readonly) SharedcodeGameCategory *expert; - (instancetype)initWithName:(NSString *)name ordinal:(int32_t)ordinal __attribute__((swift_name("init(name:ordinal:)"))) __attribute__((objc_designated_initializer)) __attribute__((unavailable)); - (int32_t)compareToOther:(SharedcodeGameCategory *)other __attribute__((swift_name("compareTo(other:)"))); @property (readonly) int32_t entryNumber; @end;
  34. BYTEARRAY VS NSDATA ( ) @ExperimentalUnsignedTypes fun ByteArray.toData(): NSData? =

    memScoped { toCValues() .ptr .let { NSData.dataWithBytes(it, size.toULong()) } }
  35. LES COROUTINES ( ! ) private class MainDispatcher: CoroutineDispatcher() {

    override fun dispatch(context: CoroutineContext, block: Runnable) { dispatch_async(dispatch_get_main_queue()) { block.run() } } } class MainScope: CoroutineScope { private val dispatcher = MainDispatcher() private val job = Job() override val coroutineContext: CoroutineContext get() = dispatcher + job }
  36. CONS > Pas le même support entre les plateformes (exemple

    avec les coroutines) > Parametrisation du build
  37. AU FINAL ... > Professionnels ➡ petits projets > Courbe

    de difficulté > Manque d'informations