Branchement d’un module Turbo intégré de bout en bout avec React Native Codegen
Un module Turbo intégré pour React Native, du début à la fin : une spécification TypeScript typée, génération de code, puis du code pour iOS et Android avec synchronisation, Promise, callbacks et événements.
Sources: React Native Turbo Modules: A Guide to Calling Native Code in the New Architecture
L nouvelle architecture de React Native remplace le pont asynchrone par des Turbo Modules, que JavaScript appelle via JSI grâce à des bindings générés et typés. Cela permet de réduire la charge liée aux appels ainsi que d’appliquer des contrôles vérifiés par codegen plutôt que par convention. Cette étape vous montrera comment créer un module au sein d’une application (sans package npm) et abordera les trois styles d’appels que vous utiliserez en pratique : une valeur de retour synchrone, des résultats asynchrones via une Promise ou un callback, et un flux d’événements.
Rédaction de la spécification typée
Créez un dossier specs à la racine du projet pour y stocker les schémas des modules :
/your-app
/specs
Dans ce fichier, ajoutez NativeShowCase.ts. Codegen ne prend en compte que les fichiers de spécification dont le nom commence par Native. La spécification ci-dessous définit une méthode par style : haveCameraFlash renvoie de manière synchrone, toggleFlashLight renvoie une Promise, fetchUser accepte un callback, et evenListenerCalled est un émetteur d’événements transportant des paires clé/valeur. getEnforcing lance une exception au démarrage si la partie native est manquante, ce qui permet de détecter tôt d’éventuelles erreurs de connexion. Les émetteurs d’événements typés dans les spécifications sont une fonctionnalité récente, il convient donc de vérifier que votre version de React Native prend en charge CodegenTypes.EventEmitter.
import type { CodegenTypes, TurboModule } from "react-native";
import { TurboModuleRegistry } from "react-native";
export type KeyValuePair = {
key: string;
value: string;
};
export interface Spec extends TurboModule {
toggleFlashLight(): Promise<boolean>;
haveCameraFlash(): boolean;
fetchUser(
id: string,
callback: (user: { id: string; name: string }) => void
): void;
readonly evenListenerCalled: CodegenTypes.EventEmitter<KeyValuePair>;
}
export default TurboModuleRegistry.getEnforcing<Spec>("NativeShowCase");
Ce fichier constitue la seule source de vérité ; les bindings des deux plateformes sont générés à partir de lui.
Configuration et exécution de Codegen
Ajoutez un bloc codegenConfig dans package.json. jsSrcsDir doit correspondre à votre dossier specs, et javaPackageName détermine l’endroit où les classes Android seront générées :
"codegenConfig": {
"name": "NativeShowCase",
"type": "modules",
"jsSrcsDir": "specs",
"android": {
"javaPackageName": "com.nativeshowcase"
}
}
Ensuite, générez les artefacts. Sur Android, exécutez la tâche Gradle :
cd android && ./gradlew generateCodegenArtifactsFromSchema
Sur iOS, codegen s’exécute lors de l’installation des pods :
cd ios && pod install
Vous disposez maintenant d’interfaces, de en-têtes et de code de liaison générés pour les deux plateformes.
Mise en œuvre du module sur iOS
Ouvrez le .xcworkspace dans Xcode, créez un groupe NativeShowCase, et ajoutez une classe Objective-C nommée RCTNativeShowCase. Renommez le fichier .m en .mm : les Turbo Modules ont besoin d’Objective-C++ pour interagir avec la couche JSI en C++.
L’en-tête importe le module généré et hérite de la classe de base générée tout en adoptant le protocole généré :
#import <Foundation/Foundation.h>
#import <NativeShowCase/NativeShowCase.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTNativeShowCase : NativeShowCaseSpecBase<NativeShowCaseSpec>
@end
NS_ASSUME_NONNULL_END
L’implémentation renvoie le nom du module, transmet à React Native un objet JSI provenant de getTurboModule, et met en œuvre chaque méthode spécifiée. toggleFlashLight résout la Promise, fetchUser appelle la fonction de rappel, et haveCameraFlash renvoie immédiatement mais démarre un compte à rebours qui émet dix événements via la méthode générée emitEvenListenerCalled.
#import "RCTNativeShowCase.h"
@interface RCTNativeShowCase()
@property (nonatomic, strong) NSTimer *myTimer;
@property (nonatomic, assign) int count;
@end
@implementation RCTNativeShowCase
+ (NSString *)moduleName {
return @"NativeShowCase";
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
return std::make_shared<facebook::react::NativeShowCaseSpecJSI>(params);
}
- (void)toggleFlashLight:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
resolve(@(false));
}
- (nonnull NSNumber *)haveCameraFlash {
[self startTimer];
return [NSNumber numberWithBool:true];
}
- (void)fetchUser:(nonnull NSString *)userId callback:(nonnull RCTResponseSenderBlock)callback {
callback(@[@{@"key": userId, @"value": @"John"}]);
}
-(void)startTimer {
if(self.myTimer) {
[self stopTimer];
}
self.count = 0;
self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(updateCount)
userInfo:nil
repeats:YES];
}
- (void)updateCount {
self.count += 1;
[self emitEvenListenerCalled:@{@"key": @"count", @"value": @(_count)}];
if (self.count == 10) {
[self stopTimer];
}
}
- (void)stopTimer {
[self.myTimer invalidate];
self.myTimer = nil;
}
@end
Notez que cette version iOS de fetchUser envoie des champs key/value, tandis que les spécifications prévoient id et name ; alignez-les dans le code réel, car codegen ne valide pas la structure des données de la fonction de rappel en temps de exécution.
Mise en œuvre du module sur Android
Créer le package com.nativeshowcase déclaré dans codegenConfig. La classe du module étend la classe générée NativeShowCaseSpec et surcharge chaque méthode : elle active/désactive un drapeau et résout la Promise, exécute un CountDownTimer qui émet le nombre de secondes restantes à chaque tick, et crée un tableau pour la fonction de rappel.
package com.nativeshowcase
import android.os.CountDownTimer
import android.widget.Toast
import com.facebook.fbreact.specs.NativeShowCaseSpec
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Callback
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
class NativeShowCaseModule(reactContext: ReactApplicationContext) :
NativeShowCaseSpec(reactContext) {
var timer: CountDownTimer? = null
var lightOn = false
override fun toggleFlashLight(promise: Promise?) {
Toast.makeText(reactApplicationContext, "Let's turn on flash light", Toast.LENGTH_LONG)
.show()
lightOn = !lightOn
promise?.resolve(lightOn)
}
override fun haveCameraFlash(): Boolean {
if (timer != null) {
timer?.cancel()
timer = null
}
timer = object : CountDownTimer(10000, 1000) {
override fun onTick(millisUntilFinished: Long) {
val eventData = Arguments.createMap().apply {
putString("key", "count")
putInt("value", (millisUntilFinished / 1000).toInt())
}
emitEvenListenerCalled(eventData)
}
override fun onFinish() {
timer?.cancel()
timer = null
}
}
timer?.start()
return true
}
override fun fetchUser(id: String?, callback: Callback?) {
val eventData = Arguments.createMap().apply {
putString("id", id)
putString("name", "John")
}
callback.let { it?.invoke(eventData) }
}
companion object {
const val NAME = "NativeShowCase"
}
override fun getName() = NAME
}
Une classe de package basée sur BaseReactPackage indique à React Native comment instancier le module. Le drapeau isTurboModule = true présent dans son ReactModuleInfo est ce qui le fait passer par le nouveau système.
package com.nativeshowcase
import com.facebook.react.BaseReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
class NativeShowCasePackage : BaseReactPackage() {
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
if (name == NativeShowCaseModule.NAME) return NativeShowCaseModule(reactContext)
return null
}
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
mapOf(
NativeShowCaseModule.NAME to ReactModuleInfo(
name = NativeShowCaseModule.NAME,
className = NativeShowCaseModule.NAME,
canOverrideExistingModule = false,
needsEagerInit = true,
isCxxModule = false,
isTurboModule = true
)
)
}
}
Finalement, enregistrer le package dans MainApplication.kt en l’ajoutant à la liste des packages auto-liés :
class MainApplication : Application(), ReactApplication {
override val reactHost: ReactHost by lazy {
getDefaultReactHost(
context = applicationContext,
packageList =
PackageList(this).packages.apply {
add(NativeShowCasePackage())
},
)
}
override fun onCreate() {
super.onCreate()
loadReactNative(this)
}
}
Appel du module depuis React
Importez l’export par défaut de la spécification et appelez-le comme n’importe quel objet. L’effet s’abonne au générateur d’événements et retourne une fonction de nettoyage qui supprime l’écouteur en cas de désinstallation ; un bouton appelle la méthode synchrone, l’autre utilise les méthodes Promise et callback.
import { StatusBar, StyleSheet, useColorScheme, View, Button } from 'react-native';
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
import NativeShowCase from './specs/NativeShowCase';
import { useEffect } from 'react';
function App() {
const isDarkMode = useColorScheme() === 'dark';
return (
<SafeAreaProvider>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<AppContent />
</SafeAreaProvider>
);
}
function AppContent() {
useEffect(() => {
const listener = NativeShowCase.evenListenerCalled(pair => {
console.log('Event received:', pair);
});
return () => listener.remove();
}, []);
return (
<View style={styles.container}>
<Button
title="Have Camera Flash?"
onPress={() => {
console.log('Have Camera Flash:', NativeShowCase.haveCameraFlash());
}}
/>
<Button
title="Toggle Flashlight"
onPress={() => {
NativeShowCase.toggleFlashLight()
.then(isOn => console.log('Flashlight:', isOn))
.catch(err => console.error(err));
NativeShowCase.fetchUser('123', user => {
console.log('Fetched user:', user);
});
}}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
});
export default App;
Utilisez les méthodes synchrones avec modération : elles bloquent le thread JavaScript jusqu’au retour de la fonction native, donc réservez-les aux recherches peu coûteuses. Pour une approche alternative de liaison, consultez comment Nitro Modules se compare à Turbo Modules.
Points clés
- La spécification TypeScript constitue le contrat ; codegen en dérive des interfaces natives pour les deux plateformes.
- Nommez les fichiers de spécification avec le préfixe
Nativeet maintenezcodegenConfigen synchronisation avec les dossiers et les packages.
.mm ; les paquets Android doivent marquer les modules avec isTurboModule.