React Native Land_

// construindo apps mobile melhores

# Código nativo com Nitro Modules

2026-11-12#mug

Construímos um módulo de bateria com Expo Modules e com Turbo Modules. Vamos construí-lo mais uma vez com Nitro Modules, da Margelo. Ele continua lendo o nível da bateria e avisando o app quando ele muda.

O Nitro mantém a ideia dos Turbo Modules, uma spec em TypeScript que gera interfaces nativas, mas o gerador dele, o Nitrogen, as escreve direto para Swift e Kotlin. Sem Objective-C++ no meio, sem package para registrar à mão.

## Crie o módulo

O Nitrogen escreve um template de biblioteca com os arquivos de build já ligados:

npx nitrogen@latest init react-native-battery

Reduzido ao que importa aqui, ele tem esta forma:

react-native-battery/
  nitro.json
  src/specs/Battery.nitro.ts
  src/index.ts
  ios/HybridBattery.swift
  android/src/main/java/com/margelo/nitro/battery/HybridBattery.kt
  nitrogen/generated/

O nitro.json dá nome aos namespaces do módulo e diz qual classe implementa cada spec em cada plataforma:

nitro.jsonjson
{
  "cxxNamespace": ["battery"],
  "ios": { "iosModuleName": "NitroBattery" },
  "android": {
    "androidNamespace": ["battery"],
    "androidCxxLibName": "NitroBattery"
  },
  "autolinking": {
    "Battery": { "swift": "HybridBattery", "kotlin": "HybridBattery" }
  }
}

## A spec

Um módulo Nitro é um hybrid object: uma interface TypeScript, implementada no nativo, que o JavaScript segura como qualquer outro objeto. A spec diz qual linguagem a implementa em cada plataforma:

src/specs/Battery.nitro.tsts
import type { HybridObject } from 'react-native-nitro-modules';

export interface Battery extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> {
  getLevel(): Promise<number>;
  addListener(onChange: (level: number) => void): () => void;
}

Não há eventos. Um callback é uma função como outra qualquer: o JavaScript passa uma para o addListener, e o código nativo devolve outra que a remove. Um método também pode devolver um valor na hora, sem promise, quando o lado nativo o tem à mão.

O Nitrogen transforma a spec num protocolo Swift e numa classe abstrata Kotlin, ambos chamados HybridBatterySpec:

npx nitrogen

A saída vai para nitrogen/generated/. Rode de novo sempre que a spec mudar.

## Swift

A classe implementa o protocolo gerado, nada mais. number é Double nas duas plataformas, e um método da spec pode lançar erro:

ios/HybridBattery.swiftswift
import NitroModules
import UIKit

class HybridBattery: HybridBatterySpec {
  func getLevel() throws -> Promise<Double> {
    return Promise.async {
      await MainActor.run {
        UIDevice.current.isBatteryMonitoringEnabled = true
        return Double(UIDevice.current.batteryLevel)
      }
    }
  }

  func addListener(onChange: @escaping (_ level: Double) -> Void) throws -> () -> Void {
    DispatchQueue.main.async {
      UIDevice.current.isBatteryMonitoringEnabled = true
    }
    let observer = NotificationCenter.default.addObserver(
      forName: UIDevice.batteryLevelDidChangeNotification,
      object: nil,
      queue: .main
    ) { _ in
      onChange(Double(UIDevice.current.batteryLevel))
    }
    return {
      NotificationCenter.default.removeObserver(observer)
    }
  }
}

UIDevice pertence à thread principal, então MainActor.run o lê nela. Um callback do Nitro pode ser chamado de qualquer thread; o Nitro o leva de volta para a do JavaScript. No simulador o nível é -1: ele não tem bateria.

## Kotlin

Os mesmos dois métodos no Android, cada listener com um broadcast receiver próprio:

android/.../HybridBattery.ktkotlin
package com.margelo.nitro.battery

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import androidx.annotation.Keep
import com.facebook.proguard.annotations.DoNotStrip
import com.margelo.nitro.NitroModules
import com.margelo.nitro.core.Promise

@Keep
@DoNotStrip
class HybridBattery : HybridBatterySpec() {
  private val context: Context
    get() = NitroModules.applicationContext ?: throw IllegalStateException("React Native is not ready")

  override fun getLevel(): Promise<Double> = Promise.async {
    val intent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
    intent?.let { levelOf(it) } ?: -1.0
  }

  override fun addListener(onChange: (level: Double) -> Unit): () -> Unit {
    val receiver = object : BroadcastReceiver() {
      override fun onReceive(context: Context, intent: Intent) {
        onChange(levelOf(intent))
      }
    }
    context.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
    return { context.unregisterReceiver(receiver) }
  }

  private fun levelOf(intent: Intent): Double {
    val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
    val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
    return if (level < 0 || scale <= 0) -1.0 else level / scale.toDouble()
  }
}

@Keep e @DoNotStrip impedem o R8 de remover uma classe que só o código nativo referencia. Nada fica escutando sem o JavaScript pedir: o receiver vive do addListener até a função que ele devolveu ser chamada.

## TypeScript

A entrada do módulo cria o hybrid object pelo nome dado no nitro.json:

src/index.tsts
import { NitroModules } from 'react-native-nitro-modules';
import type { Battery } from './specs/Battery.nitro';

export const battery = NitroModules.createHybridObject<Battery>('Battery');

O hook é o de antes, com a função do próprio listener para cancelar a inscrição:

useBatteryLevel.tsts
import { useEffect, useState } from 'react';
import { battery } from 'react-native-battery';

export function useBatteryLevel(): number | null {
  const [level, setLevel] = useState<number | null>(null);

  useEffect(() => {
    let active = true;
    battery.getLevel().then((value) => {
      if (active) setLevel(value);
    });
    const remove = battery.addListener((value) => setLevel(value));
    return () => {
      active = false;
      remove();
    };
  }, []);

  return level;
}
BatteryBadge.tsxtsx
import { Text } from 'react-native';
import { useBatteryLevel } from './useBatteryLevel';

export function BatteryBadge() {
  const level = useBatteryLevel();
  if (level === null || level < 0) return <Text>Bateria: desconhecida</Text>;
  return <Text>Bateria: {Math.round(level * 100)}%</Text>;
}

## Rode

O app precisa do runtime do Nitro ao lado do módulo, e depois de um build nativo:

npm install react-native-nitro-modules ./react-native-battery
cd ios && bundle exec pod install && cd ..
npx react-native run-ios
npx react-native run-android

## Três caminhos, uma bateria

  • Expo Modules: o menor código. Uma definição por plataforma, Swift e Kotlin, ganchos para o primeiro e o último listener, e nada para gerar.
  • Turbo Modules: nada além do React Native. A spec é conferida pelo Codegen no build, mas o iOS passa por Objective-C++, e o Android pede um package registrado à mão.
  • Nitro Modules: uma spec como a dos Turbo Modules, implementada direto em Swift e Kotlin. Callbacks são funções comuns, e um hybrid object é um objeto, então pode haver vários. O custo é uma dependência e um gerador que você mesmo roda.

A documentação do Nitro vai além: hybrid views, objetos passados entre módulos e módulos em C++.