本記事は React Native Advent Calendar 2023 の 16 日目です。
React Native を利用したアプリ開発において、Native Modules は欠かせない機能の1つです。Siri などの OS 特有の機能を使ったり、自社サービス(ビジネスロジック)に関するネイティブ機能の Native Modules など、さまざまな利用シーンがあります。
私はこれまで、業務と個人の開発を含め、いくつかの Native Modules を作りました。その際に起こった問題や、気をつけていることを紹介します。
React Native 0.68 にアップデートしてクラッシュ
React Native 0.68 から New Architecture が始まり、React Native の構造は大きく変わりました。Native Module もその影響を受けました。自作の Native Module を組み込んだアプリを React Native 0.68 にアップデートしたところ、これまで正常だった iOS アプリは Native Module 内でクラッシュしました。
iOS において、ネイティブ側から React Native にイベントを送信する際は sendEvent(withName:body:)
を利用します。この関数を実行するタイミングでクラッシュしました。
RCTCallableJSModules is not set. This is probably because you've explicitly synthesized the RCTCallableJSModules in {モジュール名}, even though it's inherited from RCTEventEmitter.
調べてみると、その sendEvent 関数の中で利用されるプロパティ callableJSModules が weak(弱参照)に変更されていました。関数実行時に callableJSModules がすでに解放されているので、クラッシュしました。
対策として、iOS 側のクラスに RCTCallableJSModules() で生成したプロパティを callableJSModules とは別に持たせました(仮に _callableJSModules と命名しました)。その生成したプロパティを関数実行前に本来の callableJSModules に代入するという力技で対応しました。正しい対応は何でしょうかね...
@objc(MyNativeModule)
class MyNativeModule: RCTEventEmitter {
// RCTEventEmitter has already defined `weak callableJSModules`.
// 代用として生成した RCTCallableJSModules のインスタンス
private let _callableJSModules = RCTCallableJSModules()
func sendEventToReactNative(message: String) -> Bool {
// 本来の self.callableJSModules に _callableJSModules を代入する
// なお self.callableJSModules = RCTCallableJSModules() と直接生成しても
// weak なので sendEvent() 内で利用されるときには既に解放されている
self.callableJSModules = _callableJSModules
self.callableJSModules.setBridge(self.bridge)
if self.bridge != nil {
self.sendEvent(withName: "EVENT_NAME",
body: ["message": imessage])
return true
}
return false
}
}
React Native にフレンドリーな関数設計を
次のネイティブ関数の Native Modules を考えます。今回は Android 向けの想定です。
int setAlignment(int alignment)
この関数は文字位置を調整する関数です。次の表に対応する整数値を引数に与えます。
引数 | 対応する文字位置 |
---|---|
0 | left |
1 | center |
2 | right |
この関数に対応する最も簡単な React Native とのインタフェースを考えてみました。
export const setAlignment = (alignment: number) => Promise<boolean>
これは関数に数値を入れるだけで簡単ですが、安全な関数ではないです。引数に何の数値を設定すればよいか、ネイティブ側が許容しない範囲の数値が設定可能など、安全性や可読性に問題がある関数です。
引数を意味あるものにしましょう。たとえば、状態を示す union 型の Alignment
を引数に設定して、引数の「翻訳」を行いましょう。
// 対応する文字位置の状態
type Alignment = 'left' | 'center' | 'right'
// ネイティブ側で実装される関数の型群
interface NativeLibrary {
setAlignment: (alignment: number) => Promise<boolean>
}
// ネイティブのインスタンス
const nativeLibrary: NativeLibrary = NativeModules.NativeLibrary
// RN で利用する関数
// 開発者はネイティブ関数の設計を知らなくても使える
export const setAlignment = (alignment: Alignment) => {
var alignmentNumber = 0
switch (alignment) {
case 'left':
alignmentNumber = 0
break
case 'center':
alignmentNumber = 1
break
case 'right':
alignmentNumber = 2
break
}
return nativeLibrary.setAlignment(alignmentNumber)
}
この「翻訳」は、上記のように React Native 側で行うか、もしくはネイティブ側で行うか、どちらがよいでしょうか。これはおそらく解答はなく、もう片方のネイティブ(この例では iOS)の実装を見てからの判断になるでしょう。また、ネイティブ側の開発言語(Swift、Kotlin)に不慣れであれば、React Native 側で行うのがベターでしょう。
その他で気を付けることとして、ネイティブ側の例外をそのまま放置する・握り潰しないようにしましょう。例外は React Native 側に伝達させて、適切に処理しましょう。
@ReactMethod
fun setAlignment(alignment: Int, promise: Promise) {
try {
val result = native.setAlignment(alignment)
promise.resolve(result)
} catch (e: Exception) {
promise.reject("0", e.message)
}
}
Native Modules の利点はネイティブ知識がなくても React Native で実装できることです。ネイティブ側の関数設計を知らなくても、安全に実装できるようにするのが大切です。
自作した Native Modules
最近、個人開発で Naitve Modules を公開しました。SUNMI 製のモバイルプリンターを搭載した端末向けの印刷ライブラリです。
以前は他の方が作成されたものを利用していました。しかし、関数の設計が分かりにくい、ネイティブ側のクラッシュが適切に処理されず RN でキャッチできずにクラッシュしてしまうと問題がありました。便利で利用していたのですが、アプリ安定性の向上を考えた場合に、このままでは難しい。一部の機能に関しては修正 PR を投げました。しかし、修正したい対象範囲が大きく PR で修正していくのは難しい、開発者様が想定する設計意図と大きく異なる恐れがあるということで、自作しました。
まとめ
私が作ってきた Native Modules の経験をまとめました。これから Native Modules を作る人に参考になれば、幸いです。
Native Modules 自体も Turbo Modules と新しいアーキテクチャが導入されています。新しい Native Modules はまだ慣れないところがあって難しく、私は従来の方を選択しがちです。反応速度が求められるものを作るときは、Turbo Modules に挑戦してみたいです。
おまけ
作成した Native Modules は npm で @mitsuharu/react-native-sunmi-printer-library
という名前で公開しました。他者さんのライブラリと名前が類似するので @mitsuharu
を文頭に付けました。
この@
で始まる名前は自身のアカウント名もしくは所属する organization と同名でなければ付けれません。私はリリース作業をするまで気付かず、いざ公開しようとしたタイミングで知りました。私のアカウント名は mitsuharu_e
なので @mitsuharu
は利用できません。
これまで業務で作成したライブラリは社名を冠にしてましたが、それらは GitHub のプライベートで利用していたので、この問題に遭遇したことはありませんでした。
さて、どうするか。アカウント名は変更できない。ライブラリ名をアカウント名に合して変更するにしても、ライブラリ名にアンダースコアがあるのはちょっと…、今更変えるの?、むしろ出来るのか??、と悩んだ末、1人だけのorganization mitsuharu
を爆誕させました。
自作ライブラリを npm で公開予定がある方は、お気を付けくださいませ。