Ever wonder how libraries like react-native-camera
or react-native-device-info
access native features?
In this guide, youโll learn how to create your own native module in React Native โ using Kotlin for Android and Swift for iOS โ to fetch the battery level of the device.
You'll understand how the React Native bridge works and how JavaScript can talk directly to platform-native code.
๐ What Youโll Build
We'll build a lightweight but powerful native module called BatteryStatus
that retrieves the current battery level of the device.
Itโs a perfect hands-on project to understand how the React Native bridge acts as a translator between your JavaScript and native code โ giving you the best of both worlds: cross-platform development and low-level access to device features.
โ Pre-requisites Checklist
Make sure the following tools are installed and working:
- โ Node.js & npm
- โ
React Native CLI (
npx react-native init
) - โ Android Studio (for Android)
- โ Xcode (for iOS on macOS)
โ ๏ธ This tutorial is for React Native CLI projects (not Expo). Expo doesnโt support custom native modules out of the box unless you eject or use Expo Modules API.
๐๏ธ Step 1: Create a New Project
Start by initializing a fresh React Native app:
npx react-native init BatteryModuleApp
cd BatteryModuleApp
Weโll first create the native module for Android, then move on to iOS.
๐ค Android: Build the Native Module in Kotlin
๐งฑ Step 2: Create the BatteryModule.kt
Class
๐ android/app/src/main/java/com/batterymoduleapp/BatteryModule.kt
package com.batterymoduleapp
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import com.facebook.react.bridge.*
class BatteryModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
// Module name used to access from JavaScript
override fun getName(): String = "BatteryStatus"
@ReactMethod
fun getBatteryLevel(promise: Promise) {
try {
// Get battery info from system intent
val batteryIntent = reactApplicationContext.registerReceiver(
null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)
)
val level = batteryIntent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
val scale = batteryIntent?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
val batteryPct = level * 100 / scale
promise.resolve(batteryPct) // Send value back to JS
} catch (e: Exception) {
promise.reject("BATTERY_ERROR", "Failed to get battery level", e)
}
}
}
๐ฆ Step 3: Register the Module with BatteryPackage.kt
๐ android/app/src/main/java/com/batterymoduleapp/BatteryPackage.kt
package com.batterymoduleapp
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
class BatteryPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(BatteryModule(reactContext))
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return emptyList()
}
}
๐งฉ Step 4: Register the Package in MainApplication.kt
๐android/app/src/main/java/com/batterymoduleapp/MainApplication.kt
override fun getPackages(): List<ReactPackage> {
return listOf(
MainReactPackage(),
BatteryPackage() // ๐ Register your package here
)
}
๐ iOS: Build the Native Module in Swift
๐งฑ Step 5: Create BatteryStatus.swift
๐ ios/BatteryModuleApp/BatteryStatus.swift
import Foundation
import UIKit
@objc(BatteryStatus)
class BatteryStatus: NSObject {
@objc
func getBatteryLevel(_ resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock) {
UIDevice.current.isBatteryMonitoringEnabled = true
let level = UIDevice.current.batteryLevel
if level < 0 {
reject("BATTERY_ERROR", "Battery level is unavailable", nil)
} else {
resolve(Int(level * 100))
}
}
}
๐ง Whatโs RCTPromiseResolveBlock?
Itโs a bridge-friendly way of returning asynchronous values from native Swift code back to JavaScript.
๐งฉ Step 6: Bridge Swift to React Native
Create an Objective-C bridging file:
๐ ios/BatteryModuleApp/BatteryStatus.m
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(BatteryStatus, NSObject)
RCT_EXTERN_METHOD(getBatteryLevel:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
@end
๐ If not already created, add a bridging header file:
BatteryModuleApp-Bridging-Header.h
#import <React/RCTBridgeModule.h>
โ๏ธ Step 7: Call the Module from JavaScript
๐ App.js
import React from 'react';
import {NativeModules, Button, Alert, View} from 'react-native';
const {BatteryStatus} = NativeModules;
export default function App() {
const getBattery = async () => {
try {
const level = await BatteryStatus.getBatteryLevel();
Alert.alert(`Battery Level: ${level}%`);
} catch (e) {
Alert.alert('Error fetching battery level');
}
};
return (
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<Button title="Get Battery Level" onPress={getBattery} />
</View>
);
}
๐ง This works because React Nativeโs bridge exposes our native BatteryStatus
class to JavaScript via NativeModules
.
๐ ๏ธ Troubleshooting Tips
โNative module cannot be foundโ error?
- Android: Check if BatteryPackage is added to MainApplication.kt
- iOS: Confirm RCT_EXTERN_MODULE is in place and bridging header is set
- Try cleaning: cd android && ./gradlew clean or npx react-native clean
iOS build fails on Swift?
- Make sure youโve created and linked the Bridging-Header.h correctly
- Check your targetโs build settings
๐ง Final Thoughts
Custom native modules unlock the full capabilities of React Native. You can:
- ๐ Access low-level features (camera, Bluetooth, sensors)
- ๐ Improve performance by offloading heavy tasks to native code
- ๐ Integrate native SDKs (payments, health data, media)
And best of all โ you gain true cross-platform control while keeping the power of React Nativeโs declarative UI.
Follow me on LinkedIn
Got questions or ideas for improvements? Drop them in the comments! ๐ฌ
Top comments (0)