← Regresar a lecciones
  • permissions

  • swift

  • native-modules

  • biometrics

  • react native

  • bridging

  • turbomodules

  • camera

  • kotlin

  • jsi

  • React Native CLI

APIs Nativas en React Native: Del JavaScript al Metal del Dispositivo

¿Qué es "hacer bridging"?
El Sistema de Permisos

Si vienes de desarrollo nativo mobile, ya sabes que el verdadero poder de una app está en acceder al hardware: cámara, sensores, biometría, almacenamiento seguro. En React Native, esto funciona mediante bridging: conectar tu JavaScript con código nativo del dispositivo.

¿Qué es "hacer bridging"?

Cuando escuches "bridging" o "crear un bridge", realmente estamos hablando de crear un Native Module: un pedazo de código nativo (Kotlin/Swift) que expones a JavaScript. No estás creando el bridge en sí - ese mecanismo de comunicación ya existe en React Native. Lo que haces es:

  1. Escribir código en Kotlin (Android) o Swift (iOS)
  2. "Exponerlo" a JavaScript con decoradores especiales
  3. React Native se encarga de la comunicación por ti

Dos formas de hacer bridging:

Forma¿Qué usa?Estado
Native Modules (tradicional)Bridge (JSON)Funciona, pero más lento
TurboModules (nuevo)JSI (directo)Más rápido, carga bajo demanda

Ambos logran lo mismo, conectar JS con código nativo. La diferencia es el mecanismo interno de comunicación.

En este articulo verás cómo usar las APIs del dispositivo y, cuando sea necesario, crear tus propios bridges (módulos nativos). Piensa en esto como el equivalente a cuando en Android escribes código Kotlin que interactúa con el sistema, o en iOS cuando usas Swift para acceder a frameworks nativos.

El Sistema de Permisos

En mobile, cualquier acceso a recursos sensibles necesita permisos explícitos del usuario. No puedes simplemente acceder a la cámara sin preguntar primero. React Native maneja esto similar a cómo lo haces en Android o iOS, pero con una API unificada. Vamos a usar react-native-permissions, la librería estándar para esto:

1npm install react-native-permissions 2cd ios && pod install

Configura los permisos en iOS

Abre el archivo ios/TuApp/Info.plist (donde "TuApp" es el nombre de tu proyecto) y agrega las descripciones que verá el usuario cuando pidas permisos:

1<key>NSCameraUsageDescription</key> 2<string>Necesitamos acceso a tu cámara para tomar fotos</string> 3 4<key>NSPhotoLibraryUsageDescription</key> 5<string>Necesitamos acceso a tus fotos para seleccionar imágenes</string> 6 7<key>NSMicrophoneUsageDescription</key> 8<string>Necesitamos acceso al micrófono para grabar audio</string> 9 10<key>NSLocationWhenInUseUsageDescription</key> 11<string>Necesitamos tu ubicación para mostrarte contenido cercano</string> 12 13<key>NSFaceIDUsageDescription</key> 14<string>Usa Face ID para acceder de forma segura</string>

Configura los permisos en Android

Abre el archivo android/app/src/main/AndroidManifest.xml y declara los permisos que necesitarás:

1<manifest> 2 <uses-permission android:name="android.permission.CAMERA" /> 3 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> 4 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> 5 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> 6 <uses-permission android:name="android.permission.RECORD_AUDIO" /> 7 <uses-permission android:name="android.permission.USE_BIOMETRIC" /> 8</manifest>

Solicita permisos en runtime

Ahora viene la parte interesante. Pedirle al usuario permiso en el momento adecuado. Crea un archivo src/utils/permissions.js (o donde tengas tus utilidades):

1import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions'; 2import { Platform } from 'react-native'; 3 4async function requestCameraPermission() { 5 // Selecciona el permiso según la plataforma 6 const permission = Platform.select({ 7 ios: PERMISSIONS.IOS.CAMERA, 8 android: PERMISSIONS.ANDROID.CAMERA, 9 }); 10 11 // Primero verifica el estado actual 12 const result = await check(permission); 13 14 switch (result) { 15 case RESULTS.UNAVAILABLE: 16 console.log('Esta funcionalidad no está disponible en este dispositivo'); 17 return false; 18 19 case RESULTS.DENIED: 20 console.log('Permiso denegado, vamos a pedirlo...'); 21 const requestResult = await request(permission); 22 return requestResult === RESULTS.GRANTED; 23 24 case RESULTS.LIMITED: 25 console.log('Permiso limitado (solo iOS 14+)'); 26 return true; 27 28 case RESULTS.GRANTED: 29 console.log('Ya tienes el permiso, todo bien'); 30 return true; 31 32 case RESULTS.BLOCKED: 33 console.log('El permiso está bloqueado, el usuario debe ir a Ajustes'); 34 // Aquí podrías mostrar un diálogo para ir a Settings 35 return false; 36 } 37}

Si vienes de nativo, esto te sonará familiar:

En Kotlin/Android:

1if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) 2 != PackageManager.PERMISSION_GRANTED) { 3 ActivityCompat.requestPermissions(activity, 4 arrayOf(Manifest.permission.CAMERA), REQUEST_CODE) 5}

En Swift/iOS:

1AVCaptureDevice.requestAccess(for: .video) { granted in 2 if granted { 3 // Permiso concedido 4 } 5}

La abstracción es genial: una sola API funciona en ambas plataformas.

Accede a la Cámara

Vamos a usar react-native-vision-camera, que es la librería más moderna y performante para trabajar con la cámara. Olvídate del viejo react-native-camera, Vision Camera es el nuevo estándar.

Instálala

1npm install react-native-vision-camera 2cd ios && pod install

Configura los permisos

Ya sabes el drill. Primero los textos que verá el usuario:

iOS: En tu ios/TuApp/Info.plist:

1<key>NSCameraUsageDescription</key> 2<string>Necesitamos la cámara para tomar fotos</string>

Android: En tu android/app/src/main/AndroidManifest.xml:

1<uses-permission android:name="android.permission.CAMERA" /> 2<uses-feature android:name="android.hardware.camera" android:required="false" />

Crea tu componente de cámara

Ahora viene lo bueno. Vamos a hacer una cámara funcional en menos de 100 líneas. Crea el archivo src/screens/CameraScreen.js (o .tsx si usas TypeScript):

1import React, { useRef, useState } from 'react'; 2import { View, TouchableOpacity, Text, StyleSheet, Image } from 'react-native'; 3import { Camera, useCameraDevice, useCameraPermission } from 'react-native-vision-camera'; 4 5export default function CameraScreen() { 6 const camera = useRef(null); 7 const device = useCameraDevice('back'); 8 const { hasPermission, requestPermission } = useCameraPermission(); 9 const [photo, setPhoto] = useState(null); 10 11 // Solicitar permiso si no lo tenemos 12 if (!hasPermission) { 13 return ( 14 <View style={styles.container}> 15 <Text>Necesitamos acceso a tu cámara</Text> 16 <TouchableOpacity style={styles.button} onPress={requestPermission}> 17 <Text style={styles.buttonText}>Conceder Permiso</Text> 18 </TouchableOpacity> 19 </View> 20 ); 21 } 22 23 // Verificar que hay una cámara disponible 24 if (!device) { 25 return ( 26 <View style={styles.container}> 27 <Text>No se encontró cámara</Text> 28 </View> 29 ); 30 } 31 32 const takePhoto = async () => { 33 if (camera.current) { 34 const photo = await camera.current.takePhoto({ 35 qualityPrioritization: 'balanced', 36 flash: 'auto', 37 }); 38 39 setPhoto(photo); 40 console.log('Foto tomada:', photo.path); 41 } 42 }; 43 44 // Si ya tomamos una foto, mostrarla 45 if (photo) { 46 return ( 47 <View style={styles.container}> 48 <Image 49 source={{ uri: `file://${photo.path}` }} 50 style={styles.preview} 51 /> 52 <TouchableOpacity 53 style={styles.button} 54 onPress={() => setPhoto(null)} 55 > 56 <Text style={styles.buttonText}>Tomar otra</Text> 57 </TouchableOpacity> 58 </View> 59 ); 60 } 61 62 return ( 63 <View style={styles.container}> 64 <Camera 65 ref={camera} 66 style={StyleSheet.absoluteFill} 67 device={device} 68 isActive={true} 69 photo={true} 70 /> 71 72 <View style={styles.controls}> 73 <TouchableOpacity style={styles.captureButton} onPress={takePhoto}> 74 <View style={styles.captureButtonInner} /> 75 </TouchableOpacity> 76 </View> 77 </View> 78 ); 79} 80 81const styles = StyleSheet.create({ 82 container: { 83 flex: 1, 84 justifyContent: 'center', 85 alignItems: 'center', 86 backgroundColor: 'black', 87 }, 88 controls: { 89 position: 'absolute', 90 bottom: 50, 91 alignSelf: 'center', 92 }, 93 captureButton: { 94 width: 70, 95 height: 70, 96 borderRadius: 35, 97 backgroundColor: 'white', 98 justifyContent: 'center', 99 alignItems: 'center', 100 }, 101 captureButtonInner: { 102 width: 60, 103 height: 60, 104 borderRadius: 30, 105 backgroundColor: 'white', 106 borderWidth: 2, 107 borderColor: 'black', 108 }, 109 preview: { 110 width: '100%', 111 height: '80%', 112 }, 113 button: { 114 backgroundColor: '#2196F3', 115 padding: 15, 116 borderRadius: 8, 117 marginTop: 20, 118 }, 119 buttonText: { 120 color: 'white', 121 fontSize: 16, 122 fontWeight: '600', 123 }, 124});

Lo que está pasando aquí:

  1. useCameraPermission() - Maneja el estado del permiso automáticamente
  2. useCameraDevice('back') - Selecciona la cámara trasera (puedes usar 'front' también)
  3. camera.current - Referencia para controlar la cámara programáticamente
  4. takePhoto() - Retorna una promesa con la ruta del archivo donde se guardó la foto

Si usaste AVFoundation en iOS o Camera2 API en Android, esto te resultará muy natural. La diferencia es que aquí escribes una vez y funciona en ambas plataformas.

Autenticación Biométrica

Face ID, Touch ID, huella digital... la biometría es estándar hoy en día. Vamos a implementarla.

Instala la librería

1npm install react-native-biometrics 2cd ios && pod install

Verifica qué hay disponible

Primero necesitas saber si el dispositivo soporta biometría y qué tipo. Crea un archivo src/utils/biometrics.js:

1import ReactNativeBiometrics from 'react-native-biometrics'; 2 3async function checkBiometrics() { 4 const rnBiometrics = new ReactNativeBiometrics(); 5 6 const { available, biometryType } = await rnBiometrics.isSensorAvailable(); 7 8 if (available) { 9 switch (biometryType) { 10 case 'FaceID': 11 console.log('Face ID disponible'); 12 break; 13 case 'TouchID': 14 console.log('Touch ID disponible'); 15 break; 16 case 'Biometrics': 17 console.log('Biometría disponible (Android)'); 18 break; 19 } 20 } else { 21 console.log('Biometría no disponible'); 22 } 23 24 return available; 25}

Autentica al usuario

Ahora sí, vamos a pedirle al usuario que se autentique. Crea src/screens/BiometricLoginScreen.js:

1import React, { useState } from 'react'; 2import { View, TouchableOpacity, Text, Alert, StyleSheet } from 'react-native'; 3import ReactNativeBiometrics from 'react-native-biometrics'; 4 5export default function BiometricLogin() { 6 const [isAuthenticated, setIsAuthenticated] = useState(false); 7 8 const authenticate = async () => { 9 try { 10 const rnBiometrics = new ReactNativeBiometrics(); 11 12 // Verificar si hay biometría disponible 13 const { available } = await rnBiometrics.isSensorAvailable(); 14 15 if (!available) { 16 Alert.alert('Error', 'Biometría no disponible en este dispositivo'); 17 return; 18 } 19 20 // Solicitar autenticación 21 const { success } = await rnBiometrics.simplePrompt({ 22 promptMessage: 'Autentícate para continuar', 23 cancelButtonText: 'Cancelar', 24 }); 25 26 if (success) { 27 setIsAuthenticated(true); 28 Alert.alert('Éxito', '¡Autenticación exitosa!'); 29 } else { 30 Alert.alert('Error', 'Autenticación cancelada o fallida'); 31 } 32 } catch (error) { 33 console.error('Error en autenticación:', error); 34 Alert.alert('Error', 'Hubo un problema con la autenticación'); 35 } 36 }; 37 38 if (isAuthenticated) { 39 return ( 40 <View style={styles.container}> 41 <Text style={styles.title}>Autenticado</Text> 42 <Text style={styles.subtitle}>Bienvenido de vuelta</Text> 43 <TouchableOpacity 44 style={styles.button} 45 onPress={() => setIsAuthenticated(false)} 46 > 47 <Text style={styles.buttonText}>Cerrar sesión</Text> 48 </TouchableOpacity> 49 </View> 50 ); 51 } 52 53 return ( 54 <View style={styles.container}> 55 <Text style={styles.title}>🔒 Inicio de Sesión</Text> 56 <Text style={styles.subtitle}>Usa biometría para acceder</Text> 57 58 <TouchableOpacity style={styles.button} onPress={authenticate}> 59 <Text style={styles.buttonText}>Autenticar</Text> 60 </TouchableOpacity> 61 </View> 62 ); 63} 64 65const styles = StyleSheet.create({ 66 container: { 67 flex: 1, 68 justifyContent: 'center', 69 alignItems: 'center', 70 padding: 20, 71 backgroundColor: '#f5f5f5', 72 }, 73 title: { 74 fontSize: 32, 75 fontWeight: 'bold', 76 marginBottom: 10, 77 }, 78 subtitle: { 79 fontSize: 16, 80 color: '#666', 81 marginBottom: 40, 82 }, 83 button: { 84 backgroundColor: '#2196F3', 85 paddingHorizontal: 40, 86 paddingVertical: 15, 87 borderRadius: 8, 88 }, 89 buttonText: { 90 color: 'white', 91 fontSize: 18, 92 fontWeight: '600', 93 }, 94});

Almacenamiento Seguro

Nunca, pero NUNCA, guardes passwords o tokens en AsyncStorage. Eso es texto plano. Para datos sensibles, usa el keychain (iOS) y el Keystore (Android).

Instala react-native-keychain

1npm install react-native-keychain 2cd ios && pod install

Guarda y recupera credenciales

Así es como guardas datos de forma segura. Crea src/utils/secureStorage.js:

1import * as Keychain from 'react-native-keychain'; 2 3// Guardar credenciales 4async function saveCredentials(username, password) { 5 try { 6 await Keychain.setGenericPassword(username, password, { 7 service: 'com.myapp.auth', 8 accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED, 9 }); 10 console.log('Credenciales guardadas de forma segura'); 11 return true; 12 } catch (error) { 13 console.error('Error al guardar:', error); 14 return false; 15 } 16} 17 18// Recuperar credenciales 19async function getCredentials() { 20 try { 21 const credentials = await Keychain.getGenericPassword({ 22 service: 'com.myapp.auth', 23 }); 24 25 if (credentials) { 26 console.log('Usuario:', credentials.username); 27 console.log('Password:', credentials.password); 28 return credentials; 29 } else { 30 console.log('No hay credenciales guardadas'); 31 return null; 32 } 33 } catch (error) { 34 console.error('Error al recuperar:', error); 35 return null; 36 } 37} 38 39// Eliminar credenciales 40async function deleteCredentials() { 41 try { 42 await Keychain.resetGenericPassword({ 43 service: 'com.myapp.auth', 44 }); 45 console.log('Credenciales eliminadas'); 46 return true; 47 } catch (error) { 48 console.error('Error al eliminar:', error); 49 return false; 50 } 51}

Ejemplo real: Login con almacenamiento seguro

Vamos a juntar todo en un login funcional que persiste las credenciales. Crea src/screens/SecureLoginScreen.js:

1import React, { useState, useEffect } from 'react'; 2import { View, TextInput, TouchableOpacity, Text, StyleSheet, Alert } from 'react-native'; 3import * as Keychain from 'react-native-keychain'; 4 5export default function SecureLogin() { 6 const [username, setUsername] = useState(''); 7 const [password, setPassword] = useState(''); 8 const [isLoggedIn, setIsLoggedIn] = useState(false); 9 10 // Al montar, verificar si hay credenciales guardadas 11 useEffect(() => { 12 checkStoredCredentials(); 13 }, []); 14 15 const checkStoredCredentials = async () => { 16 try { 17 const credentials = await Keychain.getGenericPassword(); 18 if (credentials) { 19 setUsername(credentials.username); 20 setIsLoggedIn(true); 21 } 22 } catch (error) { 23 console.error('Error al verificar credenciales:', error); 24 } 25 }; 26 27 const handleLogin = async () => { 28 if (!username || !password) { 29 Alert.alert('Error', 'Por favor completa todos los campos'); 30 return; 31 } 32 33 // Aquí harías tu llamada al API 34 // Por ahora, simulamos un login exitoso 35 36 try { 37 await Keychain.setGenericPassword(username, password); 38 setIsLoggedIn(true); 39 Alert.alert('Éxito', 'Credenciales guardadas de forma segura'); 40 } catch (error) { 41 Alert.alert('Error', 'No se pudieron guardar las credenciales'); 42 } 43 }; 44 45 const handleLogout = async () => { 46 try { 47 await Keychain.resetGenericPassword(); 48 setIsLoggedIn(false); 49 setUsername(''); 50 setPassword(''); 51 Alert.alert('Sesión cerrada', 'Credenciales eliminadas'); 52 } catch (error) { 53 Alert.alert('Error', 'No se pudo cerrar sesión'); 54 } 55 }; 56 57 if (isLoggedIn) { 58 return ( 59 <View style={styles.container}> 60 <Text style={styles.title}>¡Bienvenido, {username}!</Text> 61 <TouchableOpacity style={styles.button} onPress={handleLogout}> 62 <Text style={styles.buttonText}>Cerrar Sesión</Text> 63 </TouchableOpacity> 64 </View> 65 ); 66 } 67 68 return ( 69 <View style={styles.container}> 70 <Text style={styles.title}>Inicio de Sesión Seguro</Text> 71 72 <TextInput 73 style={styles.input} 74 placeholder="Usuario" 75 value={username} 76 onChangeText={setUsername} 77 autoCapitalize="none" 78 /> 79 80 <TextInput 81 style={styles.input} 82 placeholder="Contraseña" 83 value={password} 84 onChangeText={setPassword} 85 secureTextEntry 86 /> 87 88 <TouchableOpacity style={styles.button} onPress={handleLogin}> 89 <Text style={styles.buttonText}>Iniciar Sesión</Text> 90 </TouchableOpacity> 91 </View> 92 ); 93} 94 95const styles = StyleSheet.create({ 96 container: { 97 flex: 1, 98 justifyContent: 'center', 99 padding: 20, 100 backgroundColor: '#f5f5f5', 101 }, 102 title: { 103 fontSize: 24, 104 fontWeight: 'bold', 105 marginBottom: 30, 106 textAlign: 'center', 107 }, 108 input: { 109 backgroundColor: 'white', 110 padding: 15, 111 borderRadius: 8, 112 marginBottom: 15, 113 borderWidth: 1, 114 borderColor: '#ddd', 115 }, 116 button: { 117 backgroundColor: '#2196F3', 118 padding: 15, 119 borderRadius: 8, 120 alignItems: 'center', 121 }, 122 buttonText: { 123 color: 'white', 124 fontSize: 16, 125 fontWeight: '600', 126 }, 127});

Cómo Funciona el Bridging: JavaScript ↔ Nativo

Aquí viene la parte interesante. React Native funciona con dos "mundos" que se comunican:

┌─────────────────┐         ┌──────────────────┐
│   JavaScript    │ Bridging│   Código Nativo  │
│   (Tu código)   │ ←─────→ │  (Kotlin/Swift)  │
└─────────────────┘         └──────────────────┘

Ese "bridging" (conexión) puede funcionar de dos formas:

El Bridge tradicional (la forma vieja)

Es el mecanismo original de comunicación:

  • Los mensajes van de forma asíncrona
  • Los datos se serializan a JSON para cruzar
  • Puede haber latencia si mandas muchos mensajes
  • Todos los Native Modules se cargan al inicio (aunque no los uses)
1// Native Module tradicional 2NativeModules.MyModule.doSomething(); // Siempre asíncrono

JSI + TurboModules (la forma nueva)

La nueva arquitectura cambia el juego, porque tiene la comunicación síncrona cuando la necesitas, nada de serialización JSON innecesaria, los módulos se cargan bajo demanda (lazy loading) y es mucho, mucho más rápido.

1// TurboModule 2const result = TurboModules.MyModule.doSomething(); // Puede ser síncrono!

En resumen:

  • Bridge = El canal de comunicación viejo (JSON, async)
  • JSI = El canal de comunicación nuevo (directo, sync cuando se necesita)
  • Native Modules = Módulos que usan el Bridge
  • TurboModules = Módulos que usan JSI

Ambos logran lo mismo, conectar JS con código nativo. La diferencia es el mecanismo interno.

TurboModules y JSI: La Nueva Arquitectura

JSI (JavaScript Interface) es la capa que permite que JavaScript y C++ hablen directamente, sin el bridge JSON de por medio. Reemplaza al Bridge tradicional. Lo que te da que puedes hacer llamadas síncronas (cuando las necesitas), estar sin overhead de serialización/deserialización, te permite acceso directo a objetos nativos desde JS y rendimiento significativamente mejor.

¿Qué son los TurboModules?

Son módulos nativos construidos sobre JSI (no sobre el Bridge). Si alguna vez creaste un Native Module, los TurboModules son básicamente lo mismo pero mejor.

Las diferencias clave:

AspectoNative Modules (viejo)TurboModules (nuevo)
CargaTodos al inicioBajo demanda
Tipo de llamadasSolo asíncronasSíncronas y asíncronas
RendimientoBridge JSONJSI directo
Definición de tiposManualAutomática (Codegen)

En la práctica, los Native Modules tradicionales siguen funcionando perfectamente. Pero si estás empezando un proyecto nuevo o creando una librería, vale la pena usar TurboModules.

Crea tu Propio Módulo Nativo (Bridging)

Vamos a crear algo simple pero útil: un módulo que te da información del dispositivo. Esto te enseñará el flujo completo de hacer bridging entre JS y código nativo.

Estructura de archivos que crearemos:

tu-proyecto/
├── specs/
│   └── NativeDeviceInfo.ts          # Definición TypeScript
├── ios/
│   ├── DeviceInfo.swift             # Implementación iOS
│   └── DeviceInfo.m                 # Bridge Objective-C
└── android/app/src/main/java/com/tuapp/
    ├── DeviceInfoModule.kt          # Implementación Android
    └── DeviceInfoPackage.kt         # Registro del módulo

Paso 1: Define la interfaz en TypeScript

Primero le dices a React Native qué métodos va a tener tu módulo.

Crea el archivo specs/NativeDeviceInfo.ts en la raíz de tu proyecto:

1// specs/NativeDeviceInfo.ts 2import type { TurboModule } from 'react-native'; 3import { TurboModuleRegistry } from 'react-native'; 4 5export interface Spec extends TurboModule { 6 getDeviceModel(): string; 7 getBatteryLevel(): Promise<number>; 8 vibrate(duration: number): void; 9} 10 11export default TurboModuleRegistry.getEnforcing<Spec>('DeviceInfo');

Paso 2: Implementa el módulo en iOS (Swift)

Ahora vamos con el código nativo para iOS.

Crea el archivo ios/DeviceInfo.swift:

1// ios/DeviceInfo.swift 2import Foundation 3import UIKit 4 5@objc(DeviceInfo) 6class DeviceInfo: NSObject { 7 8 @objc 9 func getDeviceModel() -> String { 10 return UIDevice.current.model 11 } 12 13 @objc 14 func getBatteryLevel(_ resolve: @escaping RCTPromiseResolveBlock, 15 rejecter reject: @escaping RCTPromiseRejectBlock) { 16 UIDevice.current.isBatteryMonitoringEnabled = true 17 let batteryLevel = UIDevice.current.batteryLevel 18 19 if batteryLevel < 0 { 20 reject("ERROR", "No se pudo obtener nivel de batería", nil) 21 } else { 22 resolve(Double(batteryLevel * 100)) 23 } 24 } 25 26 @objc 27 func vibrate(_ duration: NSNumber) { 28 // iOS solo soporta vibración fija 29 AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) 30 } 31 32 @objc 33 static func requiresMainQueueSetup() -> Bool { 34 return false 35 } 36}

Ahora crea el bridge de Objective-C en ios/DeviceInfo.m:

1// ios/DeviceInfo.m 2#import <React/RCTBridgeModule.h> 3 4@interface RCT_EXTERN_MODULE(DeviceInfo, NSObject) 5 6RCT_EXTERN_METHOD(getDeviceModel) 7RCT_EXTERN_METHOD(getBatteryLevel:(RCTPromiseResolveBlock)resolve 8 rejecter:(RCTPromiseRejectBlock)reject) 9RCT_EXTERN_METHOD(vibrate:(nonnull NSNumber *)duration) 10 11@end

Paso 3: Implementa el módulo en Android (Kotlin)

Y ahora el código para Android.

Crea el archivo android/app/src/main/java/com/tuapp/DeviceInfoModule.kt (reemplaza "tuapp" con el nombre de tu app):

1// android/app/src/main/java/com/yourapp/DeviceInfoModule.kt 2package com.yourapp 3 4import android.os.Build 5import android.os.VibrationEffect 6import android.os.Vibrator 7import android.content.Context 8import android.content.Intent 9import android.content.IntentFilter 10import android.os.BatteryManager 11import com.facebook.react.bridge.ReactApplicationContext 12import com.facebook.react.bridge.ReactContextBaseJavaModule 13import com.facebook.react.bridge.ReactMethod 14import com.facebook.react.bridge.Promise 15 16class DeviceInfoModule(reactContext: ReactApplicationContext) : 17 ReactContextBaseJavaModule(reactContext) { 18 19 override fun getName(): String { 20 return "DeviceInfo" 21 } 22 23 @ReactMethod 24 fun getDeviceModel(): String { 25 return Build.MODEL 26 } 27 28 @ReactMethod 29 fun getBatteryLevel(promise: Promise) { 30 try { 31 val batteryStatus: Intent? = reactApplicationContext.registerReceiver( 32 null, 33 IntentFilter(Intent.ACTION_BATTERY_CHANGED) 34 ) 35 36 val level = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1 37 val scale = batteryStatus?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1 38 39 val batteryPct = level * 100 / scale.toFloat() 40 promise.resolve(batteryPct.toDouble()) 41 } catch (e: Exception) { 42 promise.reject("ERROR", "No se pudo obtener nivel de batería", e) 43 } 44 } 45 46 @ReactMethod 47 fun vibrate(duration: Double) { 48 val vibrator = reactApplicationContext 49 .getSystemService(Context.VIBRATOR_SERVICE) as Vibrator 50 51 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 52 vibrator.vibrate( 53 VibrationEffect.createOneShot( 54 duration.toLong(), 55 VibrationEffect.DEFAULT_AMPLITUDE 56 ) 57 ) 58 } else { 59 @Suppress("DEPRECATION") 60 vibrator.vibrate(duration.toLong()) 61 } 62 } 63}

Ahora crea el Package en android/app/src/main/java/com/tuapp/DeviceInfoPackage.kt:

1// android/app/src/main/java/com/yourapp/DeviceInfoPackage.kt 2package com.yourapp 3 4import com.facebook.react.ReactPackage 5import com.facebook.react.bridge.NativeModule 6import com.facebook.react.bridge.ReactApplicationContext 7import com.facebook.react.uimanager.ViewManager 8 9class DeviceInfoPackage : ReactPackage { 10 override fun createNativeModules(reactContext: ReactApplicationContext): 11 List<NativeModule> { 12 return listOf(DeviceInfoModule(reactContext)) 13 } 14 15 override fun createViewManagers(reactContext: ReactApplicationContext): 16 List<ViewManager<*, *>> { 17 return emptyList() 18 } 19}

No olvides registrar el paquete en android/app/src/main/java/com/tuapp/MainApplication.kt:

1override fun getPackages(): List<ReactPackage> { 2 return PackageList(this).packages.apply { 3 add(DeviceInfoPackage()) 4 } 5}

Paso 4: Úsalo desde JavaScript

Y ahora la magia. Desde tu JavaScript puedes llamar ese código nativo.

Crea src/screens/DeviceInfoScreen.js:

1import { NativeModules } from 'react-native'; 2const { DeviceInfo } = NativeModules; 3 4export default function DeviceInfoScreen() { 5 const [deviceModel, setDeviceModel] = useState(''); 6 const [batteryLevel, setBatteryLevel] = useState(0); 7 8 useEffect(() => { 9 loadDeviceInfo(); 10 }, []); 11 12 const loadDeviceInfo = async () => { 13 // Método síncrono 14 const model = DeviceInfo.getDeviceModel(); 15 setDeviceModel(model); 16 17 // Método asíncrono (Promise) 18 const battery = await DeviceInfo.getBatteryLevel(); 19 setBatteryLevel(battery); 20 }; 21 22 const handleVibrate = () => { 23 // Método void (sin retorno) 24 DeviceInfo.vibrate(500); // 500ms 25 }; 26 27 return ( 28 <View style={styles.container}> 29 <Text style={styles.title}>Información del Dispositivo</Text> 30 31 <View style={styles.info}> 32 <Text style={styles.label}>Modelo:</Text> 33 <Text style={styles.value}>{deviceModel}</Text> 34 </View> 35 36 <View style={styles.info}> 37 <Text style={styles.label}>Batería:</Text> 38 <Text style={styles.value}>{batteryLevel.toFixed(0)}%</Text> 39 </View> 40 41 <TouchableOpacity style={styles.button} onPress={handleVibrate}> 42 <Text style={styles.buttonText}>Vibrar</Text> 43 </TouchableOpacity> 44 </View> 45 ); 46}

Mejores Prácticas (Para Que no te Compliques)

1. Siempre maneja los errores

No asumas que todo va a funcionar:

1try { 2 const result = await NativeModule.someMethod(); 3} catch (error) { 4 console.error('Error en módulo nativo:', error); 5 // Maneja el error en la UI 6}

2. Verifica si el dispositivo soporta la feature

No todos los dispositivos tienen todas las capacidades:

1import { Platform, NativeModules } from 'react-native'; 2 3function isFeatureAvailable() { 4 if (Platform.OS === 'ios') { 5 return Platform.Version >= 13; // iOS 13+ 6 } else { 7 return Platform.Version >= 23; // Android 6.0+ 8 } 9}

3. TypeScript es tu amigo

Define los tipos de tu módulo nativo para evitar errores:

1interface DeviceInfoModule { 2 getDeviceModel(): string; 3 getBatteryLevel(): Promise<number>; 4 vibrate(duration: number): void; 5} 6 7const DeviceInfo = NativeModules.DeviceInfo as DeviceInfoModule;

Con estas herramientas puedes construir apps que se sienten tan nativas como si las hubieras escrito en Kotlin o Swift puro. La diferencia es que con React Native escribes una vez y funciona en ambas plataformas. Y sí, cuando necesitas performance extremo o acceso a APIs muy específicas, todavía puedes escribir código nativo. React Native no te limita, te da opciones.