← All shorts
41React Native 0.74+2 min read

Writing a Native Module in Swift and Kotlin

A TurboModule needs a typed JS spec plus matching Swift and Kotlin implementations generated from the same codegen contract.

A bridge-era native module is a plain @objc class registered by name, and nothing in that registration ties its exported method signatures to the JS call site. Misspell getLevel. Hand it a string where it wants a number. The build is perfectly happy either way, and you find out at runtime.

#The old way

import Foundation

@objc(BatteryInfo)
class BatteryInfo: NSObject {
  @objc
  func getLevel(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
    resolve(0.82)
  }

  @objc
  static func requiresMainQueueSetup() -> Bool {
    return false
  }
}

#The new way

One TS spec drives codegen for both platforms:

import { TurboModuleRegistry, type TurboModule } from 'react-native';

export interface Spec extends TurboModule {
  getLevel(): Promise<number>;
}

export default TurboModuleRegistry.getEnforcing<Spec>('BatteryInfo');
import Foundation

@objc(BatteryInfo)
class BatteryInfo: NSObject, NativeBatteryInfoSpec {
  func getLevel(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
    resolve(0.82)
  }
}
package com.example.batteryinfo

import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext

class BatteryInfoModule(reactContext: ReactApplicationContext) :
  NativeBatteryInfoSpec(reactContext) {

  override fun getName() = NAME

  override fun getLevel(promise: Promise) {
    promise.resolve(0.82)
  }

  companion object {
    const val NAME = "BatteryInfo"
  }
}

Both implementations conform to NativeBatteryInfoSpec. Codegen writes that interface, and its Kotlin abstract-class counterpart, from the same TS Spec.

#Why it matters

Miss a native method or type one wrong, and Xcode or Android Studio fails the build. Not a runtime crash a user discovers. The JS spec is the single source of truth for parameter and return types on both platforms, so there is no separate doc to keep in sync.

Swift and Kotlin stay symmetric by construction. They implement the same generated contract.

#Gotcha

Codegen names the generated protocol and class from the TS spec's filename (Native<Name>Spec), not from the string passed to getEnforcing, so rename the spec file, leave the conformance alone, and both sides now implement a contract codegen no longer produces.

The build fails with a generic "does not conform to protocol" error. It never points at the rename.

react-nativenative-modulesswiftkotlin

Related shorts