Développement avec Kotlin: Exercices Pratiques

Kotlin
Exercices
Guide pratique de Kotlin pour développeurs Java : syntaxe moderne, programmation fonctionnelle, coroutines et développement d’applications Android. Inclut des exercices progressifs et des cas pratiques.
Auteur
Affiliations

Université de Toulon

LIS UMR CNRS 7020

Date de publication

2025-02-27

Configuration de l’environnement

Pour ces exercices, vous pouvez utiliser :

sdk install kotlin

Compilation et Exécution en ligne de commande

Le plus simple pour commencer est d’utiliser Kotlin Playground

Ensuite, pour compiler et exécuter un fichier Kotlin :

Fichier unique

Compilation basique
# Compiler un fichier Kotlin
kotlinc MonFichier.kt -d MonFichier.jar

# Exécuter le fichier compilé
java -jar MonFichier.jar

Compilation avec dépendances

Projet avec bibliothèques
# Compiler avec inclusion des dépendances runtime
kotlinc -cp "lib/*" MonFichier.kt -include-runtime -d MonFichier.jar

# Compiler plusieurs fichiers
kotlinc Fichier1.kt Fichier2.kt -include-runtime -d MonProjet.jar

Comment Kotlin compile-t-il ?

Structure de compilation

1. Organisation du code
  • Fichiers (.kt)
    • Peuvent contenir plusieurs classes/fonctions
    • Nom du fichier libre (mais conventionnellement PascalCase)
    • Extension .kt
  • Packages
    • Déclarés en début de fichier
    • Structurent le namespace
    • Correspondent aux dossiers
2. Point d’entrée
  • Fonction main()

    // Version simple
    fun main() {
        println("Hello")
    }
    
    // Version avec arguments
    fun main(args: Array<String>) {
        println("Hello ${args[0]}")
    }
  • Fichier avec plusieurs main()

    // filepath: /home/jovyan/work/local/src/main/kotlin/com/example/App.kt
    package com.example
    
    // Point d'entrée principal
    fun main() {
        println("Main app")
    }
    
    // Point d'entrée secondaire dans un object
    object DevTools {
        @JvmStatic
        fun main(args: Array<String>) {
            println("Dev tools")
        }
    }

Compilation des fichiers

3. Processus
// filepath: /home/jovyan/work/local/src/main/kotlin/com/example/Demo.kt
package com.example

// Une classe par fichier (recommandé)
class Demo {
    fun hello() = println("Hello")
}

// Plusieurs classes possible
class Helper {
    fun help() = println("Help")
}

// Top-level functions (dans le package)
fun topLevel() = println("Top level")

Le compilateur : 1. Crée un fichier .class pour chaque classe 2. Crée un fichier *Kt.class pour les fonctions top-level 3. Ajoute les métadonnées Kotlin.

Configuration du point d’entrée

Dans les cas simples, le point d’entrée est la fonction main(). Dans les cas plus complexes, il sera défini par Gradle ou Maven.

FitTracker App - Exercices Pratiques

1. Introduction aux bases

Variables et Types
Note

En Kotlin, la déclaration des variables se distingue de Java par plusieurs aspects clés. Kotlin utilise les mots-clés val pour les variables immuables (similaire à final en Java) et var pour les variables mutables. Le type est déclaré après le nom de la variable, et grâce à l’inférence de type, il peut souvent être omis. Les constantes sont déclarées avec const val au niveau du fichier ou d’un objet companion. Cette approche rend le code plus concis et sûr, tout en maintenant une excellente lisibilité.

Créez un nouveau fichier BasicExercise.kt et :

  1. Déclarez les variables suivantes pour un exercice :

    • Nom (non modifiable)
    • Durée en minutes (modifiable)
    • Calories brûlées (modifiable)
  2. Définissez une constante pour la durée minimale d’entraînement (15 minutes)

  3. Utilisez l’inférence de type pour simplifier vos déclarations

2. Classes

Note

Les classes en Kotlin offrent une approche moderne et concise de la programmation orientée objet. Contrairement à Java, Kotlin propose des data classes qui automatisent la génération des méthodes utilitaires (equals, hashCode, toString, copy), des classes avec constructeur primaire intégré, et une syntaxe simplifiée pour les propriétés. Les classes Kotlin sont final par défaut pour favoriser l’encapsulation, et la visibilité par défaut est public. L’héritage nécessite le mot-clé open, encourageant ainsi une conception réfléchie de la hiérarchie des classes. Les propriétés peuvent être déclarées directement dans le constructeur primaire, réduisant considérablement le boilerplate code par rapport à Java.

POO

Dans un fichier Model.kt, créez les classes suivantes :

  1. Une data class BasicExercise avec :
    • Type (enum class)
    • Nom
    • Durée
    • Calories
    • Date
  2. Une classe Workout contenant :
    • Liste d’exercices
    • Méthodes de calcul (durée et calories totales)
  3. Une classe User avec :
    • Profil utilisateur
    • Historique des workouts

3. Control Flow

Note

Kotlin offre des structures de contrôle modernes et expressives qui améliorent la lisibilité et la sécurité du code. Le when remplace avantageusement le switch de Java avec une syntaxe plus flexible et la possibilité de retourner des valeurs. Les boucles en Kotlin combinent la simplicité des itérateurs avec la puissance des expressions lambda. Les collections peuvent être parcourues de multiples façons, du for traditionnel aux fonctions d’ordre supérieur comme forEach. L’API des séquences permet une gestion efficace des itérations, particulièrement pour les grandes collections. Cette approche fonctionnelle du contrôle de flux rend le code plus concis et moins sujet aux erreurs.

Conditions et Boucles

Dans WorkoutLogic.kt, implémentez :

Compiler en utilisant Model.kt pour les classes de données.

  1. Une fonction calculateIntensity qui retourne :

    • “Facile” si < 100 calories
    • “Moyen” si entre 100 et 200 calories
    • “Difficile” si > 200 calories
  2. Une fonction categorizeExercise utilisant when pour classifier :

    • Cardio (course, vélo, natation)
    • Force (squat, pompes, tractions)
    • Flexibilité (yoga, étirements)
  3. Une fonction calculant les calories totales d’une liste d’entraînements.

  4. Parcours de collections Dans WorkoutLogic.kt, implémentez différentes façons de parcourir une liste d’exercices :

  • Utilisation d’un for traditionnel
  • Utilisation de forEach
  • Utilisation de forEachIndexed
  • Parcours avec gestion d’index manuels
  1. Itérateur personnalisé Créez un itérateur spécialisé dans WorkoutIterator.kt qui :
  • Implémente l’interface Iterator
  • Filtre les exercices selon leur intensité
  • Permet de parcourir les exercices par type
  • Utilise les séquences Kotlin

4. Functions

Note

Les fonctions en Kotlin offrent une flexibilité et une expressivité supérieures à Java. Les extension functions permettent d’ajouter des méthodes à des classes existantes sans les modifier, les paramètres nommés et par défaut réduisent la nécessité de surcharges, et les fonctions d’ordre supérieur permettent de manipuler les fonctions comme des valeurs de première classe. Cette approche fonctionnelle, combinée avec la concision de la syntaxe et l’inférence de type, rend le code plus lisible et plus maintenable tout en réduisant significativement le boilerplate code.

Fonctions et Extensions

Dans un fichier Extensions.kt, créez des fonctions pour implémenter :

  1. Une extension function sur List<Exercise> calculant la moyenne des calories

  2. Une fonction createExercise avec des paramètres par défaut pour :

    • Type
    • Durée
    • Intensité
  3. Une fonction d’ordre supérieur filtrant les exercices par type. Le critère de filtrage sera une lambda.

5. Collections

Note

Les collections en Kotlin offrent une API riche et intuitive, fortement inspirée de la programmation fonctionnelle. Contrairement à Java, Kotlin fait une distinction claire entre les collections mutables et immutables, et propose des opérations de transformation puissantes comme map, filter, groupBy et maxBy. Ces opérations peuvent être chaînées de manière fluide et sont optimisées pour la performance. L’API des collections Kotlin simplifie considérablement le traitement des données tout en réduisant les risques d’erreurs grâce à son typage fort et ses opérations null-safe.

Manipulation de Données

Dans un fichier DataManipulation.kt, créez des fonctions pour : Travaillez avec une liste d’exercices pour :

  1. Filtrer les exercices > 20 minutes avec map/filter

  2. Grouper les exercices par type avec groupBy

  3. Trouver l’exercice le plus intense avec maxBy

6. Scope Functions

Note

Les scope functions (let, run, with, apply, et also) sont des fonctions spécifiques à Kotlin qui permettent d’exécuter un bloc de code dans le contexte d’un objet. Chaque scope function a un cas d’utilisation particulier : with pour grouper des opérations sur un objet, let pour le traitement des nullables, apply pour la configuration d’objets, run pour la combinaison des deux précédents, et also pour les effets secondaires. Ces fonctions améliorent la lisibilité du code en évitant la répétition et en clarifiant l’intention du développeur, tout en maintenant un scope bien défini pour les opérations.

Contexte et Configuration

Dans un fichier ScopeFunctions.kt, créez des scope functions pour :

  1. Configurer un nouveau workout avec with

  2. Traiter un exercice optionnel avec let

  3. Initialiser un profil utilisateur avec apply

7. Null Safety

Note

La gestion des valeurs nullables est l’une des fonctionnalités phares de Kotlin, conçue pour éliminer le risque de NullPointerException. Contrairement à Java, Kotlin différencie explicitement les types nullables (marqués par ?) des types non-nullables. Le langage fournit des opérateurs sécurisés comme ?. (safe call), ?: (Elvis), et !! (non-null assertion) pour manipuler les valeurs potentiellement nulles. Cette approche force les développeurs à gérer explicitement les cas de nullité dès la compilation, rendant le code plus sûr et plus prévisible.

Gestion des Nullables

Dans un fichier NullSafety.kt, créez des fonctions pour :

  1. Compléter la classe Exercise avec un coach optionnel

  2. Une fonction utilisant l’opérateur Elvis pour la durée

  3. Une fonction safe pour accéder aux détails d’exercice

8. Coroutines

Note

Les coroutines sont la solution de Kotlin pour la programmation asynchrone, offrant une alternative élégante aux callbacks et aux promesses. Elles permettent d’écrire du code asynchrone de manière séquentielle, ce qui le rend plus lisible et plus facile à maintenir. Les coroutines utilisent des suspending functions qui peuvent être pausées et reprises sans bloquer le thread principal, offrant ainsi une excellente performance et une meilleure utilisation des ressources. Avec les scopes et les dispatchers, Kotlin fournit un contrôle précis sur l’exécution des tâches asynchrones, tout en simplifiant la gestion des erreurs et l’annulation des opérations en cours.

Asynchrone

Dans un fichier AsyncTasks.kt, créez des fonctions pour :

  1. Une fonction de chargement asynchrone d’historique. Cette fonction simule un chargement réseau avec un délai de 2 secondes. Executez la, puis ecrivez une boucle qui affiche “Chargement…” toutes les 500ms jusqu’à ce que le chargement soit terminé.

  2. Un compteur de temps pour exercice en cours. Cette fonction simule un exercice en cours avec un délai de 1 seconde. Executez la, puis ecrivez une boucle qui affiche le temps écoulé toutes les 30 jusqu’à ce que l’exercice soit terminé.

  3. Un gestionnaire d’exercices simultanés. Cette fonction simule plusieurs exercices en parallèle en simulant leur durée. Appeler cette fonction dans le programme principal pour afficher les exercices en cours et afficher le nombre de calories brûlées à la fin pour chaque exercice.

9. Héritage et Interfaces

Note

L’héritage en Kotlin est plus restrictif qu’en Java pour favoriser une meilleure conception. Les classes sont final par défaut et nécessitent le mot-clé open pour être héritées. Les interfaces peuvent contenir des implémentations par défaut, et Kotlin supporte la délégation de classe comme alternative à l’héritage. Les propriétés et méthodes doivent également être explicitement marquées comme open pour être surchargées, encourageant ainsi une conception plus réfléchie de la hiérarchie des classes. Les membres non abstracts d’une classe abstraite sont final par défaut, et les classes internes sont static par défaut.

La délégation de classe est une alternative à l’héritage qui permet à une classe d’utiliser les membres d’une autre classe sans hériter de celle-ci. Cela permet de réutiliser du code sans créer de dépendances inutiles entre les classes. Kotlin fournit une syntaxe concise pour la délégation de classe, ce qui rend le code plus lisible et plus maintenable.

Héritage et Polymorphisme

Dans un fichier ExerciseHierarchy.kt, implémentez (dans un sous-package pour éviter les confusions avec les classes précédentes) :

  1. Interface de base
  • Créez une interface Exercise avec :
    • Une propriété name en lecture seule
    • Une propriété duration en lecture seule
    • Une méthode calculateCalories()
  1. Classe abstraite
  • Créez une classe abstraite AbstractExercise qui :
    • Implémente l’interface Exercise
    • Déclare toutes les propriétés et méthodes comme abstraites
    • Ajoute une méthode describe() ouverte à la surcharge
  1. Exercices spécifiques
  • Créez deux classes concrètes :
    1. CardioExercise avec :
      • Distance parcourue (km)
      • Intensité (1-10)
      • Calcul des calories : duration * intensity * 3.5
    2. StrengthExercise avec :
      • Nombre de répétitions
      • Poids (kg)
      • Calcul des calories : duration * weight * 2.1
  1. Système de suivi
  • Créez une interface ExerciseTracker avec :
    • startExercise()
    • pauseExercise()
    • stopExercise()
    • getCurrentStatus()
  1. Implémentation basique
  • Créez BasicExerciseTracker qui :
    • Implémente ExerciseTracker
    • Gère un statut interne (“Not started”, “Started”, “Paused”, “Stopped”)
    • Implémente toutes les méthodes de l’interface
  1. Délégation multiple
  • Créez une classe SmartTracker qui :
    • Combine un exercice et un tracker
    • Utilise la délégation pour implémenter les deux interfaces
    • Permet de suivre n’importe quel type d’exercice
  1. Programme principal
  • Créez une fonction main qui :
    1. Instancie différents types d’exercices
    2. Crée un tracker basique
    3. Crée des smart trackers pour chaque exercice
    4. Démontre le cycle de vie complet des exercices

10. Sealed Class, Gestion des Erreurs

Note

Les sealed classes en Kotlin représentent une hiérarchie de classes restreinte où toutes les sous-classes doivent être déclarées dans le même fichier que la classe scellée. C’est une forme de restriction plus souple que enum mais plus stricte qu’une classe abstraite classique. Cette fonctionnalité est particulièrement utile pour représenter un ensemble limité d’états ou de types, comme dans le cas de la gestion des résultats d’opérations.

Note

Kotlin améliore la gestion des erreurs par rapport à Java avec le type Result, les expressions try et une meilleure gestion des exceptions. Le type Result encapsule un succès ou un échec, permettant une gestion plus fonctionnelle des erreurs. Les expressions try peuvent retourner une valeur, et les exceptions vérifiées ne sont pas obligatoires. Cette approche rend le code plus sûr et plus expressif tout en réduisant le boilerplate code.

Le try en Kotlin est une expression, ce qui signifie qu’il peut retourner une valeur. Cela permet de gérer les erreurs de manière plus concise et expressive. Les exceptions vérifiées ne sont pas obligatoires en Kotlin, ce qui simplifie la gestion des erreurs et améliore la lisibilité du code. Les coroutines offrent également une gestion des erreurs plus efficace, avec des fonctions de sécurité pour les appels asynchrones`

Exercices de Gestion d’Erreurs

Dans un fichier ErrorHandling.kt, implémentez et lisez en détail :

package fr.univtln.bruno.exercices.basic

import kotlinx.coroutines.runBlocking

// Sealed class to represent the result of an operation, which can be either a success or an error
sealed class ExerciseResult<out T> {
    // Success case holding the data
    data class Success<T>(val data: T) : ExerciseResult<T>()
    // Error case holding the exception
    data class Error(val exception: Exception) : ExerciseResult<Nothing>()
}

// Function to validate an exercise, returning an ExerciseResult
fun validateExercise(exercise: Exercise): ExerciseResult<Exercise> = when {
    exercise.duration < 0 -> ExerciseResult.Error(IllegalArgumentException("Duration cannot be negative"))
    exercise.name.isBlank() -> ExerciseResult.Error(IllegalArgumentException("Name cannot be blank"))
    else -> ExerciseResult.Success(exercise)
}

// Extension function for handling success cases
fun <T> ExerciseResult<T>.onSuccess(action: (T) -> Unit): ExerciseResult<T> {
    if (this is ExerciseResult.Success) action(data)
    return this
}

// Extension function for handling error cases
fun <T> ExerciseResult<T>.onError(action: (Exception) -> Unit): ExerciseResult<T> {
    if (this is ExerciseResult.Error) action(exception)
    return this
}

// Define a Client interface for fetching exercises
interface Client {
    suspend fun getExercise(id: Int): Exercise
}

// Implement the Client interface with a simulated API call
class ApiClient : Client {
    override suspend fun getExercise(id: Int): Exercise {
        // Simulate an API call
        return CardioExercise("Running", 30, 10.0, 5)
    }
}

// Function to safely make an API call, returning an ExerciseResult
suspend fun safeApiCall(call: suspend () -> Exercise): ExerciseResult<Exercise> = try {
    ExerciseResult.Success(call())
} catch (e: Exception) {
    ExerciseResult.Error(e)
}

// Function to get an exercise safely using a client, with error handling
suspend fun getExerciseSafely(client: Client, id: Int): ExerciseResult<Exercise> = safeApiCall {
    client.getExercise(id)
}

// Main function to demonstrate error handling
fun main(): Unit = runBlocking {
    val client = ApiClient()
    val exercise = CardioExercise("Running", 30, 10.0, 5)

    // Validate the exercise and handle the result
    validateExercise(exercise)
        .onSuccess { println("Exercise is valid: $it") }
        .onError { println("Validation failed: ${it.message}") }

    // Fetch an exercise safely and handle the result
    getExerciseSafely(client, 1)
        .onSuccess { println("Retrieved exercise: $it") }
        .onError { println("API call failed: ${it.message}") }
}

11. Génériques

Note

Les génériques en Kotlin améliorent le système de type en permettant d’écrire du code qui fonctionne avec différents types tout en maintenant la sécurité du typage. Kotlin ajoute des fonctionnalités avancées comme la variance déclarée (out/in), les contraintes de type (where), et les projections de type (*). Ces concepts permettent une meilleure flexibilité et sécurité dans la conception des APIs.

Note

La covariance (out) permet à un type générique de préserver la relation d’héritage du type qu’il contient. En d’autres termes, si B hérite de A, alors Container<B> peut être utilisé là où Container<A> est attendu.

Note

La covariance (in) permet à un type générique de consommer des valeurs de type générique. En d’autres termes, si B hérite de A, alors Container<A> peut être utilisé là où Container<B> est attendu.

Exercices sur les Génériques

Dans un fichier Generics.kt, implémentez :

import fr.univtln.bruno.exercices.basic.inheritance.Exercise
import fr.univtln.bruno.exercices.basic.inheritance.CardioExercise
import fr.univtln.bruno.exercices.basic.inheritance.StrengthExercise

// 1. Container générique avec covariance
// T est covariant, on peut donc utiliser Container<StrengthExercise> là où Container<Exercise> est attendu
// Par exemple, si Container<StrengthExercise> hérite de Container<Exercise>, on peut utiliser Container<StrengthExercise> là où Container<Exercise> est attendu
// Cela permet de récupérer des objets de type StrengthExercise dans un container de type Exercise
interface Container<out T> {
    fun get(): T
}

// 2. Logger générique avec contravariance
// T est contravariant, on peut donc utiliser Logger<Exercise> là où Logger<StrengthExercise> est attendu
// Par exemple, si Logger<Exercise> hérite de Logger<StrengthExercise>, on peut utiliser Logger<Exercise> là où Logger<StrengthExercise> est attendu
// Cela permet de logger des objets de type Exercise dans un logger de type StrengthExercise
interface Logger<in T> {
    fun log(item: T)
}

// 3. Classe de gestion d'exercices générique
// T est un type générique qui hérite de Exercise
// On peut donc utiliser ExerciseManager<CardioExercise> ou ExerciseManager<StrengthExercise>
// Cela permet de gérer des exercices de type CardioExercise ou StrengthExercise
class ExerciseManager<T : Exercise> {
    private val exercises = mutableListOf<T>()

    fun add(exercise: T) {
        exercises.add(exercise)
    }

    fun getByType(predicate: (T) -> Boolean): List<T> =
        exercises.filter(predicate)
}

// 4. Classe avec contraintes multiples
// T est un type générique qui hérite de Exercise et Comparable<T>
// On peut donc utiliser WorkoutAnalyzer<CardioExercise> ou WorkoutAnalyzer<StrengthExercise>
// Cela permet d'analyser des exercices de type CardioExercise ou StrengthExercise
// La méthode findMostIntense renvoie l'exercice le plus intense en fonction de la comparaison
class WorkoutAnalyzer<T> where T : Exercise, T : Comparable<T> {
    fun findMostIntense(exercises: List<T>): T? =
        exercises.maxOrNull()
}

// 5. Extension function générique avec reified type
// Cette fonction d'extension permet de filtrer une liste d'exercices par type
// Par exemple, si on a une liste d'exercices de différents types, on peut filtrer les exercices de type CardioExercise
// inline permet d'utiliser le type reified T pour filtrer les éléments de la liste
// reified permet de récupérer le type de T à l'exécution
inline fun <reified T : Exercise> List<Exercise>.filterByType(): List<T> =
    filterIsInstance<T>()

// Programme de démonstration
fun main() {
    // Création d'instances
    val cardioManager = ExerciseManager<CardioExercise>()
    val strengthManager = ExerciseManager<StrengthExercise>()

    // Ajout d'exercices
    cardioManager.add(CardioExercise("Running", 30, 5.0, 8))
    strengthManager.add(StrengthExercise("Squats", 20, 12, 60))

    // Utilisation des filtres génériques
    // it est un paramètre implicite qui représente chaque élément de la liste
    val longCardio = cardioManager.getByType { it.duration > 20 }
    println("Long cardio exercises: $longCardio")

    // Utilisation de l'extension function avec type reified
    val exercises = listOf(
        CardioExercise("Running", 30, 5.0, 8),
        StrengthExercise("Squats", 20, 12, 60)
    )

    val cardioOnly = exercises.filterByType<CardioExercise>()
    println("Cardio exercises: $cardioOnly")
}

Koans Kotlin

Les Koans Kotlin sont des exercices interactifs conçus pour apprendre les idiomes Kotlin progressivement. Disponibles sur Kotlin Koans Online, ils couvrent les concepts essentiels du langage.

Un vrai projet Kotlin

IntelliJ IDEA

Configuration recommandée
  1. Créer un projet Kotlin
    • File → New → Project
    • Sélectionner “Kotlin”
    • Choisir “JVM” et “Gradle”
  2. Exécution
    • Cliquer sur le bouton ▶️ à côté de main()
    • Ou utiliser Shift + F10

Dans VS Code

Configuration recommandée
  1. Installer les extensions :
    • “Kotlin” de mathiasfrohlich
    • “Kotlin Language” de fwcd
    • “Code Runner” de Jun Han
  2. Créer un fichier .kt et ajouter une fonction main :
fun main() {
    println("Hello Kotlin!")
}
  1. Cliquer sur ▶️ en haut à droite ou utiliser Ctrl+Alt+N

Structure de projet

Organisation recommandée
monprojet/
├── src/
│   ├── main/
│   │   └── kotlin/
│   │       └── MonFichier.kt
│   └── test/
│       └── kotlin/
│           └── MonFichierTest.kt
└── build/
    └── libs/
        └── monprojet.jar

Rest Client App - Exercices Pratiques

Créer un projet Kotlin pour une application de gestion de clients REST dans IntelliJ IDEA.

Nous allons utiliser Ktor pour effectuer des requêtes HTTP vers une API REST. Ktor est un framework client et serveur pour créer des applications web et mobiles.

ajouter les dépendances suivantes dans le fichier build.gradle.kts :

plugins {
    kotlin("jvm") version "1.9.0"
    kotlin("plugin.serialization") version "1.9.0"
}

dependencies {
    implementation("io.ktor:ktor-client-core:2.3.7")
    implementation("io.ktor:ktor-client-cio:2.3.7")
    implementation("io.ktor:ktor-client-content-negotiation:2.3.7")
    implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.7")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}

Créer les classes qui représentent les entités dans l’application clientes. La bibliothèque Ktor utilise kotlinx.serialization pour la sérialisation/désérialisation des objets JSON.

package com.example.api

import kotlinx.serialization.Serializable

@Serializable
data class Exercise(
    val id: Int,
    val name: String,
    val duration: Int,
    val calories: Int
)

Créer une classe cliente pour effectuer des requêtes HTTP vers l’API REST.

package com.example.api

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*

class ExerciseApiClient(private val baseUrl: String = "http://localhost:8080") {
    private val client = HttpClient(CIO) {
        install(ContentNegotiation) {
            json()
        }
    }

    suspend fun getExercises(): List<Exercise> {
        return client.get("$baseUrl/exercises").body()
    }

    suspend fun getExercise(id: Int): Exercise {
        return client.get("$baseUrl/exercises/$id").body()
    }

    suspend fun createExercise(request: CreateExerciseRequest): Exercise {
        return client.post("$baseUrl/exercises") {
            contentType(ContentType.Application.Json)
            setBody(request)
        }.body()
    }

    suspend fun updateExercise(id: Int, request: CreateExerciseRequest): Exercise {
        return client.put("$baseUrl/exercises/$id") {
            contentType(ContentType.Application.Json)
            setBody(request)
        }.body()
    }

    suspend fun deleteExercise(id: Int): Boolean {
        val response = client.delete("$baseUrl/exercises/$id")
        return response.status == HttpStatusCode.NoContent
    }

    fun close() {
        client.close()
    }
}
package com.example

import com.example.api.*
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val client = ExerciseApiClient()

    try {
        // Créer un exercice
        val newExercise = client.createExercise(
            CreateExerciseRequest(
                name = "Course",
                duration = 30,
                calories = 300
            )
        )
        println("Exercice créé: $newExercise")

        // Récupérer tous les exercices
        val exercises = client.getExercises()
        println("Liste des exercices: $exercises")

        // Récupérer un exercice spécifique
        val exercise = client.getExercise(newExercise.id)
        println("Détail de l'exercice: $exercise")

        // Mettre à jour un exercice
        val updatedExercise = client.updateExercise(
            newExercise.id,
            CreateExerciseRequest(
                name = "Course rapide",
                duration = 45,
                calories = 450
            )
        )
        println("Exercice mis à jour: $updatedExercise")

        // Supprimer un exercice
        val deleted = client.deleteExercise(newExercise.id)
        println("Exercice supprimé: $deleted")

    } catch (e: Exception) {
        println("Erreur: ${e.message}")
    } finally {
        client.close()
    }
}

Réutilisation