skip to content

In React Native 0.87, how do you implement an Android Turbo Module in Kotlin against its Codegen-generated abstract spec class?

level: middleimportance: must knowfreq 45%

answer

  1. subclass the generated NativeXxxSpec
  2. pass ReactApplicationContext to super
  3. override every abstract method
  4. Promise arrives as a trailing parameter
  5. then register it in a BaseReactPackage

basics

~20 s

Subclass the generated abstract NativeXxxSpec, passing the ReactApplicationContext to its constructor, and override every abstract method it declares, with a Promise parameter for async results and a plain return for sync ones. Then return an instance from a BaseReactPackage.

solid answer

~40 s

From `NativeStepCounter.ts`, Codegen generates a Java class `NativeStepCounterSpec` in the package named by `codegenConfig.android.javaPackageName`. It is declared `public abstract class NativeStepCounterSpec extends ReactContextBaseJavaModule implements TurboModule`, takes a `ReactApplicationContext`, implements `getName()`, and declares one `public abstract` method per spec method, already annotated `@ReactMethod`. You write `class StepCounterModule(reactContext: ReactApplicationContext) : NativeStepCounterSpec(reactContext)` and override each method. A `Promise<number>` method becomes `getStepsSinceBoot(promise: Promise)`, which you settle with `promise.resolve(...)` or `promise.reject(...)`. A synchronous `boolean` method is generated with `isBlockingSynchronousMethod = true` and simply returns. Because the methods are abstract, a missing or mistyped override is a Kotlin compile error. Finally, return the module from a `BaseReactPackage`'s `getModule`.

code

kotlin · 48 lines
kotlin
package com.walkchallenge.steps

import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.walkchallenge.specs.NativeStepCounterSpec

class StepCounterModule(reactContext: ReactApplicationContext) :
    NativeStepCounterSpec(reactContext) {

  private val sensorManager =
      reactContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager

  // isAvailable(): boolean  -> synchronous
  override fun isAvailable(): Boolean =
      sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) != null

  // getStepsSinceBoot(): Promise<number>
  // Resolves on the first reading the sensor delivers; production code would add a timeout.
  override fun getStepsSinceBoot(promise: Promise) {
    val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
    if (sensor == null) {
      promise.reject("E_NO_SENSOR", "This device has no step counter")
      return
    }
    sensorManager.registerListener(
        object : SensorEventListener {
          override fun onSensorChanged(event: SensorEvent) {
            sensorManager.unregisterListener(this)
            promise.resolve(event.values[0].toDouble())
          }

          override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) = Unit
        },
        sensor,
        SensorManager.SENSOR_DELAY_NORMAL,
    )
  }

  // setGoal(steps: number): void
  override fun setGoal(steps: Double) {
    // persist the goal; nothing is returned to JavaScript
  }
}

go deeper

for a junior

Remember the shape: extend the generated NativeXxxSpec, pass the ReactApplicationContext to it, override its methods, and return the module from a package.

for a middle

Map spec methods to generated signatures: Promise as a trailing parameter, synchronous returns flagged isBlockingSynchronousMethod, numbers as double, constants via getTypedExportedConstants.

for a senior

Use the compiler as the contract check, keep synchronous methods cheap, and settle every Promise exactly once, including sensor-unavailable paths.

for a principal

Decide how Android modules are organised across teams, such as generated spec per feature and shared naming, so spec changes fail builds early rather than in production.

## What Codegen hands you on Android A **Turbo Module** is a native module that JavaScript calls through JSI with a typed contract. On Android, **Codegen** — run by the React Native Gradle plugin during the build — reads the TypeScript spec and writes a **Java abstract class** that your Kotlin code extends. For a walking-challenge app with `specs/NativeStepCounter.ts` and `codegenConfig.android.javaPackageName` set to `com.walkchallenge.specs`, the app build produces `NativeStepCounterSpec.java` under `android/app/build/generated/source/codegen`. Its shape is always the same: - **Declaration:** `public abstract class NativeStepCounterSpec extends ReactContextBaseJavaModule implements TurboModule`. The New Architecture spec still sits on the familiar base class, which is what gives your module `getReactApplicationContext()`. - **Name:** `public static final String NAME`, copied from the spec's `getEnforcing` call, plus a `getName()` that returns it. - **Constructor:** `NativeStepCounterSpec(ReactApplicationContext reactContext)`. - **Methods:** one `public abstract` method per spec method, annotated `@ReactMethod` and `@DoNotStrip` for you. - **Constants:** if the spec declares a typed `getConstants()`, a `protected abstract getTypedExportedConstants()` for you to fill, behind a `final` `getConstants()`. ## How spec methods become Kotlin overrides | Spec method | Generated Java signature | Your Kotlin override | |---|---|---| | `isAvailable(): boolean` | `@ReactMethod(isBlockingSynchronousMethod = true) public abstract boolean isAvailable();` | `override fun isAvailable(): Boolean` | | `getStepsSinceBoot(): Promise<number>` | `@ReactMethod public abstract void getStepsSinceBoot(Promise promise);` | `override fun getStepsSinceBoot(promise: Promise)` | | `setGoal(steps: number): void` | `@ReactMethod public abstract void setGoal(double steps);` | `override fun setGoal(steps: Double)` | Three rules fall out of the table: 1. A **`Promise` return** becomes a trailing `Promise` parameter on a `void` method; the module calls `promise.resolve(value)` or `promise.reject(code, message)` exactly once. 2. A **plain return type** is a synchronous method; JavaScript waits for it, so keep it cheap. 3. **`number` arrives as `double`**, so a Kotlin override takes `Double` even when the value is an integer count. ## Writing the module 1. Declare the class with the context in the primary constructor: `class StepCounterModule(reactContext: ReactApplicationContext) : NativeStepCounterSpec(reactContext)`. 2. Let the IDE generate overrides from the abstract class, then fill them in. 3. Reach Android services through `reactApplicationContext`, the Kotlin property for `getReactApplicationContext()` — for the step counter, `getSystemService(Context.SENSOR_SERVICE)`. 4. Do not annotate the overrides with `@ReactMethod`; the generated abstract methods already carry it. 5. Register the class: return it from a `BaseReactPackage`'s `getModule` when asked for `NativeStepCounterSpec.NAME`, and list it in that package's module-info map. Because every spec method is `abstract`, the **compiler enforces the contract**: forget one, or take `Int` where the spec produced `double`, and the Kotlin build fails. That is the practical payoff of the typed spec over the legacy hand-annotated class. ## Constants, when the spec declares them A spec may declare `getConstants()` returning an object with named properties, fixed for the module's lifetime — for the step counter, perhaps whether the device reports a hardware step detector. Codegen then generates a `protected abstract Map<String, Object> getTypedExportedConstants()` for you to implement and makes `getConstants()` itself `final`, so it can check the map you return against the typed spec. Overriding `getConstants()` directly, as legacy modules did, no longer compiles. ## Kotlin versus Java Codegen writes the spec in **Java**, and React Native's tutorial shows the implementation in both Java and Kotlin. Kotlin is the common choice in new code and interoperates with the generated class like any Java superclass. The Kotlin language itself — `override`, nullable types, companion objects — is not what interviewers probe here; the generated contract is. ## Mistakes interviewers listen for - **Extending `ReactContextBaseJavaModule` directly** in new code and adding `@ReactMethod` by hand — that is the legacy pattern the generated spec replaces. - **Returning a value from a `Promise` method** — the generated method is `void`; the value goes through `promise.resolve`. - **Editing the generated Java file** — it lives in `build/` and is regenerated by the next build. - **Forgetting registration** — a correct module class that no package returns is never created. - **Doing slow sensor or disk work in a synchronous method** — JavaScript is blocked until it returns.

  • Why does the generated spec extend ReactContextBaseJavaModule if that is the legacy base class?
    It reuses the base class for what it still does well: holding the `ReactApplicationContext` and providing `getReactApplicationContext()` and the lifecycle hooks. What changed is on top of it. The methods are abstract and generated from a typed spec, the class implements the `TurboModule` marker interface, and calls arrive through JSI instead of the legacy bridge.
  • What happens if the TypeScript spec gains a method but the Kotlin class is not updated?
    The next build regenerates `NativeStepCounterSpec` with a new abstract method, and `StepCounterModule` no longer compiles, because a concrete class must override every abstract member. The failure lands at build time, not as a missing method at runtime.
  • Where is the generated spec class, and should you commit it?
    For an app it is written to `android/app/build/generated/source/codegen`, in the package set by `codegenConfig.android.javaPackageName`. It is build output that the Gradle plugin regenerates, so you neither edit nor commit it; your module imports it like any other class.

saying these in an interview costs you the question

  • A Turbo Module class should extend ReactContextBaseJavaModule directly and add @ReactMethod by hand
  • A Promise method should return the value and React Native wraps it
  • Kotlin overrides must repeat the @ReactMethod annotation or JavaScript cannot see them
  • The generated spec is Kotlin, so it lives in src/main alongside your code
  • A spec number arrives as Int when the value happens to be whole