🤖

【React Native】Native UI Components で Android のネイティブのビューを表示する

2022/12/01に公開

こんにちは!アルダグラムでエンジニアをしている渡邊です!

React Native では Native UI Components で Android のネイティブのビューを表示させることができます。
今回この機能について掘り下げていきたいと思います!

今回のサンプルプロジェクトは こちら に公開しています。

Fabric については今回の記事では扱いません。

Native UI Components

Native UI Components でできることは大別して以下になります。

  1. Android のネイティブのビューを React Native で表示できる
  2. React Native からネイティブのビューにパラメータを渡すことができる
  3. ネイティブから React Native にイベントを伝えることができる
  4. React Native からネイティブのビューにイベントを伝えることができる

今回 RainbowView という一定時間ごとに背景色が虹色に変化するビューを Android のネイティブのビューとして作成し、それを表示する例を元に順を追って見ていきます。

1. Android のネイティブのビューを React Native で表示

まずは Android 側のコードになります。
ネイティブのビューを表示するために ViewManager を継承したクラスを用意する必要があります。
大抵の場合は以下の2つのいずれかを継承すれば問題ないはずです。

  • SimpleViewManager
  • ViewGroupManager

ViewGroupManager は Android フレームワークの ViewGroup を継承したビューを表示したい場合に使用します。 こちらを使うことで以下のコード例のように React Native において children の要素を add して表示させることができます。

// NativeViewGroup は ViewGroupManager を使ったネイティブのビュー.
<NativeViewGroup
  ...
>
    <View ... >
    <View ... >
    <View ... >
</NativeViewGroup>

上記のように children が必要ない場合は SimpleViewManager を使います。

SimpleViewManagerViewGroupManager も共通して、getName()createViewInstance() メソッドをオーバーライドする必要があります。
今回 RainbowView を表示するために SimpleViewManager を使って実装します。

RainbowViewManager.kt
class RainbowViewManager : SimpleViewManager<RainbowView>() {
    override fun getName(): String {
        // React Native で参照する際の名前.
        return "RainbowView"
    }

    override fun createViewInstance(reactContext: ThemedReactContext): RainbowView {
        // 表示したいビューをインスタンス化する.
        return RainbowView(reactContext)
    }
}

ReactPackage を継承したクラスの createViewManagers() メソッドで RainbowViewManager のインスタンスを登録します。

MyAppPackage.kt
class MyAppPackage : ReactPackage {
    override fun createNativeModules(reactContext: ReactApplicationContext) = 
        emptyList<NativeModule>()

    override fun createViewManagers(reactContext: ReactApplicationContext) =
        // RainbowViewManager を登録する.
        mutableListOf(RainbowViewManager())
}

上記の MyAppPackage は MainApplicationReactNativeHost に登録します。

MainApplication.java
public class MainApplication extends Application implements ReactApplication {

    private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
        ...
        @Override
        protected List<ReactPackage> getPackages() {
            List<ReactPackage> packages = new PackageList(this).getPackages();
            packages.add(new MyAppPackage()); // <- 追加
            return packages;
        }
        ...
    };
  
    ...

これで表示する準備はできたので、React Native 側で表示させます。
まずは RainbowView を使用するために requireNativeComponent 関数を使います。 引数には RainbowViewManagergetName() メソッドで返している文字列を指定します。

RainbowView.ts
import { requireNativeComponent, ViewProps } from 'react-native'

type RainbowViewProps = Readonly<ViewProps>

export const RainbowView =
  requireNativeComponent<RainbowViewProps>('RainbowView')

これで RainbowView をコンポーネントとして使うことができるようになりました。

App.tsx
const App = () => <RainbowView style={styles.rainbow} />

const styles = StyleSheet.create({
  rainbow: {
    flex: 1
  }
})

2. React Native からネイティブのビューにパラメータを渡す

先ほどの RainbowView は1秒ごとに色が変わるビューとして実装しています。 色が変わる時間を React Native から設定させるようにしてみます。

Android 側で React Native からパラメータを受け取るためには、ViewManager を継承したクラスで @ReactProp アノテーションが付いたセッターメソッドを定義する必要があります。
今回の場合だと RainbowViewManager に以下のようにセッターメソッドを用意します。

RainbowViewManager.kt
class RainbowViewManager : SimpleViewManager<RainbowView>() {
    ...
  
    @ReactProp(name = "updateMillis")
    fun setUpdateMillis(view: RainbowView, updateMillis: Int) {
        view.updateMillis = updateMillis
    }
}

@ReactProp アノテーションには name を必須で指定する必要があります。 ここで定義した名前は React Native からパラメータを渡す時の名前になります。

セッターメソッドにはルールがあり、第一引数には対象のビューのインスタンス(今回の例だと RainbowView)、第二引数には受け取りたいパラメータを指定します。
第二引数に指定できるパラメータの型は以下のようになっています。

  • Java の場合
    • booleanintfloatdoubleStringBooleanIntegerReadableArrayReadableMap
  • Kotlin の場合
    • BooleanIntFloatDoubleStringReadableArrayReadableMap

また @ReactProp にはプリミティブ型に対して以下のようにデフォルト値を設定しておくことが可能です。

@ReactProp(name = "updateMillis", defaultInt = 1000)

続いて React Native 側を対応します。 先ほど定義した RainbowViewPropsupdateMillis を追加します。

RainbowView.ts
type RainbowViewProps = Readonly<{
  updateMillis?: number
} & ViewProps>

export const RainbowView =
  requireNativeComponent<RainbowViewProps>('RainbowView')

そして RainbowView に対して updateMillis の props を指定すればパラメータを渡すことができます。

App.tsx
const App = () => <RainbowView
  updateMillis={2000}
  style={styles.rainbow}
/>

3. ネイティブから React Native にイベントを伝える

今度はネイティブから React Native にイベントを伝えたい場合です。 例えば色が変わったタイミングで次の色を React Native に伝えるようにしたい場合を考えます。

Android 側では イベントの登録イベントの送信 の実装を行う必要があります。 イベントの登録ViewManagergetExportedCustomDirectEventTypeConstants() メソッドをオーバーライドします。

RainbowViewManager.kt
class RainbowViewManager : SimpleViewManager<RainbowView>() {
    ...

    override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
        return mutableMapOf(
            "onColorChanged" // イベントを送信する際に指定する名前
                to mutableMapOf(
                  "registrationName" to "onColorChanged") // React Native 側でイベントを参照する際に使う名前
        )
    }
}

上記の最初の方の onColorChanged はイベントを送信する際に指定する名前になります。 registrationName は固定で設定する必要があり、その後の onColorChanged は React Native 側でイベントを参照する際の名前になります。
例では同じ名前にしていますが、もちろん異なる名前を設定しても問題はありません。

次に イベントの送信 ですが、これは RainbowView で色が変わったタイミングで以下のメソッドを呼び出しています。

RainbowView.kt
class RainbowView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    ...
  
    private fun sendColorChangedEventToJs(color: Int) {
        val arguments = Arguments.createMap().apply {
            // カラー値を16進数の文字列に変換
            putString("color", "0x${color.toUInt().toString(16)}")
        }
        (context as ReactContext)
            .getJSModule(RCTEventEmitter::class.java)
            .receiveEvent(id, "onColorChanged", arguments)
    }
}

上記の例のように WritableMap に送りたいパラメータを設定してイベントを送信することが可能です。 ここではカラー値を16進数の文字列に変換して送っています。

React Native 側でイベントを受け取るために、RainbowViewProps にコールバックを追加します。 コールバックの名前は前述の通り RainbowViewManagergetExportedCustomDirectEventTypeConstants() で設定した名前にする必要があります。

RainbowView.ts
import { NativeSyntheticEvent, requireNativeComponent, ViewProps } from 'react-native'

type ColorChangedData = {
  color: string
}

type RainbowViewProps = Readonly<{
  updateMillis?: number
  onColorChanged?: (e: NativeSyntheticEvent<ColorChangedData>) => void
} & ViewProps>

export const RainbowView =
  requireNativeComponent<RainbowViewProps>('RainbowView')

追加したコールバックは以下のように使うことができます。

App.tsx
const App = () => <RainbowView
  style={styles.rainbow}
  onColorChanged={e => console.log('color changed: ', e.nativeEvent.color)}
/>

4. React Native からネイティブのビューにイベントを伝える

最後に React Native からネイティブのビューにイベントを伝えたい場合です。 RainbowView では自動で一定時間ごとに色が変わりますが、色が変わるのを開始、終了できるように制御したい場合を考えます。

Android 側では ViewManager を継承したクラスで getCommandsMap()receiveCommand() メソッドをオーバーライドする必要があります。 RainbowViewManager では以下のようになります。

RainbowViewManager.kt
class RainbowViewManager : SimpleViewManager<RainbowView>() {
    ...
  
    override fun getCommandsMap(): Map<String, Int> {
        // イベント名と数値をマッピングする
        return mapOf("start" to COMMAND_START, "stop" to COMMAND_STOP)
    }

    override fun receiveCommand(view: RainbowView, commandId: String, args: ReadableArray?) {
        super.receiveCommand(view, commandId, args)

        // イベントを受け取った際の処理をここに書く.
        when (commandId.toInt()) {
            COMMAND_START -> {
                view.startChangeColor()
            }
            COMMAND_STOP -> {
                view.stopChangeColor()
            }
        }
    }

    companion object {
        private const val COMMAND_START = 1
        private const val COMMAND_STOP = 2
    }
}

getCommandsMap() ではイベント名と数値をマッピングして登録します。 ここでは startstop というイベントを React Native から受け取れるように設定します。

次に receiveCommand() ではイベントを受け取った際の処理を記述します。 React Native からパラメータも送ることができ、送ったパラメータは args に格納されています。

続いて React Native 側です。 イベントを送るために以下のようにメソッドを定義します。

RainbowView.ts
import { NativeSyntheticEvent, requireNativeComponent, UIManager, ViewProps } from 'react-native'

...

export const start = (viewId) =>
  UIManager.dispatchViewManagerCommand(
    viewId,
    UIManager.RainbowView.Commands.start.toString(),
    [] // ネイティブにパラメータを渡したい場合はこの配列に値を設定する
  )

export const stop = (viewId) =>
  UIManager.dispatchViewManagerCommand(
    viewId,
    UIManager.RainbowView.Commands.stop.toString(),
    []
  )

上記の UIManager.dispatchViewManagerCommand() の第二引数には、RainbowViewManagergetCommandsMap() で設定した startstop のイベント名がそれぞれ使われています。

あとはこのメソッドを任意のタイミングで呼び出すようにします。

App.tsx
const App = () => {
  const [started, setStarted] = useState(false)
  const rainbowViewRef = useRef(null)

  return (
    <View>
      <Button
        ...
        title={started ? 'stop' : 'start'}
        onPress={() => {
          const viewId = findNodeHandle(rainbowViewRef.current)
          if (started) {
            stop(viewId)
            setStarted(false)
          } else {
            start(viewId)
            setStarted(true)
          }
        }}
      />
      <RainbowView
        ref={rainbowViewRef}
        ...
      />
    </View>
  )
}

viewId を取得するために、ネイティブで作成したビュー(ここでは RainbowView)に対して ref を設定します。 そして findNodeHandle()ref.current を指定することで viewId を取得することができます。

そして先ほど定義した start もしくは stop 関数に、viewId を渡して呼び出すことでネイティブのビューに対してイベントを伝えることができるようになります。

その他 Tips

onDropViewInstance() メソッド

ViewManager には onDropViewInstance() メソッドがあり、このメソッドは View の後処理をしたい場合に便利です。

ネイティブで表示したビューが使われなくなるタイミングでこのメソッドが呼び出されるため、ビューの後処理が必要な場合にはこのメソッドをオーバーライドし、このメソッド内に処理を書くとよいです。

RainbowViewManager.kt
class RainbowViewManager : SimpleViewManager<RainbowView>() {

    override fun onDropViewInstance(view: RainbowView) {
        // ビューの後処理をここに記述する...
        super.onDropViewInstance(view)
    }
    
    ...
}

onAfterUpdateTransaction() メソッド

前述の @ReactProp アノテーションを付与したセッターは ViewManager 内に複数定義することができます。 例えば複数のパラメータを受け取った後に何か処理したい場合には ViewManageronAfterUpdateTransaction() メソッドが使えます。

@ReactProp を使ってネイティブにパラメータが渡す場合、onAfterUpdateTransaction() メソッドは全てのパラメータが渡された後に呼び出されます。

例えば以下のように全てのパラメータを受け取ってから初期化処理などを行いたい、というような場合に便利です。

RainbowView.kt
class RainbowView ... {
    var intValue: Int = 0
    var stringValue: String = ""
    private var isInitialized: Boolean = false
  
    fun initialize() {
        if (isInitialized) {
            return
        }
        isInitialized = true
      
        // intValue, stringValue を使った初期化処理をここに記述する...
    }
}
RainbowViewManager.kt
class RainbowViewManager : SimpleViewManager<RainbowView>() {

    @ReactProp(name = "intValue")
    fun setIntValue(view: RainbowView, intValue: Int) {
        view.intValue = intValue
    }

    @ReactProp(name = "stringValue")
    fun setStringValue(view: RainbowView, stringValue: String) {
        view.stringValue = stringValue
    }

    override fun onAfterUpdateTransaction(view: RainbowView) {
        // 更新があった全てのパラメータの @ReactProp のメソッドが呼び出された後にこのメソッドが呼ばれる
        super.onAfterUpdateTransaction(view)
        view.initialize()
    }

getExportedViewConstants()

ネイティブで定義した定数を React Native で使うために ViewManagergetExportedViewConstants() メソッドが使えます。 このメソッドでは React Native で参照する際の名前と定数値を Map として返すことで、React Native でこの定数値が参照できます。

RainbowViewManager.kt
class RainbowViewManager : SimpleViewManager<RainbowView>() {
    ...
  
    override fun getExportedViewConstants(): Map<String, Any> {
        return mapOf("DEFAULT_INT_VALUE" to 1, "DEFAULT_STRING_VALUE" to "hoge")
    }
}
// ネイティブで定義した定数は UIManager.<ViewName>.Constants.<定数値名> で参照できる
export const DEFAULT_INT_VALUE =
  UIManager.RainbowView.Constants.DEFAULT_INT_VALUE

export const DEFAULT_STRING_VALUE =
  UIManager.RainbowView.Constants.DEFAULT_STRING_VALUE

参考

アルダグラム Tech Blog

Discussion