RoadmapDay 35 / 80
React NativeMonth 2 · Week 7

Day 35: Native Modules Integration (iOS Swift/Objective-C & Android Kotlin/Java)

Write, register, and call a real native module on both platforms end to end — the practical skill underneath everything from Days 31-34.

Mark this day complete

Study

Concepts

The three pieces every native module needs

(1) A native implementation per platform — a Swift/Obj-C class on iOS conforming to `RCTBridgeModule`, a Kotlin/Java class on Android extending `ReactContextBaseJavaModule`. (2) Registration — iOS uses an Obj-C macro (`RCT_EXPORT_MODULE`) or, in the New Architecture, conformance to the Codegen-generated protocol; Android registers the module inside a `ReactPackage`. (3) A JS-facing spec (Day 34) or, for the old architecture, a plain `NativeModules.MyModule` lookup — this is what your React code actually imports and calls.

Threading discipline matters on both platforms: iOS native module methods run on a background queue by default unless you opt into the main queue (`requiresMainQueueSetup`), and any UIKit call must happen on the main thread; Android native methods run on a background thread from React Native's native module thread pool, and touching Android UI requires posting back to the main/UI thread via `runOnUiThread` or similar.

See It

Visualizations

Visualization

iOS vs Android native module anatomy

 iOS (Swift/Obj-C)Android (Kotlin/Java)
Base typeConforms to RCTBridgeModuleExtends ReactContextBaseJavaModule
RegistrationRCT_EXPORT_MODULE() macroAdded inside a ReactPackage
Exposed methodsRCT_EXPORT_METHOD(...)@ReactMethod annotation
Default threadBackground GCD queueNative modules thread pool
Touching UIMust dispatch to main queueMust post to main/UI thread

Build It

Code Examples

iOS: a minimal native module in Swift

swift
// DeviceInfo.swift
@objc(DeviceInfo)
class DeviceInfo: NSObject {

  @objc
  func getBatteryLevel(_ resolve: @escaping RCTPromiseResolveBlock,
                        rejecter reject: @escaping RCTPromiseRejectBlock) {
    UIDevice.current.isBatteryMonitoringEnabled = true
    let level = UIDevice.current.batteryLevel
    if level < 0 {
      reject("E_BATTERY", "Battery level unavailable", nil)
    } else {
      resolve(level)
    }
  }

  @objc
  static func requiresMainQueueSetup() -> Bool { return false }
}

// DeviceInfo.m — the Obj-C bridging header RN needs to see the exports
@interface RCT_EXTERN_MODULE(DeviceInfo, NSObject)
RCT_EXTERN_METHOD(getBatteryLevel:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
@end

Android: the same module in Kotlin

kotlin
// DeviceInfoModule.kt
class DeviceInfoModule(reactContext: ReactApplicationContext) :
  ReactContextBaseJavaModule(reactContext) {

  override fun getName() = "DeviceInfo"

  @ReactMethod
  fun getBatteryLevel(promise: Promise) {
    try {
      val bm = reactApplicationContext.getSystemService(BATTERY_SERVICE) as BatteryManager
      val level = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
      promise.resolve(level)
    } catch (e: Exception) {
      promise.reject("E_BATTERY", "Battery level unavailable", e)
    }
  }
}

// DeviceInfoPackage.kt — registers the module with React Native
class DeviceInfoPackage : ReactPackage {
  override fun createNativeModules(reactContext: ReactApplicationContext) =
    listOf(DeviceInfoModule(reactContext))
  override fun createViewManagers(reactContext: ReactApplicationContext) = emptyList<ViewManager<*, *>>()
}

Calling it from JS — identical call site on both platforms

js
import { NativeModules } from 'react-native';
const { DeviceInfo } = NativeModules;

async function logBattery() {
  try {
    const level = await DeviceInfo.getBatteryLevel();
    console.log('Battery level:', level);
  } catch (err) {
    console.error('Native module error:', err.code, err.message);
  }
}

Remember

Key Takeaways

  • Every native module needs: a native implementation, a registration step, and a JS-facing call surface — on BOTH platforms.
  • iOS: RCTBridgeModule + RCT_EXPORT_MODULE/METHOD macros. Android: ReactContextBaseJavaModule + @ReactMethod, registered via a ReactPackage.
  • Resolve/reject (Promise-based) is the standard async pattern for a native call returning a single value on either platform.
  • Never touch UIKit or Android UI directly from a native module's default background thread — dispatch/post to the main thread first.
  • The JS call site (NativeModules.X.method()) looks identical regardless of which platform actually handles it — that consistency is the whole point of the abstraction.

Do It

Practice

  1. 1Build a real "DeviceInfo.getBatteryLevel" native module end to end on one platform (iOS or Android) inside a fresh RN project and call it from a component.
  2. 2Add error handling: force a rejection path (simulate an unavailable API) and confirm the JS-side try/catch receives it correctly.
  3. 3Convert your working native module into a typed TurboModule spec (Day 34) and note exactly which files changed.