Skip to Content
React Native15. iOS & au-delà15.3 Modules natifs custom

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 --local génère un module dans modules/, 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.json

Le 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.