Как стать автором
Обновить

Когда JavaScript недостаточно: Практика разработки нативных модулей для React Native

Уровень сложностиСредний
Время на прочтение9 мин
Количество просмотров1.9K

Эта статья для React Native-разработчиков, которым не хватает возможностей JavaScript. Вы научитесь писать нативные модули на Kotlin, чтобы работать с NFC, Bluetooth, системными API и другими «закрытыми» для RN функциями. Готовый пример + объяснение каждой строки кода.

Приветствую, на связи «Ваш код еще не готов, сэр» и сегодня мы поговорим о нативных модулях в React Native приложениях. Почему я люблю javascript? Перефразирую известную фразу:

«Один код, чтоб править всеми, он главнее всех, он соберёт всех вместе и заключит во Тьме".»

Да, на js (ts) можно писать удобный и читаемый код для всего и вся или почти всего, в том числе и мобильные приложения под IOS и Android. Конечно, как и у любого языка у него есть свои недостатки, но тут не об этом.

Мы разберём ключевые аспекты разработки нативных модулей для React Native приложений. Но прежде чем погружаться в технические детали, давайте разберемся, что же представляет собой сам React Native.

Украду определение с вики.

React Native — это фреймворк для разработки мобильных приложений, созданный компанией Facebook. Он позволяет разработчикам создавать приложения для iOS и Android, используя JavaScript и React, что делает его популярным выбором для кроссплатформенной разработки.

Что ж, точнее и не скажешь.

Звучит здорово? Да. Но порой, разработчики сталкиваются с некоторыми ограничениями. Например нам нужно получить:

 1. Сканирование NFC-меток
 2. Работа с Bluetooth Low Energy
 3. Доступ к датчикам (гироскоп, барометр)
 4. Фоновые сервисы

Добиться этого механизмами RN (react native) нельзя. Вот тут на сцену выходят нативные модули. И что вообще такое нативный код и нативные модули?

Нативный код — это код, который написан на языках программирования, специфичных для определенной платформы или операционной системы. Например, для разработки приложений под iOS используется язык Swift или Objective-C, а для Android — Java или Kotlin. Нативный код выполняется непосредственно на устройстве, что обеспечивает высокую производительность и доступ к полному набору возможностей операционной системы.

Нативные модули в контексте React Native — это модули, написанные на нативных языках (например, Java для Android или Swift/Objective-C для iOS), которые позволяют разработчикам использовать функциональность, недоступную через стандартные компоненты React Native.

Теперь когда мы разобрались что и зачем нужно, приступим к написанию модуля. Модули будут написаны на kotlin, т. к. начиная с версии 0.74 это предпочтительнее.

Но прежде чем мы начнем, оговорюсь, что важно где мы размещаем создаваемый модуль. Поэтому рассмотрим структуру проекта, и разместим вновь разрабатываемый модуль в правильном месте:


my-react-native-app/
├── android/
│   ├── app/
│   │   ├── src/
│   │   │   ├── main/
│   │   │   │   ├── java/
│   │   │   │   │   └── com/
│   │   │   │   │       └── myreactnativeapp/
│   │   │   │   │           ├── MainActivity.kt
│   │   │   │   │           ├── MainApplication.kt
│   │   │   │   │           └── <your_native_module>.kt
│   │   │   │   ├── res/
│   │   │   │   └── AndroidManifest.xml
│   │   └── build.gradle
│   ├── build.gradle
│   └── settings.gradle
└── package.json

Рис. 1 Структура проекта

После того, как мы определились со структурой каталогов, приступим к написанию самого модуля

package com.myreactnativeapp

import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.Settings
import androidx.core.app.NotificationManagerCompat
import com.facebook.react.bridge.*

class NotificationStatusModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
    override fun getName(): String = "NotificationStatus"

    @ReactMethod
    fun checkFullNotificationStatus(promise: Promise) {
        try {
            val result = Arguments.createMap()
            val appEnabled = NotificationManagerCompat.from(reactContext).areNotificationsEnabled()
            result.putBoolean("appEnabled", appEnabled)

            val notificationManager = reactContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            val isDndEnabled = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                val filter = notificationManager.currentInterruptionFilter
                filter == NotificationManager.INTERRUPTION_FILTER_NONE || 
                filter == NotificationManager.INTERRUPTION_FILTER_ALARMS
            } else false
            
            result.putBoolean("doNotDisturb", isDndEnabled)
            promise.resolve(result)
        } catch (e: Exception) {
            promise.reject("ERROR", "Failed to check notification status", e)
        }
    }

    @ReactMethod
    fun openNotificationSettings() {
        val intent = Intent().apply {
            action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
            putExtra(Settings.EXTRA_APP_PACKAGE, reactContext.packageName)
            flags = Intent.FLAG_ACTIVITY_NEW_TASK
        }
        reactContext.startActivity(intent)
    }
}

Рис. 2 Модуль проверки статуса уведомлений на Android

На что тут следует обратить внимание? Для написания своего модуля, сперва необходимо унаследоваться от ReactContextBaseJavaModule. Это базовый класс для нативных модулей RN.
Данный класс принимает ReactApplicationContext - контекст React Native приложения. Контекст выступает связующим звеном между нативным кодом (Java или Kotlin) и JavaScript-кодом. Это позволяет вам вызывать методы JavaScript из нативного кода и наоборот.

Рассмотрим основные особенности работы класса NotificationStatusModule

  • Функция getName() возвращает имя модуля ("NotificationStatus"), по которому он будет доступен в JS

  • Функция проверки статуса уведомлений - checkFullNotificationStatus:

val result = Arguments.createMap()

Рис. 3 Создается новый объект

Создает WritableMap - специальный тип React Native для передачи данных в JS

val appEnabled = NotificationManagerCompat.from(reactContext).areNotificationsEnabled()
result.putBoolean("appEnabled", appEnabled)

Рис. 4 Создание экземпляра NotificationManagerCompat

Метод areNotificationsEnabled() проверяет, включены ли уведомления для приложения.
Он возвращает true, если уведомления разрешены для приложения, и false, если пользователь отключил их в настройках

val notificationManager = reactContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val isDndEnabled = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    val filter = notificationManager.currentInterruptionFilter
    filter == NotificationManager.INTERRUPTION_FILTER_NONE || 
    filter == NotificationManager.INTERRUPTION_FILTER_ALARMS
} else false

Рис. 5 Получение состояния режима "Не беспокоить"

Код проверяет версию Android, чтобы убедиться, что он работает только на API 23 (Android 6.0, Marshmallow) и выше.
Затем он анализирует currentInterruptionFilter, который определяет, как обрабатываются уведомления:

INTERRUPTION_FILTER_NONE означает, что все уведомления отключены (полное отключение).

INTERRUPTION_FILTER_ALARMS означает, что разрешены только будильники.

Для старых версий Android (до API 23) код возвращает false, так как функция currentInterruptionFilter недоступна.

result.putBoolean("doNotDisturb", isDndEnabled)
promise.resolve(result)

Рис. 6 Передача состояния DND в результат

Формирует объект с двумя полями:

{
appEnabled: boolean, // Включены ли уведомления для приложения
doNotDisturb: boolean // Активен ли режим "Не беспокоить"
}

  • Метод openNotificationSettings:

val intent = Intent().apply {
    action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
    putExtra(Settings.EXTRA_APP_PACKAGE, reactContext.packageName)
    flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
reactContext.startActivity(intent)

Рис. 7 Открытие настроек уведомлений приложения

Открывает настройки уведомлений конкретно для вашего приложения
Использует стандартный Intent Android с параметрами:

ACTION_APP_NOTIFICATION_SETTINGS - специальное действие для настроек уведомлений

EXTRA_APP_PACKAGE - указывает какое приложение настроить

FLAG_ACTIVITY_NEW_TASK - запускает новую задачу

После создания модуля, необходимо создать «пакет»

Зачем нужен отдельный пакет? Модуль содержит бизнес-логику (NotificationStatusModule), а пакет - это фабрика, которая знает как создавать модуль.

package com.myreactnativeapp

import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager

class NotificationStatusPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(NotificationStatusModule(reactContext))
}

override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return emptyList()
}
}

Рис. 8 Код пакета

Пакет позволяет:

 1. Объединять несколько модулей в один пакет:
 2. Контролировать процесс инициализации
 3. Добавлять кастомную логику создания модулей

Что было бы без пакета? React Native не смог бы:

 1. Автоматически обнаружить ваш модуль
 2. Инициализировать его с правильным контекстом
 3. Предоставить его JavaScript-части приложения

Когда можно обойтись без пакета? Технически можно зарегистрировать модуль напрямую через ReactInstanceManager, но это:

 1. Сложнее в поддержке
 2. Нарушает стандартный flow React Native
 3. Может вызвать проблемы с жизненным циклом

Полный цикл работы:

1. React Native ищет все пакеты в getPackages()
2. Для каждого пакета вызывает createNativeModules()
3. Регистрирует полученные модули в NativeModuleRegistry
4. Делает их доступными в JavaScript через NativeModules

Именно поэтому пакет - это обязательный "мост" между вашим модулем и инфраструктурой React Native.

После того, как пакет успешно создан, осталось зарегистрировать его в нашем MainApplication.kt

package com.myreactnativeapp

import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.soloader.SoLoader

class MainApplication : Application(), ReactApplication {
    override val reactNativeHost: ReactNativeHost =
        object : DefaultReactNativeHost(this) {
            override fun getPackages(): List<ReactPackage> =
                PackageList(this).packages.apply {
                    add(NotificationStatusPackage())
                }

            override fun getJSMainModuleName(): String = "index"
            override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
            override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
            override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
        }

    override val reactHost: ReactHost
        get() = getDefaultReactHost(applicationContext, reactNativeHost)

    override fun onCreate() {
        super.onCreate()
        SoLoader.init(this, false)
        if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
            load()
        }
    }
}

Рис. 9 Настройка приложения React Native с поддержкой новых пакетов

В большинтстве случаев, MainApplication уже существует в проекте, в нем необходимо только зарегистрировать вновь созданный пакет. Для этого в метод getPackages добавьте строчку:

add(NotificationStatusPackage())

Таким образом, мы только что добавили новый кастомный пакет в наше приложение.

Но, т. к. мы пытаемся запросить доступ к уведомлениям, требуется добавить необходимые разрешения. Сделать это можно в AndroidManifest.xml добавив 2 строчки кода:

<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

Рис. 10 Запрос разрешений для работы с уведомлениями

И последнее, обязательно убедитесь, что в build.gradle у вас есть строчка

implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"

эта строчка необходима для обеспечения корректной работы вашего приложения, если вы используете Kotlin в своем проекте.

Поздравляю, только что вы написали и добавили свой собственный нативный модуль в react native приложение.

А теперь самое интересно. Да, мы создали новый модуль, но как его использовать? С этим куда проще, все что вам теперь необходимо, это создать обычный компонент.

Рассмотрим класс AndroidNotificationChecker

import { NativeModules, Platform, Linking } from 'react-native';

const NotificationStatus = NativeModules.NotificationStatus;

class AndroidNotificationChecker {
  static async checkFullStatus() {
    if (Platform.OS !== 'android') {
      return {
        appEnabled: false,
        systemEnabled: false,
        doNotDisturb: false,
        error: 'This module works only on Android'
      };
    }

    try {
      const status = await NotificationStatus.checkFullNotificationStatus();
      return status;
    } catch (error) {
      console.error('Error checking notification status:', error);
      return {
        appEnabled: false,
        systemEnabled: false,
        doNotDisturb: false,
        error: error.message
      };
    }
  }

  static openNotificationSettings() {
    NotificationStatus.openNotificationSettings();
  }

  static openSystemNotificationSettings() {
    NotificationStatus.openSystemNotificationSettings();
  }
}

export default AndroidNotificationChecker;

Рис. 11 Компонент AndroidNotificationChecker

Это классовый компонент, который использует наш только что написанный нативный модуль. Теперь вы можете использовать функции этого компонента, для проверки разрешений уведомлений.

Заключение

Нативные модули открывают перед React Native-разработчиками поистине безграничные возможности. Техника, которую мы разобрали на примере работы с уведомлениями, может быть применена для интеграции практически любой нативной функциональности, недоступной "из коробки" в React Native. Это мощный инструмент, который делает ваш гибридный код по-настоящему нативным. Используйте с умом!

Если вам понравилась эта статья, то вы знаете что нужно делать — подписывайтесь, ставьте лайки и делайте репосты. Это лучшая поддержка для автора. С вами был Дубовицкий Юрий, автор канала «Ваш код еще не готов, сэр».
А наш код уже готов. До связи!

Теги:
Хабы:
+3
Комментарии0

Публикации

Работа

Ближайшие события