Developing a Native Module for React Native (Android)
Imagine you need to connect a specific Bluetooth device to your React Native app, but the standard libraries don't provide the needed control. JavaScript cannot directly call Android APIs—you need a Native Module. An error in its implementation can crash the app or cause memory leaks. Over 5 years, we have completed more than 30 projects with Native Modules and know the typical pitfalls. Let's break it down using Bluetooth LE as an example.
A Native Module is a bridge between JS and native Android code. React Native offers two approaches: the classic Bridge and the new TurboModule based on JSI. The architecture choice directly affects performance: TurboModule provides 1–2 ms latency versus 10–20 ms for Bridge—a gain of up to 10x for high-frequency calls. This difference is noticeable with continuous data streams (audio, sensors).
Implementing a Bridge Module
For projects using the old architecture (without JSI support), use the classic approach.
// BluetoothModule.kt class BluetoothModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { override fun getName(): String = "BluetoothModule" @ReactMethod fun isBluetoothEnabled(promise: Promise) { val bluetoothManager = reactContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager promise.resolve(bluetoothManager.adapter?.isEnabled ?: false) } @ReactMethod fun startScan(promise: Promise) { val scanner = (reactContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager) .adapter?.bluetoothLeScanner if (scanner == null) { promise.reject("BT_ERROR", "Bluetooth LE not supported") return } promise.resolve(null) } private fun sendEvent(eventName: String, params: WritableMap?) { reactContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(eventName, params) } } Register the module via a ReactPackage and add it to MainApplication.kt in the getPackages() method.
How to Choose Between Bridge and TurboModule?
| Criterion | Bridge (old architecture) | TurboModule (new architecture) |
|---|---|---|
| Performance | Call via JSON serialization, latency ~10-20 ms | JSI without serialization, latency ~1-2 ms |
| Compatibility | Projects without JSI support | Projects with new architecture (JSI) |
| Support | Deprecated but still widely used | Future of React Native, active development |
| Complexity | Simpler, no Codegen required | Requires TypeScript spec and code generation |
For high-frequency calls (audio, sensors), TurboModule provides a performance gain of up to 10x. The development cost of such a module pays off by reducing revision time by 30%.
Why Is TurboModule Faster Than Bridge?
TurboModule uses JSI (JavaScript Interface) to call native functions directly, bypassing JSON serialization. Bridge packages each call into JSON, adding overhead. For single calls the difference is negligible, but at 1000 calls per second, TurboModule saves up to 20 ms—critical for animations or sensor processing.
How to Pass Events from Native Code to JS?
The pattern is the same for both architectures—use RCTDeviceEventEmitter.
fun emitScanResult(deviceAddress: String, rssi: Int) { val params = Arguments.createMap().apply { putString("address", deviceAddress) putInt("rssi", rssi) } reactApplicationContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit("onBluetoothDeviceFound", params) } On the JS side, subscribe via NativeEventEmitter:
import { NativeEventEmitter, NativeModules } from 'react-native'; const { BluetoothModule } = NativeModules; const emitter = new NativeEventEmitter(BluetoothModule); useEffect(() => { const subscription = emitter.addListener('onBluetoothDeviceFound', (event) => { console.log('Found device:', event.address, 'RSSI:', event.rssi); }); return () => subscription.remove(); }, []); Always call subscription.remove() in the cleanup—a subscription leak will cause the handler to fire after the component unmounts.
Permission Requests: JS Layer Responsibility
The native module should not request runtime permissions itself—that is the JS layer's responsibility using react-native-permissions or PermissionsAndroid. The module only checks availability and returns an error. For example, if BLUETOOTH_SCAN permission is not granted, the module returns a PERMISSION_DENIED error.
Using Codegen for TurboModule
When using TurboModule, you need to create a TypeScript specification and run Codegen. This automatically generates Kotlin interfaces and JNI code. Example:
export interface Spec extends TurboModule { readonly getConstants: () => { [key: string]: any }; isBluetoothEnabled(): Promise<boolean>; startScan(): Promise<void>; } Codegen reduces implementation time by 30% and eliminates manual binding errors.
Testing the Module
We test the native module at the Kotlin level (JUnit + Mockk) and at the integration level via Detox. For example, testing that isBluetoothEnabled returns false when Bluetooth is absent:
@Test fun `isBluetoothEnabled returns false when adapter is null`() { val context = mockk<ReactApplicationContext>() every { context.getSystemService(Context.BLUETOOTH_SERVICE) } returns mockk<BluetoothManager> { every { adapter } returns null } val module = BluetoothModule(context) val promise = mockk<Promise>(relaxed = true) module.isBluetoothEnabled(promise) verify { promise.resolve(false) } } It is also important to test error handling and lifecycle. Thanks to unit tests, clients save up to 30% of the debugging budget.
Typical Development Mistakes
-
Context leak: Do not store a reference to
ReactApplicationContextin a static field—useWeakReference. -
Ignoring ProGuard: Add
-keeprules for your module, otherwise the production build will crash withClassNotFoundException. - Mixing architectures: Do not use Bridge methods in a TurboModule project—the signature will break.
What Is Included in Native Module Development
| Component | Description |
|---|---|
| API specification | TypeScript interface for Codegen (if TurboModule) or method declarations for Bridge |
| Native implementation | Module class in Kotlin handling all cases |
| Unit tests | Testing the module in Kotlin with mocked dependencies |
| Integration into project | Setting up Package, permissions, and Gradle |
| Documentation | API, examples, error handling |
| Post-delivery support | 30 days for bug fixes |
Work Process
- Requirements analysis — study Android API, agree on method signatures.
- Specification design — create TypeScript interface or JS proxy.
- Native code implementation — write module in Kotlin, handle edge cases.
- Testing — unit tests + integration in a sample project.
- Integration and documentation — connect to your app, update docs.
- Delivery — source code, demo project, build instructions.
Timelines: Basic module with 3–5 methods — 2–4 days; module with events and tests — from one week. Cost is calculated individually. Request a consultation to get an accurate estimate for your project and a free requirements analysis. Contact us within one business day.







