Concepto clave
La integración nativa en Expo permite acceder a funcionalidades del dispositivo que no están disponibles en JavaScript puro, como sensores, hardware específico o APIs del sistema operativo. Imagina que tu aplicación React Native es como un coche con un motor JavaScript: la integración nativa es el sistema de transmisión que conecta ese motor con las ruedas (el hardware real del dispositivo). Sin esta conexión, por más potente que sea tu motor, no podrás mover las ruedas.
Expo maneja esta integración a través de dos enfoques principales: APIs nativas preconstruidas (como Camera, Location o Notifications) y módulos personalizados que tú desarrollas cuando necesitas funcionalidades específicas no cubiertas por Expo. La clave está en entender que JavaScript y el código nativo (Java/Kotlin para Android, Objective-C/Swift para iOS) se comunican a través de un puente (bridge) que serializa y deserializa datos de forma asíncrona.
Cómo funciona en la práctica
Vamos a crear un módulo personalizado paso a paso. Supongamos que necesitas acceder al sensor de proximidad del dispositivo para detectar cuando un objeto se acerca a la pantalla (útil para aplicaciones de llamadas que apagan la pantalla al acercar el teléfono al oído).
- Primero, creas la interfaz nativa en cada plataforma. Para Android en Java, defines una clase que extiende ReactContextBaseJavaModule y registra métodos con @ReactMethod.
- Luego, creas un paquete ReactPackage que registra tu módulo.
- En iOS, haces lo equivalente en Objective-C o Swift con RCT_EXPORT_METHOD.
- Finalmente, en JavaScript, importas tu módulo con NativeModules y lo usas como cualquier otra API.
El flujo completo es: llamada JavaScript → puente → código nativo → ejecución en dispositivo → respuesta a través del puente → callback en JavaScript.
Código en acción
Aquí tienes un ejemplo funcional de un módulo personalizado para sensor de proximidad:
// En tu archivo JavaScript (ProximitySensor.js)
import { NativeModules } from 'react-native';
const { ProximityModule } = NativeModules;
export default class ProximitySensor {
static async isNear() {
try {
const isNear = await ProximityModule.isNear();
return isNear;
} catch (error) {
console.error('Error accediendo al sensor:', error);
return false;
}
}
static startListening(callback) {
ProximityModule.startListening(callback);
}
static stopListening() {
ProximityModule.stopListening();
}
}Y el código nativo para Android (antes de optimizar):
// ProximityModule.java (versión inicial)
package com.yourapp;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Callback;
public class ProximityModule extends ReactContextBaseJavaModule {
private SensorManager sensorManager;
private Sensor proximitySensor;
private Callback jsCallback;
public ProximityModule(ReactApplicationContext reactContext) {
super(reactContext);
sensorManager = (SensorManager) reactContext.getSystemService(Context.SENSOR_SERVICE);
proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
}
@Override
public String getName() {
return "ProximityModule";
}
@ReactMethod
public void isNear(Callback callback) {
// Implementación básica
sensorManager.registerListener(new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
float distance = event.values[0];
callback.invoke(distance < proximitySensor.getMaximumRange());
sensorManager.unregisterListener(this);
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
}, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
}
}Después de refactorizar para mejor rendimiento:
// ProximityModule.java (versión optimizada)
public class ProximityModule extends ReactContextBaseJavaModule {
private SensorManager sensorManager;
private Sensor proximitySensor;
private SensorEventListener listener;
private Callback jsCallback;
public ProximityModule(ReactApplicationContext reactContext) {
super(reactContext);
sensorManager = (SensorManager) reactContext.getSystemService(Context.SENSOR_SERVICE);
proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
}
@ReactMethod
public void isNear(Promise promise) {
// Usando Promise en lugar de Callback para mejor manejo de errores
if (proximitySensor == null) {
promise.reject("NO_SENSOR", "Dispositivo no tiene sensor de proximidad");
return;
}
listener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
float distance = event.values[0];
boolean isNear = distance < proximitySensor.getMaximumRange();
sensorManager.unregisterListener(this);
promise.resolve(isNear);
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
};
sensorManager.registerListener(listener, proximitySensor, SensorManager.SENSOR_DELAY_FASTEST);
}
@ReactMethod
public void startListening(Callback callback) {
this.jsCallback = callback;
if (listener == null) {
listener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
float distance = event.values[0];
boolean isNear = distance < proximitySensor.getMaximumRange();
WritableMap eventData = Arguments.createMap();
eventData.putBoolean("isNear", isNear);
eventData.putDouble("distance", distance);
sendEvent("proximityChange", eventData);
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
};
}
sensorManager.registerListener(listener, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
}
private void sendEvent(String eventName, WritableMap params) {
getReactApplicationContext()
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}
}Errores comunes
- No manejar la ausencia del hardware: Asumir que todos los dispositivos tienen el sensor o API que necesitas. Siempre verifica la disponibilidad y proporciona fallbacks.
- Bloquear el hilo principal: Ejecutar operaciones pesadas en el hilo de UI nativo. Usa hilos secundarios o trabajadores para operaciones largas.
- Fugas de memoria en listeners: Registrar event listeners sin desregistrarlos cuando el componente se desmonta. Implementa cleanup en onCatalystInstanceDestroy.
- Serialización incorrecta de datos: Intentar pasar objetos complejos a través del puente. Solo se permiten tipos básicos: boolean, number, string, array, map.
- Olvidar configurar permisos: APIs como cámara o ubicación requieren permisos en AndroidManifest.xml e Info.plist, y solicitudes en tiempo de ejecución.
Checklist de dominio
- Puedo crear un módulo nativo personalizado que exponga al menos 3 métodos a JavaScript
- Sé configurar correctamente los permisos necesarios para APIs nativas en ambas plataformas
- He implementado manejo de errores robusto tanto en código nativo como en JavaScript
- Puedo optimizar la comunicación entre JavaScript y nativo para minimizar latencia
- Entiendo cómo depurar problemas en el puente usando herramientas como Flipper
- Sé cuándo usar APIs preconstruidas de Expo vs. desarrollar módulos personalizados
- He probado mi integración nativa en dispositivos físicos con diferentes versiones de OS
Crear un módulo nativo para control de brillo de pantalla
Desarrolla un módulo nativo personalizado que permita ajustar el brillo de la pantalla desde tu aplicación Expo. Este ejercicio simula un caso real donde necesitas funcionalidad específica no cubierta por las APIs estándar.
- Configuración inicial: Crea un proyecto Expo con desarrollo desnudo (bare workflow) o modifica uno existente. Asegúrate de tener las herramientas de desarrollo nativo instaladas (Android Studio, Xcode).
- Implementación Android:
- Crea una clase Java BrightnessModule que extienda ReactContextBaseJavaModule
- Implementa dos métodos: setBrightness(float brightness) y getBrightness()
- Usa WindowManager.LayoutParams para ajustar el brillo (valores de 0.0 a 1.0)
- Registra tu módulo en un ReactPackage personalizado
- Implementación iOS:
- Crea un archivo BrightnessModule.m (Objective-C) o .swift
- Implementa los mismos métodos usando UIScreen.main.brightness
- Exporta los métodos con RCT_EXPORT_METHOD
- Capa JavaScript:
- Crea un archivo BrightnessController.js que importe NativeModules
- Expón una API limpia con métodos setBrightness() y getBrightness()
- Añade validación de rangos (0-1) y manejo de errores
- Pruebas:
- Crea una pantalla de prueba con un Slider para ajustar brillo
- Muestra el valor actual del brillo en tiempo real
- Prueba en dispositivo físico Android e iOS
- Optimización:
- Añade un método resetToSystemBrightness() que restaure la configuración original
- Implementa eventos nativos para detectar cambios de brillo del sistema
- Documenta tu módulo con TypeScript definitions
- Recuerda que en Android necesitas el permiso WRITE_SETTINGS para modificar brillo del sistema
- En iOS, UIScreen.main.brightness solo funciona dentro del rango 0.0 a 1.0, valores fuera causarán excepciones
- Considera usar eventos nativos (DeviceEventEmitter) para notificar cambios de brillo iniciados por el sistema
Evalua tu comprension
Completa el quiz interactivo de arriba para ganar XP.