Expo ModulesとConfig pluginsでサードパーティのSDKをReact Nativeのアプリに組み込む
こんにちは!テラーノベルでiOS/Android/Webとフロントエンド周りを担当している @kazutoyoです!
Expoでアプリケーションを開発している際、React NativeのサポートがないサードパーティのSDKを導入したいといった要件がたまにあると思います。
React Nativeでアプリケーションを開発する際、ネイティブSDKの導入が必要になることがあります。
そんなときは、Expo ModulesとConfig pluginsを活用することで、CNG(Continuous Native Generation)の恩恵を受けながら、ネイティブコードを直接編集せずにSDKを導入できます。
CNGとは?
Continuous Native Generation (CNG)は、設定値からios/
や android/
ディレクトリなど、ネイティブのプロジェクトファイルを生成する仕組みです。
これにより、開発者はネイティブ層を意識せずに設定を行い、プロジェクトをクリーンに保つことができます。
より詳細な説明はitomeさんの記事やIchikiさんのスライドがとても参考になります。
Expo ModulesとConfig pluginsの役割
簡単にまとめると、次のような役割となっています。
- Expo Modules: React Nativeからネイティブ機能を呼び出すためのモジュール
- Config plugins: Expo Modulesに対し、Prebuild時にネイティブプロジェクトへの変更を行うプラグイン
今回のケースのようなサードパーティのSDKを導入する際、APIキーなどの情報をネイティブ機能の呼び出し時に必要になると思います。
Config Pluginsを利用することで、 Info.plist
や AndroidManifest.xml
に追加し、Expo Modulesから参照できるようになります。
Expo ModulesとConfig pluginsを使ってみよう!
では実際に既存のExpoアプリケーションに、SugoiSDK
という架空のSDKを導入する例を通じて、Expo ModulesとConfig Pluginsの使い方を解説します。
Expo Modulesのプロジェクトの作成
今回は sugoi-sdk-wrapper
という名前でExpo Modulesを作成します。
また、今回は既存のアプリケーションに追加する想定のため、 --local
を指定しています。
npx create-expo-module sugoi-sdk-wrapper --local
各ステップの入力を行った後、 modules/
配下にExpo Modulesのプロジェクトが作成されました。
今回はNativeViewを利用しない、Webを対応しないため、 SugoiSdkWrapperView.tsx
/ SugoiSdkWrapperView.web.tsx
/ SugoiSdkWrapperModule.web.ts
の3ファイルを削除し、それらを参照している index.ts
ファイルも修正しましょう。
また、 expo-module.config.json
の platforms
から "web"
の指定も削除します。
{
"platforms": [
"apple",
"android"
],
"apple": {
"modules": [
"SugoiSdkWrapperModule"
]
},
"android": {
"modules": [
"jp.kazutoyo.sugoisdkwrapper.SugoiSdkWrapperModule"
]
}
}
SDKのセットアップ実装
iOSではAppDelegate
のdidFinishLaunchingWithOptions
、AndroidではApplication
のonCreate
でSDKのセットアップを行います
それぞれネイティブの実装を追加してみましょう。
iOS
まず最初に .podspec
ファイルにSDKの依存設定を追加しましょう。
+ s.dependency 'SugoiSDK'
また、Viewの実装は行わないため SugoiSdkWrapperView.swift
は削除します。
そして、 SugoiSdkWrapperAppDelegate.swift
というAppDelegateをハンドリングするファイルを追加し、次のように実装します。
import ExpoModulesCore
import SugoiSDK
public class SugoiSdkWrapperAppDelegate: ExpoAppDelegateSubscriber {
public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
// Info.plistからAPIキーを取得
guard let apiKey = Bundle.main.object(forInfoDictionaryKey: "SUGOI_SDK_API_KEY") as? String else {
return false
}
// TODO: SDKのセットアップを行う
print("SugoiSdkWrapperAppDelegate", apiKey)
return true
}
}
Android
Androidも同様に行っていきます。
SugoiSdkWrapperView.kt
ファイルは削除し、build.gradleファイルに依存するSDKを追加します。
そして SugoiSdkWrapperApplicationLifecycleListener.kt
と SugoiSdkWrapperPackage.kt
ファイルを追加し、それぞれ実装します。
SugoiSdkWrapperApplicationLifecycleListener.kt
package jp.kazutoyo.sugoisdkwrapper
import android.app.Application
import android.content.Context
import android.content.pm.PackageManager
import android.util.Log
import expo.modules.BuildConfig
import expo.modules.core.interfaces.ApplicationLifecycleListener
import jp.kazutoyo.sugoisdk
class SugoiSdkWrapperApplicationLifecycleListener: ApplicationLifecycleListener {
companion object {
const val TAG = "SugoiSdkWrapperModule"
const val SUGOI_SDK_API_KEY = "SUGOI_SDK_API_KEY"
}
override fun onCreate(application: Application?) {
super.onCreate(application)
application?.let { app ->
// API Keyを取得
val appInfo = app.packageManager.getApplicationInfo(app.packageName, PackageManager.GET_META_DATA)
val apiKey = appInfo.metaData.getString(SUGOI_SDK_API_KEY)
// TODO: セットアップを行う
Log.d(TAG, "SUGOI_SDK_API_KEY: $apiKey")
}
}
}
SugoiSdkWrapperPackage.kt
package jp.kazutoyo.sugoisdkwrapper
import android.content.Context
import expo.modules.core.interfaces.ApplicationLifecycleListener
import expo.modules.core.interfaces.Package
class SugoiSdkWrapperPackage: Package {
override fun createApplicationLifecycleListeners(context: Context?): List<ApplicationLifecycleListener> {
return listOf(
SugoiSdkWrapperApplicationLifecycleListener()
)
}
}
このとき、Packageを実装しているファイル名のSuffixは必ず Package.kt
する必要があります。
Config Pluginsの実装
作成したExpo Modulesのディレクトリに、 plugin/
ディレクトリを作成します。
その配下に tsconfig.json
を作成し、次のように記載します。
{
"extends": "expo-module-scripts/tsconfig.plugin",
"compilerOptions": {
"outDir": "build",
"rootDir": "src"
},
"include": ["./src"],
"exclude": ["**/__mocks__/*", "**/__tests__/*"]
}
また、 plugin/src/index.ts
ファイルを作成し、そちらにプラグインを実装していきます。
plugin/src/index.ts
の実装
次のように withInfoPlist
と withAndroidManifest
modsを使い、ファイルに変更を加えます。
import {
ConfigPlugin,
AndroidConfig,
withAndroidManifest,
withInfoPlist,
} from "@expo/config-plugins";
type Config = {
apiKey: string;
};
const withSugoiSdkWrapperConfig: ConfigPlugin<{
ios: Config;
android: Config;
}> = (config, { ios, android }) => {
config = withInfoPlist(config, (config) => {
// Info.plistにAPIキーを追加
config.modResults["SUGOI_SDK_API_KEY"] = ios.apiKey;
return config;
});
config = withAndroidManifest(config, (config) => {
const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(
config.modResults
);
// AndroidManifestにAPIキーを追加
AndroidConfig.Manifest.addMetaDataItemToMainApplication(
mainApplication,
"SUGOI_SDK_API_KEY",
android.apiKey
);
return config;
});
return config;
};
export default withSugoiSdkWrapperConfig;
pluginのビルド
ルートのpackage.jsonに次のようにビルドスクリプトを追加します。
+ "sugoi-sdk-wrapper:build:plugin": "cd modules/sugoi-sdk-wrapper && expo-module build plugin"
そしてこのスクリプトを実行することで、 modules/sugoi-sdk-wrapper/plugin/build/
にプラグインがビルドされました。
app.plugin.jsファイルの追加
最後に modules/sugoi-sdk-wrapper/app.plugin.js
ファイルを追加します。
ファイルでは先程ビルドしたpluginをexportします。
module.exports = require('./plugin/build')
プラグインを利用する
アプリケーションでプラグインを利用し、API Keyを設定できるようにしていましょう。
app.json
(app.config.(ts/js)
)の plugins
で次のように定義しましょう。
"plugins": [
[
"./modules/sugoi-sdk-wrapper/app.plugin.js",
{
"ios": {
"apiKey": "sugoi-sdk-no-ios-api-key"
},
"android": {
"apiKey": "sugoi-sdk-no-android-api-key"
}
}
]
],
そして expo prebuild
を実行してみましょう。
無事、次のようにInfo.plistとAndroidManifest.xmlにAPIキーが設定されましたね! 🎉
Info.plist
AndroidManifest.xml
動作確認
プラグインで設定されたAPIKeyがExpo Modulesで読み込めるか確認のため、モジュールに getApiKey()
といった関数を作成し、React Native側から呼び出して画面上に表示してみましょう。
モジュール側の実装
TSでの関数の定義とiOS/Android側の実装をしていきます。
SugoiSdkWrapperModule.ts
import { NativeModule, requireNativeModule } from 'expo';
declare class SugoiSdkWrapperModule extends NativeModule {
getApiKey(): string | null;
}
// This call loads the native module object from the JSI.
export default requireNativeModule<SugoiSdkWrapperModule>('SugoiSdkWrapper');
SugoiSdkWrapperModule.swift (iOS)
import ExpoModulesCore
public class SugoiSdkWrapperModule: Module {
// Each module class must implement the definition function. The definition consists of components
// that describes the module's functionality and behavior.
// See https://docs.expo.dev/modules/module-api for more details about available components.
public func definition() -> ModuleDefinition {
// Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
// Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
// The module will be accessible from `requireNativeModule('SugoiSdkWrapper')` in JavaScript.
Name("SugoiSdkWrapper")
// Defines a JavaScript synchronous function that runs the native code on the JavaScript thread.
Function("getApiKey") {
return Bundle.main.object(forInfoDictionaryKey: "SUGOI_SDK_API_KEY") as? String
}
}
}
SugoiSdkWrapperModule.kt (Android)
package jp.kazutoyo.sugoisdkwrapper
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import android.content.pm.PackageManager
class SugoiSdkWrapperModule : Module() {
// Each module class must implement the definition function. The definition consists of components
// that describes the module's functionality and behavior.
// See https://docs.expo.dev/modules/module-api for more details about available components.
override fun definition() = ModuleDefinition {
// Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
// Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
// The module will be accessible from `requireNativeModule('SugoiSdkWrapper')` in JavaScript.
Name("SugoiSdkWrapper")
Function("getApiKey") {
val applicationInfo = appContext?.reactContext?.packageManager?.getApplicationInfo(appContext?.reactContext?.packageName.toString(), PackageManager.GET_META_DATA)
return@Function applicationInfo?.metaData?.getString("SUGOI_SDK_API_KEY")
}
}
}
React Native側の実装
モジュールを利用し、 getAPIKey()
メソッドを呼び出して値を画面上に表示します。
import { StyleSheet, View, Text } from 'react-native';
import SugoiSdkWrapper from '@/modules/sugoi-sdk-wrapper'
export default function HomeScreen() {
return (
<View style={styles.container}>
<Text style={styles.text}>API Key: {SugoiSdkWrapper.getApiKey()}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 32,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
},
text: {
fontSize: 16,
color: 'black',
}
});
結果
iOS/Androidともに正しく値が取得できていますね! 🎉
iOS | Android |
---|---|
![]() |
![]() |
まとめ
Expo ModulesとConfig Pluginsを使うことで、ネイティブプロジェクトへの変更を最小限に抑えつつ、サードパーティSDKを導入できます。
CNGの恩恵を最大限に活かし、より良いReact Native開発ライフを送りましょう!
Discussion