Inicio / Artículos / Conexión end to end de un módulo Turbo dentro de la aplicación con React Native Codegen

Conexión end to end de un módulo Turbo dentro de la aplicación con React Native Codegen

Define una especificación tipada, ejecuta codegen e implementa un módulo Turbo en iOS y Android con métodos síncronos, Promise, callback y emisor de eventos.

1409 palabras

La nueva arquitectura de React Native reemplaza el puente asíncrono por Turbo Modules, que JavaScript llama a través de JSI mediante enlaces generados y tipados. El resultado es una menor carga por llamadas y un contrato verificado mediante codegen en lugar de por convención. Esta guía crea un módulo dentro de una aplicación (sin paquete npm) y aborda los tres estilos de llamadas que utilizará en la práctica: un valor de retorno síncrono, resultados asíncronos a través de una Promise o callback, y un flujo de eventos.

Escribiendo la especificación tipada

Cree una carpeta specs en la raíz del proyecto para almacenar los esquemas de los módulos:

/your-app
  /specs

Dentro de él, agregue NativeShowCase.ts. Codegen solo detecta los archivos de especificación cuyos nombres comienzan con Native. La especificación a continuación declara un método por estilo: haveCameraFlash devuelve de forma síncrona, toggleFlashLight devuelve una Promise, fetchUser acepta un callback, y evenListenerCalled es un emisor de eventos que transporta pares clave/valor. getEnforcing lanza un error al iniciar si falta el componente nativo, lo que permite detectar errores de conexión temprano. Los emisores de eventos tipados en las especificaciones son una adición reciente, así que verifique si su versión de React Native es compatible con 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");

Este archivo es la única fuente de verdad; las vinculaciones de ambas plataformas se generan a partir de él.

Configuración y ejecución de codegen

Agregue un bloque codegenConfig a package.json. jsSrcsDir debe coincidir con la carpeta de especificaciones, y javaPackageName determina dónde se generarán las clases para Android:

"codegenConfig": {
  "name": "NativeShowCase",
  "type": "modules",
  "jsSrcsDir": "specs",
  "android": {
    "javaPackageName": "com.nativeshowcase"
  }
}

Luego genere los artefactos. En Android, ejecute la tarea Gradle:

cd android && ./gradlew generateCodegenArtifactsFromSchema

En iOS, codegen se ejecuta como parte de la instalación de pods:

cd ios && pod install

Ahora cuenta con interfaces, encabezados y código de enlace generados para ambas plataformas.

Implementación del módulo en iOS

Abra el .xcworkspace en Xcode, cree un grupo NativeShowCase y agregue una clase Objective-C llamada RCTNativeShowCase. Cambie el nombre del archivo .m a .mm: Los Turbo Modules necesitan Objective-C++ para interoperar con la capa JSI de C++.

El encabezado importa el módulo generado y hereda de la clase base generada, al tiempo que adopta el protocolo generado:

#import <Foundation/Foundation.h>
#import <NativeShowCase/NativeShowCase.h>
NS_ASSUME_NONNULL_BEGIN

@interface RCTNativeShowCase : NativeShowCaseSpecBase<NativeShowCaseSpec>

@end

NS_ASSUME_NONNULL_END

La implementación devuelve el nombre del módulo, entrega a React Native un objeto JSI proveniente de getTurboModule, e implementa cada método especificado. toggleFlashLight resuelve la Promise, fetchUser invoca la función de callback, y haveCameraFlash devuelve inmediatamente pero inicia un temporizador que emite diez eventos a través del método generado 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

Tenga en cuenta que esta versión para iOS de fetchUser envía campos key/value, mientras que las especificaciones prometen id y name; debe alinearlos en el código real, ya que codegen no valida la estructura de los datos de las funciones de callback en tiempo de ejecución.

Implementación del módulo en Android

Cree el paquete com.nativeshowcase declarado en codegenConfig. La clase del módulo extiende la clase generada NativeShowCaseSpec y sobrescribe cada método: activa o desactiva una bandera y resuelve la Promise, ejecuta un CountDownTimer que emite los segundos restantes en cada actualización, y crea un mapa para la función de callback.

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
}

Una clase de paquete basada en BaseReactPackage indica a React Native cómo instanciar el módulo. La bandera isTurboModule = true en su ReactModuleInfo es lo que hace que se procese a través del nuevo sistema.

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
            )
        )
    }
}

Finalmente, registre el paquete en MainApplication.kt añadiéndolo a la lista de paquetes con enlace automático:

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)
    }
}

Llamar al módulo desde React

Importe la exportación predeterminada de la especificación y úsela como cualquier otro objeto. El efecto se suscribe al emisor de eventos y devuelve una función de limpieza que elimina el oyente al desmontarse; un botón llama al método síncrono, mientras que el otro utiliza los métodos Promise y 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;

Use los métodos síncronos con moderación: bloquean el hilo de JavaScript hasta que la función nativa devuelve un resultado, así que guárdelos para búsquedas simples. Para conocer otro enfoque de vinculación, consulte cómo Nitro Modules se comparan con Turbo Modules.

Puntos clave

  • La especificación de TypeScript es el contrato; codegen deriva las interfaces nativas para ambas plataformas a partir de ella.
  • Nombré los archivos de especificación con el prefijo Native y mantenga codegenConfig sincronizado con las carpetas y paquetes.
  • Las implementaciones para iOS necesitan archivos .mm; los paquetes de Android deben marcar los módulos con isTurboModule.
  • Elija deliberadamente el estilo de llamada: sincrónico para lecturas sencillas, Promises o callbacks para tareas complejas, y emisores para flujos de datos.
  • Lecturas relacionadas