Розробка Native Module для React Native-додатків (Android)
Native Module потрібен, коли JavaScript не може дістатися потрібного Android API: Bluetooth LE, NFC, SDKs від вендора обладнання, операції з файловою системою нижче того, що пропонує react-native-fs. У 2024 році React Native пропонує два шляхи: стару архітектуру (Bridge) та нову (JSI + TurboModules). Проекти, створені з react-native init версії 0.74+, за замовчуванням використовують нову архітектуру.
Стара архітектура: Bridge Module
Для проектів на RN < 0.68 або з вимкненою новою архітектурою:
// 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)
}
// Надіслання подій у JS
private fun sendEvent(eventName: String, params: WritableMap?) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(eventName, params)
}
}
Реєстрація через Package:
class BluetoothPackage : ReactPackage {
override fun createNativeModules(context: ReactApplicationContext) =
listOf(BluetoothModule(context))
override fun createViewManagers(context: ReactApplicationContext) = emptyList<ViewManager<*, *>>()
}
Додайте до MainApplication.kt у методу getPackages().
Нова архітектура: TurboModule + JSI
З RN 0.74+ та newArchEnabled=true реалізуйте модуль як TurboModule через Codegen. По-перше, специфікація TypeScript:
// NativeBluetoothModule.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
isBluetoothEnabled(): Promise<boolean>;
startScan(): Promise<void>;
addListener(eventType: string): void;
removeListeners(count: number): void;
}
export default TurboModuleRegistry.getEnforcing<Spec>('BluetoothModule');
Codegen генерує NativeBluetoothModuleSpec.kt з цієї специфікації. Нативне впровадження спадкує генерований абстрактний клас:
class BluetoothModule(context: ReactApplicationContext) :
NativeBluetoothModuleSpec(context) {
override fun getName() = NAME
override fun isBluetoothEnabled(): Promise<Boolean> {
// впровадження ідентичне варіанту bridge
}
companion object {
const val NAME = "BluetoothModule"
}
}
Ключова різниця: TurboModule викликається безпосередньо через JSI без серіалізації через JSON-міст. Для високочастотних викликів (аудіопотік, датчики) це критично—latency падає з десятків мс до одиниць.
Надіслання подій з нативного коду в JavaScript
Паттерн однаковий для обох архітектур—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)
}
На стороні JS:
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();
}, []);
subscription.remove() у cleanup useEffect є обов'язковим. Витоки підписок призводять до виклику обробника після розмонтування компонента й потенційного оновлення стану розмонтованого компонента.
Обробка дозволів всередину модуля
Нативний модуль не повинен самостійно запитувати дозволи під час виконання—це відповідальність шару JS через react-native-permissions або вбудований PermissionsAndroid. Модуль лише перевіряє наявність дозволу й повертає відповідну помилку:
@ReactMethod
fun startScan(promise: Promise) {
if (ActivityCompat.checkSelfPermission(
reactContext,
Manifest.permission.BLUETOOTH_SCAN
) != PackageManager.PERMISSION_GRANTED) {
promise.reject("PERMISSION_DENIED", "BLUETOOTH_SCAN permission required")
return
}
// продовження
}
Відлагодження та поширені помилки
Модуль не знайдено: null is not an object (evaluating 'NativeModules.BluetoothModule.isBluetoothEnabled')—Package не додано до getPackages() або помилка у getName(). Перевірте журналів bundler Metro та adb logcat.
Виклик нативного методу з фонового потоку оновлює UI. React Native bridge та JSI — це не головний потік. Якщо ви оновлюєте якийсь Android UI-елемент всередину @ReactMethod, використовуйте Handler(Looper.getMainLooper()).post { ... }.
WritableMap після передачі в promise більше не можна використовувати. Arguments.createMap() створює об'єкт на одноразове використання. Після promise.resolve(map) цей об'єкт більше не дійсний—повторне читання викликає IllegalStateException.
Сумісність старої та нової архітектур. Доки проект повністю не перейде на нову архітектуру, модуль повинен підтримувати обидві. ReactPackage залишається, TurboModule-впровадження додається поверх. У build.gradle використовуйте умовну компіляцію з isNewArchEnabled.
Тестування
Нативні модулі тестуються на рівні Kotlin (JUnit + Mockk для ReactApplicationContext) та на рівні інтеграції через Detox або Maestro. Ізольований тест bridge-методу:
@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) }
}
Розробка Native Module: базовий модуль з 3–5 методами займає 2–4 дні. Модуль з подіями, підтримкою обох архітектур та тестами займає тиждень і більше. Інтеграція важкого SDK вендора розраховується індивідуально. Вартість визначається після аналізу вимог.







