Native Module è una classe Java o Objective-C che rende le API native della piattaforma accessibili da JavaScript in React Native. Ogni modulo viene registrato nel Bridge ed esporta metodi che possono essere chiamati dal codice JS come funzioni normali. Secondo Meta, 2024, Native Module rimane il modo principale per integrare il codice della piattaforma nelle applicazioni React Native.
Punti chiave
Native Module è un elemento architetturale di React Native che consente di eseguire codice nel linguaggio della piattaforma (Objective-C/Swift per iOS, Java/Kotlin per Android) e restituire il risultato a JavaScript. Senza un Native Module è impossibile accedere alle capacità native del dispositivo — fotocamera, GPS, accelerometro, file system o Bluetooth.
React Native viene fornito con un insieme di Native Module integrati: CameraRoll, AsyncStorage, Geolocation, NetInfo e altri. Tuttavia, per attività specifiche — integrazione di SDK di terze parti, lavoro con sensori hardware o processi in background — lo sviluppatore crea moduli personalizzati. Secondo il sondaggio State of React Native 2024, il 67% degli sviluppatori utilizza almeno un Native Module personalizzato nei propri progetti.
L'architettura di Native Module dipende dalla versione di React Native. Nell'architettura classica (React Native 0.72 e precedenti) il modulo si connette tramite il Bridge e comunica con JS in modo asincrono tramite serializzazione JSON. Nella nuova architettura (React Native 0.76+) il modulo può funzionare come Turbo Module, utilizzando JSI per l'accesso sincrono senza serializzazione.
La creazione di un Native Module per iOS inizia con la dichiarazione di una classe Objective-C che implementa il protocollo RCTBridgeModule. La macro RCT_EXPORT_MODULE registra il modulo nel Bridge, e RCT_EXPORT_METHOD esporta un metodo disponibile da JavaScript.
// ImageCompressor.m — Native Module per iOS
@interface ImageCompressor () RCT_EXPORT_MODULE()
@end
@implementation ImageCompressor
RCT_EXPORT_METHOD(compressImage:(NSString *)imagePath
quality:(NSNumber *)quality
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
NSData *compressedData = [UIImageJPEGRepresentation(image, quality.floatValue)];
NSString *outputPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"compressed.jpg"];
[compressedData writeToFile:outputPath atomically:YES];
resolve(outputPath);
}
@end
Il metodo compressImage accetta un percorso immagine e una qualità di compressione (0.0–1.0), elabora i dati sul lato nativo e restituisce il percorso al file compresso. Il vantaggio principale è che la compressione viene eseguita da codice nativo, che è significativamente più veloce ed efficiente in memoria rispetto all'operazione equivalente in JavaScript.
Per i moduli Swift, l'annotazione @objc viene utilizzata prima della classe e dei metodi per renderli disponibili al runtime Objective-C con cui lavora il Bridge. La classe deve ereditare da NSObject e implementare RCTBridgeModule.
// ImageCompressor.swift — Modulo Swift Nativo
@objc(ImageCompressor)
class ImageCompressor: NSObject {
@objc
func compressImage(
_ imagePath: String,
quality: Float,
resolver: @escaping RCTPromiseResolveBlock,
rejecter: @escaping RCTPromiseRejectBlock
) {
guard let image = UIImage(contentsOfFile: imagePath) else {
rejecter("FILE_ERROR", "Cannot load image", nil)
return
}
guard let data = image.jpegData(compressionQuality: CGFloat(quality)) else {
rejecter("COMPRESS_ERROR", "Compression failed", nil)
return
}
let outputPath = NSTemporaryDirectory() + "compressed.jpg"
try? data.write(to: URL(fileURLWithPath: outputPath))
resolver(outputPath)
}
}
Su Android, un Native Module viene creato come classe Java che estende ReactContextBaseJavaModule. L'annotazione @ReactMethod esporta il metodo nel Bridge. L'interfaccia Promise di com.facebook.react.bridge viene utilizzata per restituire i risultati.
// ImageCompressorModule.java — Native Module per Android
public class ImageCompressorModule
extends ReactContextBaseJavaModule {
@Override
public String getName() {
return "ImageCompressor";
}
@ReactMethod
public void compressImage(
String imagePath,
Float quality,
Promise promise) {
try {
Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
File outputFile = new File(
ReactNative.getApplicationContext()
.getCacheDir(), "compressed.jpg");
FileOutputStream fos = new FileOutputStream(outputFile);
bitmap.compress(
Bitmap.CompressFormat.JPEG,
(int)(quality * 100), fos);
fos.close();
promise.resolve(outputFile.getAbsolutePath());
} catch (Exception e) {
promise.reject("COMPRESS_ERROR", e.getMessage());
}
}
}
Il metodo getName() restituisce il nome del modulo sotto cui sarà accessibile da JavaScript. Nell'esempio sopra, il modulo è registrato come ImageCompressor. L'annotazione @ReactMethod indica al Bridge che il metodo deve essere esportato. Importante: i metodi devono essere void e accettare solo i tipi supportati dal Bridge: String, Boolean, Integer, Double, ReadableArray, ReadableMap, Promise.
Dopo aver creato la classe del modulo, deve essere registrata nel pacchetto dell'applicazione. Per questo, viene creata una classe che implementa ReactPackage e aggiunta all'elenco dei moduli nel metodo createNativeModules.
// ImageCompressorPackage.java — registrazione del modulo
public class ImageCompressorPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
return Arrays.asList(
new ImageCompressorModule(reactContext)
);
}
@Override
public List<ViewManager> createViewManagers(
ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
Dopo aver creato i moduli per entrambe le piattaforme, devono essere registrati in React Native. Per Android, il pacchetto viene aggiunto in MainApplication.java nel metodo getPackages(). Per iOS, il modulo viene registrato automaticamente tramite la macro RCT_EXPORT_MODULE, ma è possibile utilizzare anche la registrazione manuale in AppDelegate.mm.
// MainApplication.java — aggiunta pacchetto a React Native
import com.yourapp.nativemodules.ImageCompressorPackage;
public class MainApplication extends Application
implements ReactApplication {
private final ReactNativeHost mReactNativeHost =
new ReactNativeHost(this) {
@Override
protected List<ReactPackage> getPackages() {
List<ReactPackage> packages =
new PackageList(this).getPackages();
packages.add(new ImageCompressorPackage());
return packages;
}
};
}
Dopo la registrazione, il modulo diventa disponibile in JavaScript tramite NativeModules. React Native utilizza automaticamente il nome del modulo specificato in getName() per Android o RCT_EXPORT_MODULE per iOS.
// Utilizzo di Native Module da JavaScript
import { NativeModules } from 'react-native';
import { Platform } from 'react-native';
const ImageCompressor = NativeModules.ImageCompressor;
async function compressPhoto(uri: string) {
try {
const result = await ImageCompressor.compressImage(
uri.replace('file://', ''), 0.8
);
console.log('Compresso:', result);
return result;
} catch (error) {
console.error('Compressione fallita:', error);
throw error;
}
}
Il confronto tra il Native Module classico e Turbo Module aiuta a capire quale approccio scegliere per un nuovo progetto. Entrambi i meccanismi forniscono accesso al codice nativo, ma differiscono fondamentalmente per architettura e prestazioni.
| Caratteristica | Native Module (Bridge) | Turbo Module (JSI) |
|---|---|---|
| Comunicazione | Asincrona tramite JSON | Sincrona tramite JSI |
| Serializzazione | JSON ad ogni chiamata | Senza copia dei dati |
| Tipizzazione | Manuale, senza generazione | Automatica tramite Codegen |
| Caricamento | All'inizializzazione dell'applicazione | Caricamento differito (lazy load) |
| Compatibilità | Tutte le versioni di React Native | React Native 0.73+ |
Per i progetti esistenti su React Native 0.72 e precedenti, i Native Module classici rimangono la scelta principale. Per i nuovi progetti, si consiglia Turbo Module, specialmente se sono richieste prestazioni elevate per chiamate frequenti a metodi nativi. Con l'aggiornamento graduale di React Native, la comunità si sta muovendo verso una transizione completa alla nuova architettura.
Domande frequenti
Sì, il Bridge supporta i callback. Invece di Promise, si possono usare funzioni callback RCTResponseSenderBlock in iOS e Callback in Android. Tuttavia, Promise è considerato lo standard moderno ed è raccomandato per i nuovi moduli.
Un Native Module viene eseguito il debug come codice nativo normale — impostare punti di interruzione in Xcode o Android Studio. Per iOS, utilizzare lo schema di build con React Native; per Android, utilizzare la configurazione Debug. Il punto di ingresso sono i metodi chiamati da JS.
Native Module tramite Bridge non supporta dati binari (NSData/byte[]), oggetti personalizzati e funzioni. Per le immagini, utilizzare un percorso file o una stringa base64. Turbo Module tramite JSI rimuove alcune di queste limitazioni.
Di solito no — l'API Native Module è stabile e retrocompatibile. Durante la transizione alla nuova architettura (Turbo Module), il modulo viene adattato tramite decoratori, ma il codice esistente continua a funzionare.
Utilizzare RCTEventEmitter in iOS o DeviceEventEmitter in Android. Il modulo invia un evento e il lato JS si iscrive tramite NativeEventEmitter di react-native. Questo è utile per dati in streaming ed eventi dei sensori.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche