Как использовать все возможности мобильной ОС в React Native

  • Tutorial

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


В статье я развенчаю этот миф и расскажу о механизме в React Native, который позволяет сделать все, на что способно нативное приложение. Этот механизм – нативные модули. Под катом – подробное описание, как создавать нативные модули для Android и iOS.


image


Нативные модули в кроссплатформенной разработке под мобильники помогают сделать несколько вещей:


  • Предоставить доступ к возможностям платформы, почитать из контент-провайдеров на Android или адресную книгу на iOS
  • Обернуть стороннюю библиотеку для вызова в js
  • Обернуть уже существующий код при добавлении в приложение частей на React Native
  • Реализовать части, критические к производительности(н-р шифрование)

Примерная схема приложения на React Native


В операционной системе запущено нативное приложение. В нем на низком уровне работают рантайм React Native и код нативных модулей, созданных разработчиком приложения (или автором библиотек для React Native). Выше уровнем работает React Native Bridge – промежуточное звено между нативным кодом и js. Сам js исполняется внутри JS VM, чью роль исполняет JavaScriptCore. На iOS она предоставляется системой, на Android же приложение тащит ее в виде библиотеки.


image


Пишем нативный модуль


Под Android


План такой:


  • Зарегистрировать пакет в ReactNativeHost
  • Создать пакет
  • Создать модуль
  • Зарегистрировать модуль в пакете
  • Создать метод в модуле

Лирическое отступление 1 – Компоненты Android


Если ваш основной бэкграунд — Android, это отступление можно пропустить. Для разработчиков с основным опытом iOS или React JS нужно узнать, что приложение под Android может содержать следующие компоненты:


  • Activity
  • BroadcastReceiver
  • Service
  • ContentProvider
  • Application

В этом контексте (кхе-кхе) нас, конечно, интересует только Application. Напомню, что этот компонент и есть обьект самого приложения. Вы можете (а для React Native приложения и должны) реализовать свой класс приложения и реализовать этим классом интерфейс ReactApplication:


package com.facebook.react;

public interface ReactApplication {

  ReactNativeHost getReactNativeHost();
}

Нужно это, чтобы ReactNative узнал о тех нативных пакетах, которые вы хотите использовать. Для этого наш Application должен вернуть экземпляр ReactNativeHost, в котором перечислить список пакетов:


class MainApplication : Application(), ReactApplication {

private val mReactNativeHost = object : ReactNativeHost(this) {

       override fun getPackages(): List<ReactPackage> {
             return Arrays.asList(
                  MainReactPackage(),
                  NativeLoggerPackage()
             )
      }

     override fun getReactNativeHost(): ReactNativeHost {
            return mReactNativeHost
     }
}

NativeLoggerPackage — тот пакет, который мы будем с вами писать. Он будет только логировать переданные в него значения, а мы сконцентрируемся на процессе создания нативного модуля вместо фактической функциональности.


Зачем нужно, чтобы Application реализовывал ReactApplication? Потому что внутри React Native есть вот такой веселый код:


public class ReactActivityDelegate {

  protected ReactNativeHost getReactNativeHost() {
    return ((ReactApplication) getPlainActivity().getApplication())
                                .getReactNativeHost();
  }

}

image


Теперь реализуем NativeLoggerPackage:


class NativeLoggerPackage : ReactPackage {

     override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> 
     {
          return Arrays.asList<NativeModule>(NativeLoggerModule())
     }

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

Метод createViewManagers мы опустим, нам он в этой статье неважен. А важен метод createNativeModules, который должен вернуть список созданных модулей. Модули – это классы, содержащие методы, которые можно вызвать из js. Давайте создадим NativeLoggerModule:


class NativeLoggerModule : BaseJavaModule() {

      override fun getName(): String {
            return "NativeLogger"
      }
}

Модуль должен наследоваться как минимум от BaseJavaModule, если вам не нужен доступ к контексту Android. Если же в нем есть нужда, необходимо использовать другой базовый класс:


class NativeLoggerModule(context : ReactApplicationContext) 
                                                : ReactContextBaseJavaModule(context) {

    override fun getName(): String {
          return "NativeLogger"
    }
}

В любом случае, необходимо определить метод getName(), который вернет имя, под которым ваш модуль будет доступен в js, мы увидим это чуть позже.
Теперь давайте наконец создадим метод для js. Делается это с помощью аннотации ReactMethod:


class NativeLoggerModule : BaseJavaModule() {

     override fun getName(): String {
           return "NativeLogger"
     }

     @ReactMethod
     fun logTheObject() {
           Log.d(name, “Method called”)
     }
}

Здесь метод logTheObject становится доступен для вызова из js. Но вряд ли мы хотим просто вызывать методы без параметров, которые ничего не возвращают. Давайте разбираться с аргументами (слева java-типы, справа js):


Boolean -> Bool
Integer -> Number
Double -> Number
Float -> Number
String -> String
Callback -> function
ReadableMap -> Object
ReadableArray -> Array


Предположим, что в нативный метод мы хотим передать js-обьект. В java будет приходить ReadableMap:


@ReactMethod
fun logTheObject(map: ReadableMap) {
     val value = map.getString("key1")
     Log.d(name, "key1 = " + value)
}

В случае массива будет передаваться ReadableArray, итерация по которому не составляет проблем:


@ReactMethod
fun logTheArray(array: ReadableArray) {
      val size = array.size()
      for (index in 0 until size) {
          val value = array.getInt(index)
          Log.d(name, "array[$index] = $value")
      }
}

Впрочем, если вы хотите передать первым аргументом обьект, а вторым массив, то тут тоже без сюрпризов:


@ReactMethod
fun logTheMapAndArray(map: ReadableMap, array: ReadableArray): Boolean {
     logTheObject(map)
     logTheArray(array)
     return true
}

Как же это добро вызывать из javascript? Нет ничего проще. Первым делом нужно импортнуть NativeModules из корневой библиотеки react-native:


import { NativeModules } from 'react-native';

А затем заимпортить наш модуль(помните, мы назвали его NativeLogger ?):


import { NativeModules } from 'react-native';
const nativeModule = NativeModules.NativeLogger;

Теперь можно вызывать метод:



import { NativeModules } from 'react-native';
const nativeModule = NativeModules.NativeLogger;

export const log = () => {
    nativeModule.logTheMapAndArray(
        { key1: 'value1' },
        ['1', '2', '3']
    );

};

Работает! Но постойте, хочется же знать, все ли в порядке, удалось ли записать то, что мы хотели записать. Как насчет возвращаемых значений?


image


А вот возвращаемых значений у функций из нативных модулей и нет. Придется выкручиваться, передавая функцию:


@ReactMethod
fun logWithCallback(map: ReadableMap, array: ReadableArray, callback: Callback) {
      logTheObject(map)
      logTheArray(array)
      callback.invoke("Logged")
}

В нативный код будет приходить интерфейс Callback с единственным методом invoke(Object… args). Со стороны js — это просто функция:



import { NativeModules } from 'react-native';
const nativeModule = NativeModules.NativeLogger;

export const log = () => {
    const result = nativeModule.logWithCallback(
        { key1: 'value1' },
        [1, 2, 3],
        (message) => { console.log(`[NativeLogger] message = ${message}`) }
    );
};

К сожалению, в compile-time нет инструментов сверить параметры коллбэка из нативного кода и функции в js, будьте внимательны.


К счастью, можно пользоваться механизмом промисов, которые в нативном коде поддерживаются интерфейсом Promise:


@ReactMethod
fun logAsync(value: String, promise: Promise) {
     Log.d(name, "Logging value: " + value)

     promise.resolve("Promise done")
}

Тогда вызывать этот код можно используя async/await:



import { NativeModules } from 'react-native';
const nativeModule = NativeModules.NativeLogger;

export const log = async () => {
    const result = await nativeModule.logAsync('Logged value');
    console.log(`[NativeModule] results = ${result}`);
};

На этом работа по выставлению нативного метода в js в Android завершена. Смотрим на iOS.


image


Создание нативного модуля в iOS


Первым делом создаем модуль NativeLogger.h :


#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>

@interface NativeLogger : NSObject<RCTBridgeModule>

@end

и его реализацию NativeLogger.m:


#import <Foundation/Foundation.h>
#import "NativeLogger.h"

@implementation NativeLogger {

}

RCT_EXPORT_MODULE();

RCT_EXPORT_MODULE — это макрос, который регистрирует наш модуль в ReactNative под именем файла, в котором обьявлен. Если это имя в js для вас не очень подходит, вы можете его поменять:


@implementation NativeLogger {

}

RCT_EXPORT_MODULE(NativeLogger);

Теперь давайте реализуем методы, которые делали для Android. Для этого нам понадобятся параметры.


string -> (NSString*)
number -> (NSInteger*, float, double, CGFloat*, NSNumber*)
boolean -> (BOOL, NSNumber*)
array -> (NSArray*)
object -> (NSDictionary*)
function -> (RCTResponseSenderBlock)

Для обьявления метода можно использовать макрос RCT_EXPORT_METHOD:


RCT_EXPORT_METHOD(logTheObject:(NSDictionary*) map)
{
  NSString *value = map[@"key1"];
  NSLog(@"[NativeModule] %@", value);
}

RCT_EXPORT_METHOD(logTheArray:(NSArray*) array)
{
  for (id record in array) {
    NSLog(@"[NativeModule] %@", record);
  }
}

RCT_EXPORT_METHOD(log:(NSDictionary*) map 
                                withArray:(NSArray*)array 
                                andCallback:(RCTResponseSenderBlock)block)
{
  NSLog(@"Got the log");
  NSArray* events = @[@"Logged"];
  block(@[[NSNull null], events]);
}

Самое интересное тут — поддержка промисов. Для этого придется воспользоваться другим макросом RCT_REMAP_METHOD, который первым аргументом принимает имя метода для js, а вторым и последующими — уже сигнатуру метода в objective-c.
Вместо интерфейса тут передаются два аргумента, RCTPromiseResolveBlock для резолва промиса и RCTPromiseRejectBlock для реджекта:


RCT_REMAP_METHOD(logAsync,
                 logAsyncWith:(NSString*)value
                 withResolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
  NSLog(@"[NativeModule] %@", value);
  NSArray* events = @[@"Logged"];
  resolve(events);
}

На этом все. Механизм передачи событий из нативных модулей в js мы рассмотрим в отдельной статье.


Нюансы


  • Помните, что основная идея нативных модулей — абстракция операционной системы для кроссплатформенного кода. Это значит, что интерфейс модуля должен быть согласован между Android и iOS. Автоматических средств, как это контролировать я, к сожалению, не знаю.


  • Помните, что по отдельности js и нативный код работают быстро. Бриджинг же между ними относительно медленный. Не стоит писать js циклы, в которых вызывать нативный модуль — перенесите цикл в натив.

Полезные ссылки


  • +13
  • 6.1k
  • 5
EPAM
157.95
Компания для карьерного и профессионального роста
Share post

Comments 5

    0
    Огромное спасибо за статью.
    Я всегда задаюсь вопросом: возможна ли подмена уже определенного нативного модуля в React Native (DatePickerIOS к примеру) для использования private API и т.п.?
    В официальной документации по нативным модулям упоминается механизм Dependency Injection, но как им пользоваться толком не описано. Очень бы хотелось увидеть информацию по этому поводу
      0
      Да, в React Native предусмотрена такая функциональность, смотрите метод canOverrideExistingModule.
        0
        Как я понимаю это метод для Java. Что на счет ObjC?
      0

      Это так и должно быть, что в нативном коде promise передаётся вторым (последним) аргументом, а в js Promise возвращается из функции?

        0
        Да, так и должно быть. Весь код в статье проверен на эмуляторах/симуляторах.

      Only users with full accounts can post comments. Log in, please.