Chapitre 15.3 — Modules natifs custom : l’Expo Modules API
Où on en est : tout le cours a consommé du natif écrit par d’autres (expo-*, libs). Dernier verrou à faire sauter, au moins conceptuellement : écrire le SIEN — pour le jour où aucun module n’existe pour votre besoin.
⏱️ TL;DR — L’Expo Modules API rend l’exercice abordable :
npx create-expo-module --localgénère un module dansmodules/, le Kotlin s’écrit dans une DSL déclarative (Function,AsyncFunction,Events), le TS est typé en face, et le build l’intègre automatiquement (CNG). On l’illustre par un vrai cas Drill-compatible : lire le volume média du téléphone (prévenir « ton son est coupé » avant une partie). Périmètre honnête : savoir le faire UNE fois, pour ne plus jamais être bloqué — pas devenir dev Android.
🎯 Objectifs
- Savoir quand un module custom est LA solution (l’arbre de décision complet).
- Générer un module local et lire sa structure (le Kotlin déclaratif).
- Écrire une fonction native simple, l’appeler du TS typé.
- Connaître le chemin de la publication (le module qui sert à d’autres).
Quand ? L’arbre de décision final
Le ch. 11.4 avait posé l’escalade — la voici complète, avec le nouveau barreau :
Des exemples RÉELS du dernier barreau : un widget d’écran d’accueil, lire un réglage système non exposé (notre volume), parler à un SDK constructeur (imprimante, TPE), une lib Android existante sans binding RN.
Générer et lire
npx create-expo-module@latest --local volume-info
# → modules/volume-info/ : android/ (Kotlin), src/ (TS), expo-module.config.jsonLe module local vit DANS le projet (pas de publication npm nécessaire) et s’intègre au build automatiquement — dev build à rebuilder, comme tout changement natif (ch. 11.1, le réflexe tient toujours).
Le cœur Kotlin, dans la DSL Expo :
// modules/volume-info/android/src/main/java/expo/modules/volumeinfo/VolumeInfoModule.kt
package expo.modules.volumeinfo
import android.content.Context
import android.media.AudioManager
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
class VolumeInfoModule : Module() {
override fun definition() = ModuleDefinition {
Name("VolumeInfo")
Function("getMediaVolume") {
val audio = appContext.reactContext?.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val current = audio.getStreamVolume(AudioManager.STREAM_MUSIC)
val max = audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
current.toDouble() / max
}
}
}Lisez la forme plus que le fond : ModuleDefinition déclare un nom et des fonctions —
Function (synchrone, via JSI !), AsyncFunction (les Promises), Events (émettre vers le
JS), View (même des composants natifs). La conversion des types Kotlin↔JS est automatique.
C’est la même API avec laquelle sont écrits les modules expo-* officiels — en l’apprenant,
vous lisez aussi LEURS sources.
Côté TS, le miroir typé :
// modules/volume-info/src/index.ts
import VolumeInfoModule from './VolumeInfoModule'
export function getMediaVolume(): number {
return VolumeInfoModule.getMediaVolume()
}// Et dans Drill — l'usage produit :
import { getMediaVolume } from '../modules/volume-info'
// Au lancement d'une partie, si le son est activé dans les réglages :
if (soundEnabled && getMediaVolume() < 0.05) {
// petit toast : « 🔇 Ton volume média est coupé — les sons de jeu sont muets »
}Un vrai micro-besoin, insoluble sans natif, résolu en ~25 lignes de Kotlin déclaratif.
🍏 Côté iOS : le même module accueille un fichier Swift jumeau (même DSL) — le jour du
portage (ch. 15.1), la structure est déjà bi-plateforme.
Publier — le module qui sert à d’autres
Si le besoin dépasse votre projet : create-expo-module SANS --local génère un package
publiable (npm) avec exemple, config plugin éventuel et CI. Le circuit : repo GitHub →
npm publish → référencement sur reactnative.directory. C’est exactement ainsi que
l’écosystème des libs du ch. 1.4 s’est construit — et contribuer un module bien fait reste
l’une des meilleures cartes de visite d’un dev RN.
⚠️ Piège — Le module custom est un engagement de MAINTENANCE : chaque montée de SDK Expo peut toucher l’API des modules (rarement, mais ça arrive), et VOUS êtes le mainteneur. D’où l’arbre de décision : on n’écrit du natif custom qu’en DERNIER recours, on le garde MINUSCULE (une capacité = un module), et on re-vérifie à chaque montée de SDK si un module officiel n’est pas apparu entre-temps (le catalogue grandit chaque année — votre module a vocation à mourir remplacé).
💡 Pour un dev Next.js — C’est votre « écrire un addon natif Node en Rust/C++ » : possible, encadré, rarement nécessaire — mais SAVOIR que c’est accessible change la psychologie. Plus aucune capacité du téléphone n’est « hors de portée » : au pire, c’est 30 lignes de Kotlin déclaratif au bout d’un arbre de décision. Le plafond de verre du dev JS mobile vient de sauter.
✏️ Exercices
1. Générez le module volume-info en local, implémentez getMediaVolume, rebuild du dev build, et branchez le toast « volume coupé » au lancement d’une partie.
✅ Solution
Suivre le chapitre ; les vérifications : le module apparaît dans le build (log gradle),
getMediaVolume() retourne 0-1 cohérent avec les boutons de volume, le toast n’apparaît
que si soundEnabled && volume < 5 %. Bonus propreté : envelopper dans lib/feedback.ts
(le module sensoriel — ch. 9.3) plutôt que d’appeler le module depuis l’écran : la
frontière reste nette.
2. Ajoutez la fonction jumelle AsyncFunction("setMediaVolume") — et découvrez pourquoi
elle est plus délicate qu’il n’y paraît (indice : la politique Android).
✅ Solution
audio.setStreamVolume(STREAM_MUSIC, (v * max).toInt(), 0) fonctionne… sauf en mode Ne pas
déranger, où Android peut exiger une permission d’accès DND — l’exception
SecurityException à attraper. Leçon double : (1) AsyncFunction pour tout ce qui peut
échouer (la Promise porte l’erreur au JS proprement), (2) MODIFIER l’état système est
toujours plus réglementé que le LIRE — le natif custom hérite des politiques Android,
il ne les contourne pas.
3. Cherchez sur reactnative.directory et npm : le module volume existe-t-il déjà ? Refaites l’arbre de décision honnêtement, et concluez sur la place du module custom.
✅ Solution
Des libs volume existent (expo-av exposait des bribes, des libs communautaires aussi — qualité variable, New Arch à vérifier). L’arbre honnête : pour le seul getMediaVolume, une lib maintenue ferait l’affaire SI elle passe les critères du ch. 1.4 ; notre module local de 25 lignes gagne sur : zéro dépendance, périmètre exact, maintenance triviale. Les DEUX conclusions sont défendables — l’important est d’avoir DÉROULÉ l’arbre au lieu de coder par réflexe (dans un sens ou dans l’autre).
🧠 Quiz
1. Récitez l’arbre de décision complet d’un besoin natif.
Réponse
Module expo-* du SDK → lib communautaire saine (directory, New Arch) → config plugin (si c’est de la CONFIG, pas du code) → Expo Modules API en dernier recours (local d’abord, publié si utile à d’autres).
2. Que génère create-expo-module —local ?
Réponse
Un module DANS le projet (modules/…) : Kotlin (DSL ModuleDefinition), TS typé en face,
intégré automatiquement au build (CNG) — rebuild du dev build requis.
3. Function vs AsyncFunction ?
Réponse
Function : appel synchrone (JSI — pour les lectures rapides). AsyncFunction : Promise
côté JS — pour tout ce qui est long OU peut échouer (l’erreur voyage proprement).
4. Pourquoi garder un module custom minuscule ?
Réponse
C’est un engagement de maintenance (montées de SDK) : une capacité = un module, et il a vocation à mourir remplacé par un module officiel/communautaire quand il apparaît.
5. Quel bénéfice psychologique ce chapitre verrouille-t-il ?
Réponse
Plus de plafond de verre : AUCUNE capacité du téléphone n’est hors de portée d’un dev JS — au pire, 30 lignes de Kotlin déclaratif après un arbre de décision honnête.
👉 Chapitre suivant : 15.4 — Le futur & la veille — suivre l’écosystème sans s’y noyer.