Native Modules & Bridging — Mobile

Native Modules & Bridging Kadang kamu butuh akses ke API native yang belum ada wrapper JavaScript-nya. Native Modules memungkinkan kamu menulis kode…

Native module adalah kode Swift (iOS) dan Kotlin (Android) di dalam aplikasimu yang bisa dipanggil dari JavaScript. Dipakai kalau yang kamu butuhkan belum ada di paket Expo atau library komunitas. Tablet kasir Bu Rini harus mencetak nota ke printer thermal Bluetooth, dan pabrik printernya (kita sebut "PrinterKu", contoh) cuma memberi SDK untuk Android dan iOS. Bayangin printer itu cuma paham bahasa pabriknya sendiri. Bu Rini (JavaScript) butuh penerjemah yang duduk di sebelah printer: itulah native module.

🧪 Tebakan cepat: Kamu membuat modul lokal thermal-printer, lalu membuka layar kasir yang meng-import modul itu di Expo Go (atau di development build yang dibuat sebelum modulnya ada). Apa yang terjadi? Jawabannya ada di bagian "Expo Modules API: modul lokal".

Kapan butuh kode native

Periksa urut dari atas, dan berhenti di jawaban pertama yang cukup:

  1. Ada paket Expo? Kamera, lokasi, notifikasi, penyimpanan aman: sudah ada (pelajaran sensor dan push).
  2. Ada library komunitas? Cari di React Native Directory, pastikan mendukung New Architecture.
  3. Cuma butuh konfigurasi native? Teks izin, entitlement, intent filter: config plugin (pelajaran Expo), tanpa kode native.
  4. Butuh kode yang memanggil API native atau SDK vendor? Baru tulis native module. Printer Bu Rini masuk sini.

JSI dan New Architecture

Di arsitektur lama, JavaScript dan native bicara lewat bridge: tiap panggilan diubah jadi pesan JSON, diantrikan, lalu diproses secara asinkron di seberang. Sejak React Native 0.82, cuma ada New Architecture (pelajaran pertama), dan dasarnya JSI (JavaScript Interface): JavaScript memegang referensi ke object C++ dan memanggil method-nya langsung, tanpa mengubahnya jadi JSON, bahkan secara sinkron. Di atas JSI ada TurboModules (native module yang dimuat saat dipakai, dengan kontrak bertipe dari Codegen) dan Fabric (renderer baru). Expo Modules API juga dibangun di atas JSI, dan menurut dokumentasinya performanya mirip Turbo Modules: keduanya sanggup ratusan ribu panggilan native per detik.

Expo Modules API: modul lokal

Dijalankan di project Laundry Bu Rini (create-expo-module 57.0.1, 11 Oktober 2026):

$ npx create-expo-module@latest --local thermal-printer --name ThermalPrinter --package id.laundryburini.thermalprinter --platform apple android --features Constant Function AsyncFunction Event
The local module will be created in modules/thermal-printer directory.
✔ Downloaded module template from npm registry.
✔ Created the module from template files
Remember to re-build your native app (for example, with npx expo run) when you make changes to the module. Native code changes are not reloaded with Fast Refresh.

Hasilnya modules/thermal-printer/ berisi expo-module.config.json, ios/ThermalPrinterModule.swift, ios/ThermalPrinter.podspec, android/.../ThermalPrinterModule.kt, dan src/ThermalPrinterModule.ts. Modul di folder modules/ ditemukan autolinking tanpa didaftarkan di package.json. Template-nya diubah jadi modul printer (SDK PrinterKu cuma contoh, jadi kode native ini gak dikompilasi di sini):

// modules/thermal-printer/ios/ThermalPrinterModule.swift
import ExpoModulesCore

struct Nota: Record {
  @Field var id: String = ""
  @Field var nama: String = ""
  @Field var kg: Double = 0
  @Field var total: Int = 0
}

public class ThermalPrinterModule: Module {
  public func definition() -> ModuleDefinition {
    Name("ThermalPrinter")

    Function("isConnected") { () -> Bool in
      PrinterKu.shared.isConnected
    }

    AsyncFunction("printNota") { (nota: Nota) -> Bool in
      guard PrinterKu.shared.isConnected else {
        throw Exception(name: "PrinterOffline", description: "Printer belum tersambung", code: "ERR_PRINTER_OFFLINE")
      }
      try PrinterKu.shared.print(lines: ["LAUNDRY BU RINI", "#\(nota.id) \(nota.nama)", "\(nota.kg) kg  Rp \(nota.total)"])
      return true
    }
  }
}

Versi Kotlin-nya sama bentuknya: class Nota : Record dengan @Field val, Function("isConnected") { PrinterKu.isConnected() }, dan AsyncFunction("printNota") { nota: Nota -> ... } yang melempar CodedException("ERR_PRINTER_OFFLINE", "Printer belum tersambung", null).

// modules/thermal-printer/src/ThermalPrinterModule.ts
import { NativeModule, requireNativeModule } from 'expo';

export type Nota = { id: string; nama: string; kg: number; total: number };

declare class ThermalPrinterModule extends NativeModule {
  isConnected(): boolean;
  printNota(nota: Nota): Promise<boolean>;
}

export default requireNativeModule<ThermalPrinterModule>('ThermalPrinter');

Kode native hanya ada di binary yang di-build. Expo Go dan development build lama gak punya ThermalPrinter, jadi requireNativeModule langsung gagal saat file itu di-import: Error: Cannot find native module 'ThermalPrinter'. Itu jawaban tebakannya. Buat build baru: npx expo run:android, npx expo run:ios, atau eas build --profile development. Pakai requireOptionalNativeModule (yang mengembalikan null) kalau layarnya harus tetap jalan tanpa modul itu.

Turbo Module dengan Codegen

Cara bawaan React Native. Kontraknya ditulis sebagai spec TypeScript, dan Codegen membuat kode C++ dan native dari spec itu saat build (task Gradle generateCodegenArtifactsFromSchema di Android, saat pod install di iOS):

// specs/NativeThermalPrinter.ts (nama file wajib diawali "Native")
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  isConnected(): boolean;
  printNota(nota: { id: string; nama: string; kg: number; total: number }): Promise<boolean>;
}

export default TurboModuleRegistry.getEnforcing<Spec>('NativeThermalPrinter');

Ditambah codegenConfig di package.json (name, type: "modules", jsSrcsDir: "specs", android.javaPackageName), lalu implementasi di Kotlin dan Objective-C++. Kalau modulnya belum ada di binary: TurboModuleRegistry.getEnforcing(...): 'NativeThermalPrinter' could not be found. Verify that a module by this name is registered in the native binary. Pilih Turbo Module kalau butuh C++ bersama untuk kedua platform, atau library yang gak mau bergantung pada paket expo. Untuk aplikasi Expo, Expo Modules API lebih ringkas.

Config plugin untuk izin Bluetooth

Modul printer butuh izin Bluetooth: NSBluetoothAlwaysUsageDescription di Info.plist, dan BLUETOOTH_CONNECT di Android 12 ke atas. Jangan ditulis tangan di folder ios/ (hilang saat prebuild, pelajaran Expo). Tulis config plugin:

// plugins/withPrinterBluetooth.js, lalu "./plugins/withPrinterBluetooth" di plugins app.json
const { AndroidConfig, withInfoPlist } = require('expo/config-plugins');

module.exports = function withPrinterBluetooth(config, { alasan } = {}) {
  config = withInfoPlist(config, (cfg) => {
    cfg.modResults.NSBluetoothAlwaysUsageDescription = alasan ?? 'Izinkan Laundry Bu Rini tersambung ke printer nota.';
    return cfg;
  });
  return AndroidConfig.Permissions.withPermissions(config, ['android.permission.BLUETOOTH_CONNECT']);
};

npx expo config --type introspect di project ini menampilkan NSBluetoothAlwaysUsageDescription: 'Izinkan Laundry Bu Rini tersambung ke printer nota.' dan 'android.permission.BLUETOOTH_CONNECT' di daftar izin. Tanpa plugin itu, iOS menghentikan aplikasi saat Bluetooth dipakai: This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an NSBluetoothAlwaysUsageDescription key with a string value explaining to the user how the app uses this data. Di Android 12 ke atas, BLUETOOTH_CONNECT juga diminta saat runtime, misalnya dengan PermissionsAndroid.request.

⚠️ Jebakan yang sering ketemu

TL;DR: Tulis native module cuma kalau paket Expo, library komunitas, dan config plugin gak cukup, misalnya untuk SDK printer vendor. Bridge lama adalah penghubung asinkron yang mengirim pesan JSON antara JavaScript dan native; New Architecture (TurboModules dan Fabric, satu-satunya sejak React Native 0.82) memakai JSI, jadi JavaScript memanggil native langsung, bisa sinkron, tanpa serialisasi JSON. Expo Modules API: npx create-expo-module@latest --local membuat modules/thermal-printer dengan Swift, Kotlin, dan TypeScript; Name, Function (sinkron, di thread JavaScript), AsyncFunction (Promise, thread lain), Record untuk object, dan requireNativeModule('ThermalPrinter') dengan interface bertipe. Turbo Module memakai spec NativeThermalPrinter.ts, TurboModuleRegistry.getEnforcing, dan Codegen. Kode native butuh build baru: di Expo Go hasilnya Cannot find native module 'ThermalPrinter'. Izin Bluetooth dipasang lewat config plugin (withInfoPlist, AndroidConfig.Permissions.withPermissions), bukan diedit tangan.

Yang akan kamu pelajari