СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

| сохранено

H Пишем нативные модули в React Native в черновиках Tutorial

Зачем нужны нативные модули?


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


• Доступ до возможностей платформы, почитать из контент-провайдеров на 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 циклы, в которых вызывать нативный модуль — перенесите цикл в натив.

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


комментарии (0)