skip to content

Kotlin Spec Classes

On Android a Turbo Module is a Kotlin or Java class extending the generated abstract spec and registered through a BaseReactPackage. Interviewers probe that path and the legacy API it replaced.

part ofReact Nativeoverview, primer and where to startread it →
on this pageshow

explore

questions

5

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
open as a page

In React Native on Android, what is a native module's getName() for, and where does its value come from in a Turbo Module?

level: juniorimportance: should knowfreq 35%

basics

~20 s

getName() returns the name a native module is known by. Legacy packages registered modules under it, as NativeModules.<name>. In a Turbo Module the Codegen-generated spec implements it from the getEnforcing name, while lookup itself runs through the package's ReactModuleInfo key and getModule.

open as a page

In React Native on Android, what do BaseReactPackage's getModule and getReactModuleInfoProvider each contribute when registering a Turbo Module?

level: middleimportance: should knowfreq 30%

basics

~20 s

getReactModuleInfoProvider advertises which module names the package can supply and how to treat them (isTurboModule, needsEagerInit, canOverrideExistingModule). getModule is the lazy factory React Native calls with one of those names to create the instance the first time JavaScript asks.

open as a page

When migrating a legacy React Native Android module built on ReactContextBaseJavaModule and @ReactMethod to a Turbo Module, what changes and what stays the same?

level: middleimportance: should knowfreq 35%

basics

~20 s

The module stops extending ReactContextBaseJavaModule directly and extends the generated spec, which itself extends it and carries the @ReactMethod annotations. Registration moves from an eager ReactPackage.createNativeModules list to a lazy BaseReactPackage with getModule and a module-info map.

open as a page

Wrapping Android's step-counter sensor in a React Native Turbo Module, how do you use ReactApplicationContext safely across activity changes, backgrounding and JavaScript reloads?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Treat ReactApplicationContext as an Application-scoped handle: use it for system services, read currentActivity only when needed and never store it, pause the sensor from a LifecycleEventListener, and unregister everything in invalidate(), which runs before the React instance is torn down.

open as a page