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-batteryReduzido 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:
{
"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:
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 nitrogenA 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:
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:
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:
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:
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;
}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++.