ネイティブモジュール
React Nativeアプリケーションのコードでは、React Nativeや既存のライブラリが提供していないネイティブプラットフォームのAPIとやり取りする必要があるかもしれません。その場合は、Turbo Native Moduleを使用して自分で統合コードを書くことができます。このガイドでは、その書き方を紹介します。
基本的な手順は以下の通りです。
- 広く使われているJavaScriptの型注釈言語であるFlowまたはTypeScriptを使用して、型付けされたJavaScriptの仕様を定義します。
- Codegenを実行するように依存関係管理システムを設定します。Codegenは、仕様をネイティブ言語のインターフェースに変換します。
- 仕様を使用してアプリケーションコードを書きます。
- 生成されたインターフェースを使用してネイティブプラットフォームのコードを書き、ネイティブコードをReact Nativeランタイム環境にフックします。
これらの各ステップを、Turbo Native Moduleの例を作成しながら見ていきましょう。このガイドの残りの部分では、以下のコマンドを実行してアプリケーションを作成済みであることを前提としています。
npx @react-native-community/cli@latest init TurboModuleExample --version 0.76.0
ネイティブ永続ストレージ
このガイドでは、Web Storage APIの一種である `localStorage` の実装方法を紹介します。このAPIは、プロジェクトでアプリケーションコードを書いているReact開発者にとって馴染み深いものです。
これをモバイルで動作させるためには、AndroidとiOSのAPIを使用する必要があります。
- Android: SharedPreferences
- iOS: NSUserDefaults
1. 型付けされた仕様の宣言
React Nativeは、Codegenというツールを提供しています。これは、TypeScriptまたはFlowで書かれた仕様を受け取り、AndroidとiOS向けのプラットフォーム固有のコードを生成します。仕様は、ネイティブコードとReact NativeのJavaScriptランタイム間でやり取りされるメソッドとデータ型を宣言します。Turbo Native Moduleは、仕様、作成するネイティブコード、そして仕様から生成されるCodegenインターフェースのすべてを指します。
仕様ファイルを作成するには
- アプリのルートフォルダ内に、 `specs` という新しいフォルダを作成します。
- `NativeLocalStorage.ts` という新しいファイルを作成します。
仕様で使用できるすべての型と、生成されるネイティブ型は、付録のドキュメントで確認できます。
以下は `localStorage` 仕様の実装です。
- TypeScript
- Flow
import type {TurboModule} from 'react-native';
import {TurboModuleRegistry} from 'react-native';
export interface Spec extends TurboModule {
setItem(value: string, key: string): void;
getItem(key: string): string | null;
removeItem(key: string): void;
clear(): void;
}
export default TurboModuleRegistry.getEnforcing<Spec>(
'NativeLocalStorage',
);
import type {TurboModule} from 'react-native';
import {TurboModule, TurboModuleRegistry} from 'react-native';
export interface Spec extends TurboModule {
setItem(value: string, key: string): void;
getItem(key: string): ?string;
removeItem(key: string): void;
clear(): void;
}
2. Codegenを実行する設定
この仕様は、React Native Codegenツールによって、プラットフォーム固有のインターフェースとボイラープレートを生成するために使用されます。これを行うには、Codegenが仕様をどこで見つけ、それをどう扱うかを知る必要があります。 `package.json` を更新して以下を含めます。
"start": "react-native start",
"test": "jest"
},
"codegenConfig": {
"name": "NativeLocalStorageSpec",
"type": "modules",
"jsSrcsDir": "specs",
"android": {
"javaPackageName": "com.nativelocalstorage"
}
},
"dependencies": {
Codegenの準備が整ったので、生成されたコードにフックするためにネイティブコードを準備する必要があります。
- Android
- iOS
Codegenは `generateCodegenArtifactsFromSchema` Gradleタスクを通じて実行されます。
cd android
./gradlew generateCodegenArtifactsFromSchema
BUILD SUCCESSFUL in 837ms
14 actionable tasks: 3 executed, 11 up-to-date
これはAndroidアプリケーションをビルドする際に自動的に実行されます。
Codegenは、CocoaPodsによって生成されたプロジェクトに自動的に追加されるスクリプトフェーズの一部として実行されます。
cd ios
bundle install
bundle exec pod install
出力は次のようになります。
...
Framework build type is static library
[Codegen] Adding script_phases to ReactCodegen.
[Codegen] Generating ./build/generated/ios/ReactCodegen.podspec.json
[Codegen] Analyzing /Users/me/src/TurboModuleExample/package.json
[Codegen] Searching for codegen-enabled libraries in the app.
[Codegen] Found TurboModuleExample
[Codegen] Searching for codegen-enabled libraries in the project dependencies.
[Codegen] Found react-native
...
3. Turbo Native Moduleを使用してアプリケーションコードを書く
`NativeLocalStorage` を使用して、永続化したいテキスト、入力フィールド、およびこの値を更新するためのいくつかのボタンを含むように変更された `App.tsx` を以下に示します。
`TurboModuleRegistry` はTurbo Native Moduleを取得するために2つのモードをサポートしています。
- `get
(name: string): T | null` は、Turbo Native Moduleが利用できない場合に `null` を返します。 - `getEnforcing
(name: string): T` は、Turbo Native Moduleが利用できない場合に例外をスローします。これはモジュールが常に利用可能であることを前提としています。
import React from 'react';
import {
SafeAreaView,
StyleSheet,
Text,
TextInput,
Button,
} from 'react-native';
import NativeLocalStorage from './specs/NativeLocalStorage';
const EMPTY = '<empty>';
function App(): React.JSX.Element {
const [value, setValue] = React.useState<string | null>(null);
const [editingValue, setEditingValue] = React.useState<
string | null
>(null);
React.useEffect(() => {
const storedValue = NativeLocalStorage?.getItem('myKey');
setValue(storedValue ?? '');
}, []);
function saveValue() {
NativeLocalStorage?.setItem(editingValue ?? EMPTY, 'myKey');
setValue(editingValue);
}
function clearAll() {
NativeLocalStorage?.clear();
setValue('');
}
function deleteValue() {
NativeLocalStorage?.removeItem('myKey');
setValue('');
}
return (
<SafeAreaView style={{flex: 1}}>
<Text style={styles.text}>
Current stored value is: {value ?? 'No Value'}
</Text>
<TextInput
placeholder="Enter the text you want to store"
style={styles.textInput}
onChangeText={setEditingValue}
/>
<Button title="Save" onPress={saveValue} />
<Button title="Delete" onPress={deleteValue} />
<Button title="Clear" onPress={clearAll} />
</SafeAreaView>
);
}
const styles = StyleSheet.create({
text: {
margin: 10,
fontSize: 20,
},
textInput: {
margin: 10,
height: 40,
borderColor: 'black',
borderWidth: 1,
paddingLeft: 5,
paddingRight: 5,
borderRadius: 5,
},
});
export default App;
4. ネイティブプラットフォームのコードを書く
すべての準備が整ったので、ネイティブプラットフォームのコードを書き始めます。これは2つの部分で行います。
このガイドでは、新しいアーキテクチャでのみ動作するTurbo Native Moduleの作成方法を示します。新しいアーキテクチャとレガシーアーキテクチャの両方をサポートする必要がある場合は、後方互換性ガイドを参照してください。
- Android
- iOS
それでは、アプリケーションが閉じられた後も `localStorage` が存続するように、Androidプラットフォームのコードを書いていきましょう。
最初のステップは、生成された `NativeLocalStorageSpec` インターフェースを実装することです。
- Java
- Kotlin
package com.nativelocalstorage;
import android.content.Context;
import android.content.SharedPreferences;
import com.nativelocalstorage.NativeLocalStorageSpec;
import com.facebook.react.bridge.ReactApplicationContext;
public class NativeLocalStorageModule extends NativeLocalStorageSpec {
public static final String NAME = "NativeLocalStorage";
public NativeLocalStorageModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return NAME;
}
@Override
public void setItem(String value, String key) {
SharedPreferences sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putString(key, value);
editor.apply();
}
@Override
public String getItem(String key) {
SharedPreferences sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE);
String username = sharedPref.getString(key, null);
return username;
}
@Override
public void removeItem(String key) {
SharedPreferences sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE);
sharedPref.edit().remove(key).apply();
}
}
package com.nativelocalstorage
import android.content.Context
import android.content.SharedPreferences
import com.nativelocalstorage.NativeLocalStorageSpec
import com.facebook.react.bridge.ReactApplicationContext
class NativeLocalStorageModule(reactContext: ReactApplicationContext) : NativeLocalStorageSpec(reactContext) {
override fun getName() = NAME
override fun setItem(value: String, key: String) {
val sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
val editor = sharedPref.edit()
editor.putString(key, value)
editor.apply()
}
override fun getItem(key: String): String? {
val sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
val username = sharedPref.getString(key, null)
return username.toString()
}
override fun removeItem(key: String) {
val sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
val editor = sharedPref.edit()
editor.remove(key)
editor.apply()
}
override fun clear() {
val sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
val editor = sharedPref.edit()
editor.clear()
editor.apply()
}
companion object {
const val NAME = "NativeLocalStorage"
}
}
次に、 `NativeLocalStoragePackage` を作成する必要があります。これは、モジュールをBase Native Packageとしてラップすることで、React Nativeランタイムに登録するためのオブジェクトを提供します。
- Java
- Kotlin
package com.nativelocalstorage;
import com.facebook.react.BaseReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import java.util.HashMap;
import java.util.Map;
public class NativeLocalStoragePackage extends BaseReactPackage {
@Override
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
if (name.equals(NativeLocalStorageModule.NAME)) {
return new NativeLocalStorageModule(reactContext);
} else {
return null;
}
}
@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
return new ReactModuleInfoProvider() {
@Override
public Map<String, ReactModuleInfo> getReactModuleInfos() {
Map<String, ReactModuleInfo> map = new HashMap<>();
map.put(NativeLocalStorageModule.NAME, new ReactModuleInfo(
NativeLocalStorageModule.NAME, // name
NativeLocalStorageModule.NAME, // className
false, // canOverrideExistingModule
false, // needsEagerInit
false, // isCXXModule
true // isTurboModule
));
return map;
}
};
}
}
package com.nativelocalstorage
import com.facebook.react.BaseReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
class NativeLocalStoragePackage : BaseReactPackage() {
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
if (name == NativeLocalStorageModule.NAME) {
NativeLocalStorageModule(reactContext)
} else {
null
}
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
mapOf(
NativeLocalStorageModule.NAME to ReactModuleInfo(
name = NativeLocalStorageModule.NAME,
className = NativeLocalStorageModule.NAME,
canOverrideExistingModule = false,
needsEagerInit = false,
isCxxModule = false,
isTurboModule = true
)
)
}
}
最後に、メインアプリケーションのReact Nativeにこの `Package` の見つけ方を教える必要があります。これをReact Nativeでのパッケージの「登録」と呼びます。
この場合、getPackages メソッドによって返されるリストに追加します。
後ほど、ネイティブモジュールをnpmパッケージとして配布する方法を学びます。その場合、ビルドツールが自動的にリンクしてくれます。
- Java
- Kotlin
package com.inappmodule;
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;
import com.facebook.react.defaults.DefaultReactHost;
import com.facebook.react.defaults.DefaultReactNativeHost;
import com.facebook.soloader.SoLoader;
import com.nativelocalstorage.NativeLocalStoragePackage;
import java.util.ArrayList;
import java.util.List;
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost reactNativeHost = new DefaultReactNativeHost(this) {
@Override
public List<ReactPackage> getPackages() {
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(new NativeLocalStoragePackage());
return packages;
}
@Override
public String getJSMainModuleName() {
return "index";
}
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
public boolean isNewArchEnabled() {
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
}
@Override
public boolean isHermesEnabled() {
return BuildConfig.IS_HERMES_ENABLED;
}
};
@Override
public ReactHost getReactHost() {
return DefaultReactHost.getDefaultReactHost(getApplicationContext(), reactNativeHost);
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, false);
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
DefaultNewArchitectureEntryPoint.load();
}
}
}
package com.inappmodule
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
import com.nativelocalstorage.NativeLocalStoragePackage
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
add(NativeLocalStoragePackage())
}
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) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
}
}
}
これで、エミュレータでコードをビルドして実行できます。
- npm
- Yarn
npm run android
yarn run android
それでは、アプリケーションが閉じられた後も `localStorage` が存続するように、iOSプラットフォームのコードを書いていきましょう。
Xcodeプロジェクトの準備
Xcodeを使用してiOSプロジェクトを準備する必要があります。この6つのステップを完了すると、生成された `NativeLocalStorageSpec` インターフェースを実装する `RCTNativeLocalStorage` が完成します。
- CocoaPodsで生成されたXcodeワークスペースを開きます。
cd ios
open TurboModuleExample.xcworkspace

- appを右クリックし、 `New Group` を選択し、新しいグループに `NativeLocalStorage` という名前を付けます。

- `NativeLocalStorage` グループで、 `New`→`File from Template` を作成します。

- `Cocoa Touch Class` を使用します。

- Cocoa Touch Classに `RCTNativeLocalStorage` という名前を付け、言語は `Objective-C` を選択します。

- `RCTNativeLocalStorage.m` → `RCTNativeLocalStorage.mm` に名前を変更し、Objective-C++ファイルにします。

NSUserDefaultsでlocalStorageを実装
まず、 `RCTNativeLocalStorage.h` を更新します。
// RCTNativeLocalStorage.h
// TurboModuleExample
#import <Foundation/Foundation.h>
#import <NativeLocalStorageSpec/NativeLocalStorageSpec.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTNativeLocalStorage : NSObject
@interface RCTNativeLocalStorage : NSObject <NativeLocalStorageSpec>
@end
次に、カスタムスイート名を使用して `NSUserDefaults` を使うように実装を更新します。
// RCTNativeLocalStorage.m
// TurboModuleExample
#import "RCTNativeLocalStorage.h"
static NSString *const RCTNativeLocalStorageKey = @"local-storage";
@interface RCTNativeLocalStorage()
@property (strong, nonatomic) NSUserDefaults *localStorage;
@end
@implementation RCTNativeLocalStorage
- (id) init {
if (self = [super init]) {
_localStorage = [[NSUserDefaults alloc] initWithSuiteName:RCTNativeLocalStorageKey];
}
return self;
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
return std::make_shared<facebook::react::NativeLocalStorageSpecJSI>(params);
}
- (NSString * _Nullable)getItem:(NSString *)key {
return [self.localStorage stringForKey:key];
}
- (void)setItem:(NSString *)value
key:(NSString *)key {
[self.localStorage setObject:value forKey:key];
}
- (void)removeItem:(NSString *)key {
[self.localStorage removeObjectForKey:key];
}
- (void)clear {
NSDictionary *keys = [self.localStorage dictionaryRepresentation];
for (NSString *key in keys) {
[self removeItem:key];
}
}
+ (NSString *)moduleName
{
return @"NativeLocalStorage";
}
@end
注意すべき重要な点
- Xcodeを使用してCodegenの `@protocol NativeLocalStorageSpec` にジャンプできます。また、Xcodeを使用してスタブを生成することもできます。
アプリにネイティブモジュールを登録
最後のステップは、 `package.json` を更新して、ネイティブモジュールのJS仕様と、ネイティブコードでのその仕様の具体的な実装との間のリンクをReact Nativeに伝えることです。
`package.json` を以下のように変更します。
"start": "react-native start",
"test": "jest"
},
"codegenConfig": {
"name": "AppSpecs",
"type": "modules",
"jsSrcsDir": "specs",
"android": {
"javaPackageName": "com.sampleapp.specs"
}
"ios": {
"modulesProvider": {
"NativeLocalStorage": "RCTNativeLocalStorage"
}
},
},
"dependencies": {
この時点で、新しいファイルを生成するためにcodegenが再度実行されるように、podsを再インストールする必要があります。
# from the ios folder
bundle exec pod install
open SampleApp.xcworkspace
これでXcodeからアプリケーションをビルドすれば、正常にビルドできるはずです。
シミュレータでコードをビルドして実行
- npm
- Yarn
npm run ios
yarn run ios