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.
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 type | Conforms to RCTBridgeModule | Extends ReactContextBaseJavaModule |
| Registration | RCT_EXPORT_MODULE() macro | Added inside a ReactPackage |
| Exposed methods | RCT_EXPORT_METHOD(...) | @ReactMethod annotation |
| Default thread | Background GCD queue | Native modules thread pool |
| Touching UI | Must dispatch to main queue | Must post to main/UI thread |
Build It
Code Examples
iOS: a minimal native module in 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)
@endAndroid: the same module in 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
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
- 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.
- 2Add error handling: force a rejection path (simulate an unavailable API) and confirm the JS-side try/catch receives it correctly.
- 3Convert your working native module into a typed TurboModule spec (Day 34) and note exactly which files changed.