Building a React Native Demo with the CLI: Native Modules, Event Emitters & Device Info
React Native is great for cross-platform apps, but sometimes you need platform-specific features like battery level or device model that aren’t available out-of-the-box. That’s where native modules come in.
In this tutorial, we’ll build a demo app using Kotlin native modules on Android that:
- Emits events to JS using EventEmitter
- Fetches device info like battery level and device model
- We’ll use React Native 0.81.1 and Kotlin.
Demo App Features
EventEmitterModule1 — Emits random greeting messages
EventEmitterModule2 — Emits random numbers
DeviceInfoModule1 — Fetches battery level
DeviceInfoModule2 — Fetches device model
Step 1: Set Up the Project
npx @react-native-community/cli@latest init NativeModulesDemo
cd NativeModulesDemo
npm install OR yarn installStep 2: Create Native Modules in Kotlin
1️⃣ EventEmitterModule1.kt — Random Greetings
package com.nativemodulesdemo.modules
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.modules.core.DeviceEventManagerModule
import kotlin.random.Random
class EventEmitterModule1(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
private val greetings = listOf("Hello!", "Hi there!", "Welcome!", "Good day!", "Howdy!", "Hey!")
override fun getName(): String = "EventEmitterModule1"
@ReactMethod
fun emitGreeting() {
val randomGreeting = greetings[Random.nextInt(greetings.size)]
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("onGreetingEvent", randomGreeting)
}
}Diagram: Random greeting emitted from Android → JS
User Clicks Button
↓
EventEmitterModule1
↓ emitGreeting()
"onGreetingEvent" → JS Listener
↓
React Native UI updatesTip: ⚠️ Avoid emitting events too frequently — it can overload the JS thread.
2️⃣ EventEmitterModule2.kt — Random Numbers
class EventEmitterModule2(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
private val random = Random
private val handler = Handler(Looper.getMainLooper()) // Ensure main thread
override fun getName(): String = "EventEmitterModule2"
@ReactMethod
fun sendRandomNumber() {
handler.post {
val num = random.nextInt(100)
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("onRandomNumber", num)
}
}
}Diagram: Random number emitted to JS
Button Click
↓
EventEmitterModule2.sendRandomNumber()
↓ "onRandomNumber"
JS Listener updates UI3️⃣ DeviceInfoModule1.kt — Battery Level
class DeviceInfoModule1(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
private val handler = Handler(Looper.getMainLooper()) // run on main thread
override fun getName(): String = "DeviceInfoModule1"
@ReactMethod
fun getBatteryLevel(promise: Promise) {
handler.post {
try {
val batteryIntent = reactApplicationContext.registerReceiver(
null,
IntentFilter(Intent.ACTION_BATTERY_CHANGED)
)
val level = batteryIntent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
promise.resolve(level)
} catch (e: Exception) {
promise.reject("BATTERY_ERROR", e)
}
}
}
}Screenshot Placeholder:
Battery Level on Android emulatorBattery: 87%
4️⃣ DeviceInfoModule2.kt — Device Model
class DeviceInfoModule2(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
private val handler = Handler(Looper.getMainLooper()) // run on main thread
override fun getName(): String = "DeviceInfoModule2"
@ReactMethod
fun getDeviceModel(promise: Promise) {
handler.post {
try {
promise.resolve(Build.MODEL)
} catch (e: Exception) {
promise.reject("MODEL_ERROR", e)
}
}
}
}Screenshot Placeholder:
Device model on Android emulatorDevice Model: Pixel 5
5️⃣ Register All Modules
class MyNativeModulesPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(
EventEmitterModule1(reactContext),
EventEmitterModule2(reactContext),
DeviceInfoModule1(reactContext),
DeviceInfoModule2(reactContext)
)
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return emptyList()
}
}Add this package to MainApplication.kt in getPackages().
Step 3: App.js — Using Native Modules in React Native
import React, { useEffect, useState } from 'react';
import {
View,
Text,
Button,
NativeModules,
NativeEventEmitter,
StyleSheet,
} from 'react-native';
const {
EventEmitterModule1,
EventEmitterModule2,
DeviceInfoModule1,
DeviceInfoModule2,
} = NativeModules;
export default function App() {
const [greeting, setGreeting] = useState('');
const [randomNumber, setRandomNumber] = useState(0);
const [battery, setBattery] = useState(0);
const [model, setModel] = useState('');
useEffect(() => {
// EventEmitter for greetings
const greetingEmitter = new NativeEventEmitter(EventEmitterModule1);
const greetingListener = greetingEmitter.addListener(
'onGreetingEvent',
message => {
setGreeting(message);
},
);
// EventEmitter for random numbers
const randomEmitter = new NativeEventEmitter(EventEmitterModule2);
const randomListener = randomEmitter.addListener('onRandomNumber', num => {
setRandomNumber(num);
});
return () => {
greetingListener.remove();
randomListener.remove();
};
}, []);
const fetchBattery = async () => {
try {
const level = await DeviceInfoModule1.getBatteryLevel();
setBattery(level);
} catch (e) {
console.error('Battery error:', e);
}
};
const fetchDeviceModel = async () => {
try {
const deviceModel = await DeviceInfoModule2.getDeviceModel();
setModel(deviceModel);
} catch (e) {
console.error('Device model error:', e);
}
};
return (
<View style={styles.container}>
{/* EventEmitterModule1 */}
<Text style={styles.text}>Greeting: {greeting}</Text>
<Button
title="Get Greeting"
onPress={() => EventEmitterModule1.emitGreeting()}
/>
{/* EventEmitterModule2 */}
<Text style={styles.text}>Random Number: {randomNumber}</Text>
<Button
title="Get Random Number"
onPress={() => EventEmitterModule2.sendRandomNumber()}
/>
{/* DeviceInfoModule1 */}
<Text style={styles.text}>Battery Level: {battery}</Text>
<Button title="Get Battery Level" onPress={fetchBattery} />
{/* DeviceInfoModule2 */}
<Text style={styles.text}>Device Model: {model}</Text>
<Button title="Get Device Model" onPress={fetchDeviceModel} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 16,
},
text: { fontSize: 18, marginVertical: 10 },
});Tip: ✅ Always remove listeners in useEffect cleanup to prevent memory leaks.
Step 4: Run the App
npx react-native run-android- Click Get Greeting → emits a random greeting
- Click Get Random Number → emits a random number
- Click Get Battery Level → shows battery percentage
- Click Get Device Model → shows device model
Conclusion
In this tutorial, we:
- Built four Kotlin native modules
- Used EventEmitter to send events from Android → JS
- Used Promises to fetch device info
- Integrated everything with a React Native UI
✅ The app is fully functional and crash-free — a perfect demo of how to connect Android native code with React Native.
Get Ashish Negi’s stories in your inbox
Join Medium for free to get updates from this writer.
⚠️ Note on React Native 0.81.1:
This demo uses the classic React Native bridge (ReactContextBaseJavaModule, @ReactMethod, DeviceEventManagerModule). Even though React Native is moving towards the new architecture (TurboModules + JSI), the classic bridge is still supported in 0.81.1 and is the simplest way to learn native modules.
For production apps, you can later migrate the same logic to TurboModules if needed, but the core concepts remain the same.
Dev Tip: Native modules are extremely useful for platform-specific functionality, like sensors, cameras, or system-level info not exposed by React Native.




