Accueil / Articles / Branchement d’un module Turbo intégré de bout en bout avec React Native Codegen

Branchement d’un module Turbo intégré de bout en bout avec React Native Codegen

Définir une spécification typée, exécuter le codegen, et implémenter un module Turbo sur iOS et Android en utilisant des méthodes synchrones, Promise, callback ainsi que des émetteurs d’événements.

1409 mots

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 Native et maintenez codegenConfig en synchronisation avec les dossiers et les packages.
  • Les implémentations iOS nécessitent des fichiers .mm ; les paquets Android doivent marquer les modules avec isTurboModule.
  • Choisissez délibérément le style d’appel : en mode synchrone pour des lectures simples, avec des Promises ou des callbacks pour des tâches plus complexes, et des émetteurs pour les flux de données.
  • Lectures complémentaires