Às vezes o JavaScript não basta: você precisa de uma API que só a plataforma tem. A Expo Modules API permite escrever esse código em Swift e Kotlin com o mesmo formato dos dois lados, e chamá-lo do TypeScript como qualquer outro módulo.
Vamos construir um pequeno. Ele lê o nível da bateria e avisa o app quando ele muda.
## Crie o módulo
Num app Expo, um módulo local fica em modules/ e é ligado no próximo build nativo:
npx create-expo-module@latest --localDê a ele o nome battery. Reduzido ao que importa aqui, ele tem esta forma:
modules/battery/
expo-module.config.json
index.ts
src/BatteryModule.ts
ios/BatteryModule.swift
android/src/main/java/expo/modules/battery/BatteryModule.ktO arquivo de configuração diz ao autolinking qual classe carregar em cada plataforma:
{
"platforms": ["apple", "android"],
"apple": { "modules": ["BatteryModule"] },
"android": { "modules": ["expo.modules.battery.BatteryModule"] }
}## Swift
Um módulo é uma classe com uma definition(). Name é o nome que o JavaScript pede, AsyncFunction devolve uma promise e Events declara o que o módulo pode enviar:
import ExpoModulesCore
import UIKit
public class BatteryModule: Module {
private var observer: NSObjectProtocol?
public func definition() -> ModuleDefinition {
Name("Battery")
Events("onLevelChange")
AsyncFunction("getLevelAsync") { () -> Float in
UIDevice.current.isBatteryMonitoringEnabled = true
return UIDevice.current.batteryLevel
}.runOnQueue(.main)
OnStartObserving {
UIDevice.current.isBatteryMonitoringEnabled = true
self.observer = NotificationCenter.default.addObserver(
forName: UIDevice.batteryLevelDidChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.sendEvent("onLevelChange", ["level": UIDevice.current.batteryLevel])
}
}
OnStopObserving {
if let observer = self.observer {
NotificationCenter.default.removeObserver(observer)
}
self.observer = nil
}
}
}UIDevice pertence à thread principal, então runOnQueue(.main) executa a função nela. No simulador o nível é -1: ele não tem bateria.
## Kotlin
A mesma definição, palavra por palavra, no Android. Não há um callback de nível de bateria para observar, então o módulo escuta o broadcast ACTION_BATTERY_CHANGED do sistema:
package expo.modules.battery
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
class BatteryModule : Module() {
private val context: Context
get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
sendEvent("onLevelChange", mapOf("level" to levelOf(intent)))
}
}
override fun definition() = ModuleDefinition {
Name("Battery")
Events("onLevelChange")
AsyncFunction("getLevelAsync") {
val intent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
intent?.let { levelOf(it) } ?: -1f
}
OnStartObserving {
context.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
}
OnStopObserving {
context.unregisterReceiver(receiver)
}
}
private fun levelOf(intent: Intent): Float {
val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
return if (level < 0 || scale <= 0) -1f else level / scale.toFloat()
}
}OnStartObserving e OnStopObserving rodam quando o JavaScript adiciona o primeiro listener e remove o último, então nada fica escutando enquanto ninguém tem interesse.
## TypeScript
Uma só declaração tipa as duas plataformas:
import { NativeModule, requireNativeModule } from 'expo';
type BatteryEvents = {
onLevelChange(event: { level: number }): void;
};
declare class BatteryModule extends NativeModule<BatteryEvents> {
getLevelAsync(): Promise<number>;
}
export default requireNativeModule<BatteryModule>('Battery');O index.ts do módulo a reexporta, e um hook esconde a inscrição:
import { useEffect, useState } from 'react';
import Battery from './modules/battery';
export function useBatteryLevel(): number | null {
const [level, setLevel] = useState<number | null>(null);
useEffect(() => {
let active = true;
Battery.getLevelAsync().then((value) => {
if (active) setLevel(value);
});
const subscription = Battery.addListener('onLevelChange', (event) => setLevel(event.level));
return () => {
active = false;
subscription.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 Expo Go só tem o código nativo com que é distribuído, então um módulo seu precisa de um development build:
npx expo run:ios
npx expo run:androidPronto: uma definição em duas linguagens e um módulo tipado no JavaScript. A documentação da Expo Modules API cobre views, records e config plugins.