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?
answer
- it wraps the Application, not an Activity
- currentActivity is nullable, never store it
- LifecycleEventListener: resume, pause, destroy
- invalidate() runs before teardown
- check the instance is still active
basics
~20 sTreat 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.
solid answer
~40 s`ReactApplicationContext` wraps the **Application** context — its constructor deliberately takes `context.applicationContext` — so it is safe to use for `getSystemService(Context.SENSOR_SERVICE)` for the module's whole life. It is not an Activity: when UI needs one, read `reactApplicationContext.currentActivity` at that moment, handle `null`, and never keep it in a field, because that leaks the Activity; the module's own `getCurrentActivity()` shortcut is deprecated since 0.80. For backgrounding, implement `LifecycleEventListener` and register it with `addLifecycleEventListener`, so `onHostPause` can stop sensor callbacks and `onHostResume` can restart them. For reloads, override `invalidate()`, which React Native calls before the instance is destroyed: unregister the sensor listener and remove the lifecycle listener there, or the old module keeps receiving events after a reload.
code
kotlin · 53 linesclass StepCounterModule(reactContext: ReactApplicationContext) :
NativeStepCounterSpec(reactContext), LifecycleEventListener, SensorEventListener {
private val sensorManager =
reactContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager
private var watching = false
@Volatile private var latestSteps = -1.0
override fun initialize() {
super.initialize()
reactApplicationContext.addLifecycleEventListener(this)
}
// startWatching(): void
override fun startWatching() {
watching = true
register()
}
// stopWatching(): void
override fun stopWatching() {
watching = false
sensorManager.unregisterListener(this)
}
// getLatestSteps(): number -> synchronous, reads a cached value
override fun getLatestSteps(): Double = latestSteps
override fun onHostResume() {
if (watching) register()
}
override fun onHostPause() = sensorManager.unregisterListener(this)
override fun onHostDestroy() = sensorManager.unregisterListener(this)
override fun onSensorChanged(event: SensorEvent) {
latestSteps = event.values[0].toDouble()
}
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) = Unit
override fun invalidate() {
sensorManager.unregisterListener(this)
reactApplicationContext.removeLifecycleEventListener(this)
super.invalidate()
}
private fun register() {
val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) ?: return
sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL)
}
}go deeper
Remember that the module gets an Application-scoped context, and that anything registered with Android must be unregistered when React Native tears the module down.
Explain why ReactApplicationContext wraps the Application context, how to read currentActivity safely, and what each LifecycleEventListener callback is for.
Show how reloads and backgrounding leak listeners and Activities, and fix them with invalidate(), lifecycle pausing and an active-instance check before calling back.
Define a cleanup contract for all in-house modules, such as registration in initialize and teardown in invalidate, and make leak checks part of review.
## What ReactApplicationContext is Every Android native module is constructed with a **`ReactApplicationContext`** — the object the generated spec's constructor takes and `getReactApplicationContext()` returns. It is a `ReactContext`, React Native's Android `Context` subclass, and its constructor wraps `context.applicationContext` on purpose: the source comments explain that React Native wants to be sure it wraps the **Application** context, not an Activity. That one fact settles most safety questions: - It is **safe to keep** for the module's lifetime — holding it cannot leak an Activity. - It is the right handle for **system services**: `getSystemService(Context.SENSOR_SERVICE)` for the step counter, shared preferences, file paths. - It is the **wrong** handle for anything that needs an Activity, such as showing a dialog or starting an activity for a result. ## Getting an Activity without leaking it When the walking-challenge app needs an Activity — to launch a settings screen, say — read it at the moment of use: 1. Call `reactApplicationContext.currentActivity`. 2. Handle `null`: there may be no foreground Activity, for example while the app is in the background. 3. Use it immediately and let it go. The base class's documentation says this in capitals: never store the returned Activity in a member variable, because that causes memory leaks. In React Native 0.87 the module-level shortcut `getCurrentActivity()` on `ReactContextBaseJavaModule` is deprecated since 0.80, with `reactApplicationContext.currentActivity` as the replacement. ## Following the app to the background and back A step counter that keeps a sensor listener registered while the user is on another app wastes battery for data nobody is looking at. `ReactContext` offers host lifecycle callbacks through **`LifecycleEventListener`**: | Callback | Fires when | Step-counter action | |---|---|---| | `onHostResume()` | The host Activity comes to the foreground | Re-register the sensor listener if JavaScript asked to watch | | `onHostPause()` | The host Activity goes to the background | Unregister the sensor listener | | `onHostDestroy()` | The host Activity is destroyed | Unregister; nothing will display the data | Register the listener with `reactApplicationContext.addLifecycleEventListener(this)` — in `initialize()`, which React Native calls once the module has been created — and remove it with `removeLifecycleEventListener(this)` during cleanup. Whether a challenge must keep counting while the app is in the background is a product decision governed by Android's own sensor and background-work rules; the React Native part is wiring the listener to the host lifecycle so the module does what that decision says. ## Surviving a JavaScript reload In development, a reload tears down the React instance and creates a new one, with **new module instances**; in production, the host can be destroyed and rebuilt too. Before the old instance goes away, React Native's Turbo Module manager calls **`invalidate()`** on every module it created. That is the cleanup hook: 1. Unregister every `SensorEventListener` the module registered. 2. Remove the `LifecycleEventListener`. 3. Cancel any pending work that would call back into JavaScript. Skip this and the discarded module keeps its sensor registration: callbacks keep firing into an instance nobody can reach, the old module and its context cannot be collected, and after a few reloads there are several listeners doing the same work. ## Before talking back to JavaScript A sensor callback can arrive after teardown has begun. `getReactApplicationContextIfActiveOrWarn()` returns the context only if the React instance is still active, otherwise `null` with a logged warning, and `hasActiveReactInstance()` answers the same question directly. Check before emitting anything; how values are pushed to JavaScript as events is a separate topic. ## A checklist for any module that holds resources 1. **Acquire** Android resources (sensor listeners, receivers, callbacks) only when JavaScript asks, or in `initialize()`. 2. **Pause** them in `onHostPause()` if they are only useful in the foreground, and resume in `onHostResume()`. 3. **Release** all of them in `invalidate()`, and call `super.invalidate()`. 4. **Guard** every callback that reaches JavaScript with an active-instance check. 5. **Keep** synchronous getters, like `getLatestSteps()`, to cached values, so a JavaScript call never waits on hardware. ## Mistakes interviewers listen for - Storing `currentActivity` in a field "for later". - Casting the context to an Activity. - Registering the sensor in the constructor and never unregistering it. - Doing cleanup in `onHostDestroy` only, and missing reloads, which call `invalidate()` without destroying the Activity.
- Why is onHostDestroy not enough for cleanup?A JavaScript reload replaces the React instance and its modules without destroying the host Activity, so `onHostDestroy` never fires for it. React Native does call `invalidate()` on each Turbo Module before the old instance goes away. Put sensor and listener cleanup there, and treat `onHostDestroy` as an extra, not the only, exit.
- Where should the module register its LifecycleEventListener?In `initialize()`, which React Native calls after creating the module, or in the constructor, since the context already exists then. Pair it with `removeLifecycleEventListener` in `invalidate()`, so a module discarded by a reload does not keep receiving host callbacks.
saying these in an interview costs you the question
- ReactApplicationContext is the current Activity, so it can be cast to one
- Storing currentActivity in a field is fine if you null-check it later
- onHostDestroy covers JavaScript reloads, so invalidate() is unnecessary
- A sensor listener is unregistered automatically when the React instance reloads
- getCurrentActivity() on the module is the current recommended API