Par Valentin Brosseau / @c4software
Les données sont dans le téléphone.
Question : et si on veut les partager entre plusieurs téléphones ? Ou avec un site web ?
Application Android ─┐
├──→ API ──→ Base de données
Page web (Vue.js) ───┘Un seul endroit pour les données, plusieurs façons de les afficher.
/v1/lampadaires.GET, POST, DELETE…Le parc de lampadaires solaires d'une commune.
{
"id": 3,
"numeroSerie": "LUM-2026-0003",
"titre": "Route du col",
"image": "/images/lampadaire-3.jpg",
"statut": "EN_PANNE",
"dateInstallation": "2026-05-20"
}| Action | Verbe | Route |
|---|---|---|
| Lister | GET |
/v1/lampadaires |
| Lire | GET |
/v1/lampadaires/{id} |
| Créer | POST |
/v1/lampadaires |
| Supprimer | DELETE |
/v1/lampadaires/{id} |
Create, Read, Update, Delete.
Le serveur, en quel langage l'écrire ?
Une route, une requête SQL, une règle métier…
Tout écrire dans un seul fichier, ce serait plus simple, non ?
Mais pour cinquante ?
Route HTTP : la requête, la réponse en JSON
Service Les règles métier
DAO Les requêtes vers la base
Table La description de la table en KotlinChaque couche ne parle qu'à celle du dessous.
cours.brosseau
├── Application.kt
├── plugins/ Serialization, DatabaseConnect…
├── common/domain/ les DTO (le contrat avec les clients)
├── database/tables/ les tables
└── domain/lampadaire/ Route, Service, DaoUn dossier par rôle : on sait où chercher.
@Serializable
data class CreateLampadaireDTO(val numeroSerie: String, val titre: String, …)
@Serializable
data class LampadaireDTO(val id: Int, …, val statut: StatutLampadaire)Ce que le client envoie, ce que le serveur renvoie.
Les données, on les range où ?
lampadaires.db.object Lampadaires : Table("lampadaires") {
val id = integer("id").autoIncrement()
val titre = varchar("titre", 100)
val statut = enumerationByName("statut", 20, StatutLampadaire::class)
}La table décrite en Kotlin. Comme les modèles d'Eloquent avec Laravel.
override fun getAll() = transaction {
Lampadaires.selectAll().map { it.toLampadaireDTO() }
}SELECT * FROM lampadairesfun create(dto: CreateLampadaireDTO): LampadaireDTO {
require(dto.numeroSerie.isNotBlank()) { "Le numéro de série est obligatoire" }
return lampadaireDao.create(dto, StatutLampadaire.EN_SERVICE, LocalDate.now().toString())
}Un lampadaire neuf est en service : c'est une règle métier.
post {
val dto = call.receive<CreateLampadaireDTO>()
call.respond(HttpStatusCode.Created, lampadaireService.create(dto))
}Le JSON reçu devient un objet Kotlin. Pas de SQL ici.
staticResources("/images", "images")Le serveur sert aussi des fichiers : l'API renvoie le chemin, le client télécharge la photo.
curl http://localhost:8080/v1/lampadairescurl -X POST http://localhost:8080/v1/lampadaires \
-H "Content-Type: application/json" \
-d '{"numeroSerie": "…", "titre": "…", "description": "…", "image": "…"}'Le serveur se teste avant d'écrire la moindre application.
Le même Ktor… en version client.
suspend fun lampadaires(): List<LampadaireDTO> =
client.get("$baseUrl/v1/lampadaires").body()Une fonction suspend : l'interface ne gèle pas.
Depuis l'émulateur Android, localhost, c'est qui ?
localhost : l'émulateur.10.0.2.2 : votre PC, vu depuis l'émulateur.Chargement ──→ Lampadaires (la liste)
└───────→ Erreur (le serveur ne répond pas)Une sealed interface, et un when dans l'écran. Comme dans le TP BLE.
AsyncImage(model = urlImage(lampadaire.image), …)Coil télécharge la photo à partir de son URL, sans bloquer l'écran.
static lampadaires() {
return fetch(`${API_URL}/v1/lampadaires`)
.then((response) => response.json())
.then((json) => json.map((l) => new LampadaireDTO(l)));
}La même API, les mêmes DTO et le même LampadairesApi, cette fois en JavaScript, avec Vue.js.
La page est sur localhost:5500, l'API sur localhost:8080.
Le navigateur va-t-il accepter ?
🚨 Dans la console du navigateur :
Access to fetch at 'http://localhost:8080/v1/lampadaires'
from origin 'http://localhost:5500' has been blocked by CORS policyLe navigateur protège l'utilisateur : c'est au serveur d'autoriser la page.
fun Application.installCors() {
install(CORS) {
allowHost("localhost:5500")
allowHeader(HttpHeaders.ContentType)
}
}Curl et Android ne sont pas concernés : CORS est une règle des navigateurs.
Place au TP 🚀