All Articles

React NativeからNative Moduleを呼び出す(iOS & Android)

はじめに

マルチプラットフォーム開発していくなかで、React Nativeですべて完結できるとは考えておらず、Native機能を使う機会はありそうなので、Android (Java) + iOS(Swift)のNative Moduleをどのように呼び出す調べてみました。

すごくシンプルに、インクリメントとデクリメントの2つの関数を 持つカウンターをネイティブ側に実装していきます。

ベースとなるプロジェクトは、下記のコマンドで作成されたものです。

npx react-native init AwesomeTSProject --template react-native-template-typescript

TypeScript側の変更部分コード

今回は例として、Increment側はCallbackを使い、Decrement側はPromiseで実装しています。 ReactはHooksを使い実装しています。

const App = () => {
  // ↓----追加----↓
  const [count, setCount] = useState(0);
  const [errorText, setErrorText] = useState('');

  const onPressIncrement = () => {
    const callback = (incrementedCount: number) => {
      setCount(incrementedCount);
      setErrorText('');
    };
    NativeModules.Counter.increment(count, callback);
  };

  const onPressDecrement = () => {
    NativeModules.Counter.decrement(count)
      .then((decrementedCount: number) => setCount(decrementedCount))
      .catch((error: any) => {
        setErrorText(error.message);
      });
  };
  // ↑----追加----↑

  return (
    <>
    (省略)
  // ↓----追加----↓
            <Text style={styles.padding}>{count}</Text>
            <Text style={styles.waringText}>{errorText}</Text>
            <Button
              onPress={onPressIncrement}
              title="Increment(+)"
              style={styles.padding}
            />
            <Button
              onPress={onPressDecrement}
              title="Decrement(-)"
              style={styles.padding}
            />
  // ↑----追加----↑
          </View>
        </ScrollView>
      </SafeAreaView>
    </>
  );
};

const styles = StyleSheet.create({
  scrollView: {
    backgroundColor: Colors.lighter,
  },
    (省略)
  // ↓----追加----↓
  padding: {
    padding: 10,
  },
  waringText: {
    fontSize: 24,
    fontWeight: '600',
    color: 'red',
  },
  // ↑----追加----↑
});

AndroidのNative Module

Native Modules Android ※公式に細かく書かれていますが、ここではコードをメインに書きます。

新規作成するクラス

  • CounterModule
  • CounterPackage

変更するクラス

  • MainApplication

CounterModule

package com.learningreactnative;

import android.util.Log;

import androidx.annotation.NonNull;

import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

public class CounterModule extends ReactContextBaseJavaModule {
    public CounterModule(ReactApplicationContext reactApplicationContext) {
        super(reactApplicationContext);
    }

    @ReactMethod
    public void increment(int origin, Callback callback) {
        int count = origin + 1;
        Log.d("DEBUG", String.format("---------- count is (%s)", count));
        callback.invoke(count);
    }

    @ReactMethod
    public void decrement(int origin, Promise promise) {
        if (origin == 0) {
            promise.reject("E_COUNT", "You can't decrement any more!!");
        } else {
            int count = origin - 1;
            Log.d("DEBUG", String.format("---------- count is (%s)", count));
            promise.resolve(count);
        }
    }

    @NonNull
    @Override
    public String getName() {
        return "Counter";
    }
}

IncrementとDecrementは現在のカウンターの数(Origin)とCallback or Promiseの2つの引数を受け取る。

CounterPackage

package com.learningreactnative;

import androidx.annotation.NonNull;

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

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CounterPackage implements ReactPackage {
    @NonNull
    @Override
    public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new CounterModule(reactContext));
        return modules;
    }

    @NonNull
    @Override
    public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }
}

今回はView部分は作成しないので emptyList() 上で作成した、Moduleを配列に追加。

MainApplication

React Nativeから呼び出せるようにPackage追加

         @Override
         protected List<ReactPackage> getPackages() {
           @SuppressWarnings("UnnecessaryLocalVariable")
           List<ReactPackage> packages = new PackageList(this).getPackages();
+           packages.add(new CounterPackage());
           return packages;
         }

iOSのNative Module

React Nativeプロジェクト内のiOSコードはObjective-Cで構成されています。 ただ、現在はObjective-Cで書くメリットはなく、Swiftでコードを書くほうが 可読性も高いので、Objective-CからSwiftコードを呼び出す形で実装していきます。

追加ファイルは、/ios直下に配置しないとコンパイルエラーとなります。

新規作成クラス

  • ios/Counter.m
  • ios/Counter.swift
  • ios/AwesomeTSProject-Bridging-Header.h

ios/Counter.m

Swiftで書いたコードをReact Native側から実行できるIF Objective-Cの文法で記載。

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

@interface RCT_EXTERN_MODULE(Counter, NSObject)
RCT_EXTERN_METHOD(increment: (NSInteger)origin withCallback:(RCTResponseSenderBlock)callback)
RCT_EXTERN_METHOD(decrement: (NSInteger)origin withResolve:(RCTPromiseResolveBlock)resolve withReject:(RCTPromiseRejectBlock)reject)
@end

ios/Counter.swift

incrementと、decrementの関数を追加する。

import Foundation

@objc(Counter)
class Counter: NSObject {
  static func requiresMainQueueSetup() -> Bool {
    return false
  }
  
  @objc
  func increment(_ origin: Int, withCallback callback: RCTResponseSenderBlock) -> Void {
    callback([origin + 1])
  }
  
  @objc
  func decrement(_ origin: Int, withResolve resolve: RCTPromiseResolveBlock, withReject reject: RCTPromiseRejectBlock) -> Void{
    if(origin <= 0){
      reject("E_COUNT", "You can't decrement any more!!", nil)
      return
    }
    resolve(origin - 1)
  }
  
}

ios/AwesomeTSProject-Bridging-Header.h

Swiftコードから、RCTBridgeModuleを呼び出せるようにするIFファイル

#import "React/RCTBridgeModule.h"