Разработка Native Module для React Native-приложения (Android)
Native Module нужен тогда, когда JavaScript не может дотянуться до нужного Android API: Bluetooth LE, NFC, специфичный SDK от вендора оборудования, работа с файловой системой ниже уровня что даёт react-native-fs. В 2024 году React Native предлагает два пути: старая архитектура (Bridge) и новая (JSI + TurboModules). Проект, созданный с react-native init версии 0.74+, по умолчанию использует новую архитектуру.
Старая архитектура: Bridge-модуль
Для проектов на 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 падает с десятков мс до единиц.
Передача событий из нативного кода в JS
Паттерн один для обеих архитектур — 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 — обязательно. Утечка подписки приводит к вызову обработчика после размонтирования компонента и потенциальному обновлению размонтированного state.
Работа с разрешениями внутри модуля
Нативный модуль не должен самостоятельно запрашивать рантайм-разрешения — это ответственность 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(). Проверить через Metro bundler логи и adb logcat.
Вызов нативного метода из фонового потока обновляет UI. React Native bridge и JSI — не main thread. Если внутри @ReactMethod обновляете какой-то Android UI-элемент — нужен 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 — индивидуально. Стоимость рассчитывается после анализа требований.







