Skip to content

Battery

@symbiote-native/battery wraps expo-battery so every SymbioteNative adapter can read battery level, charging state, and low-power-mode. Unlike the slider (a native view) or splash screen (one imperative TurboModule), expo-battery is built on expo-modules-core — a pure async-function

  • EventEmitter surface, no Fabric view or ViewConfig involved. Like sensors, battery exposes three independent subscriptions (level, state, low-power-mode) rather than one combined hook — matching upstream’s own useBatteryLevel/useBatteryState/useLowPowerMode being three separate hooks, not one.
OS platform Support
iOS ✅ live
Android ✅ live
Framework adapter Support
React ✅ live
Vue ✅ live
Angular ✅ live
Svelte ✅ live
Terminal window
npm install @symbiote-native/battery

expo-battery and expo-modules-core come along as regular dependencies, pinned to exact versions — never install either yourself, and never add the expo meta-package to your project (it bundles its own Metro/Babel pipeline, which conflicts with this project’s own).

import { Text } from '@symbiote-native/react';
import { useBatteryLevel } from '@symbiote-native/battery/react';
export default function BatteryLevel() {
const batteryLevel = useBatteryLevel(); // number, -1 until the first reading arrives
return <Text>{batteryLevel}</Text>;
}
import { Text } from '@symbiote-native/react';
import { useBatteryState } from '@symbiote-native/battery/react';
import { BatteryState } from '@symbiote-native/battery';
export default function ChargingIndicator() {
const batteryState = useBatteryState();
return <Text>{batteryState === BatteryState.CHARGING ? 'Charging' : 'Not charging'}</Text>;
}
import { Text } from '@symbiote-native/react';
import { useLowPowerMode } from '@symbiote-native/battery/react';
export default function LowPowerBadge() {
const lowPowerMode = useLowPowerMode();
return <Text>{lowPowerMode ? 'Low Power Mode is on' : null}</Text>;
}

The stateless functions are already framework-agnostic — import them straight from the package root, on any adapter:

import {
isAvailableAsync,
getPowerStateAsync,
isBatteryOptimizationEnabledAsync, // Android only
} from '@symbiote-native/battery';
const available = await isAvailableAsync();
const { batteryLevel, batteryState, lowPowerMode } = await getPowerStateAsync();
Signature Description
isAvailableAsync(): Promise<boolean> Whether the battery API is available on this device — false on an iOS Simulator
getBatteryLevelAsync(): Promise<number> Battery level between 0 and 1, inclusive, or -1 if the device can’t report it
getBatteryStateAsync(): Promise<BatteryState> Current charging state — see the BatteryState table below
isLowPowerModeEnabledAsync(): Promise<boolean> Whether Low Power Mode (iOS) / Power Saver (Android) is currently on
isBatteryOptimizationEnabledAsync(): Promise<boolean> Whether Android battery optimization is enabled for this app (background tasks may be affected in doze mode) — always resolves false on iOS
getPowerStateAsync(): Promise<PowerState> Combined { batteryLevel, batteryState, lowPowerMode } snapshot, gathered with a single Promise.all over the three calls above
addBatteryLevelListener(listener): EventSubscription Subscribes to battery level change events; call .remove() on the returned subscription to unsubscribe
addBatteryStateListener(listener): EventSubscription Subscribes to battery state (charging/full/unplugged/unknown) change events
addLowPowerModeListener(listener): EventSubscription Subscribes to Low Power Mode / Power Saver toggle events
React (/react) Vue (/vue) Angular (/angular) Signature Returns
useBatteryLevel useBatteryLevel BatteryLevelService.connect() () Live battery level — number (React), Ref<number> (Vue), Signal<number> (Angular), seeded -1
useBatteryState useBatteryState BatteryStateService.connect() () Live BatteryState — plain value (React/Vue Ref), Signal<BatteryState> (Angular), seeded UNKNOWN
useLowPowerMode useLowPowerMode LowPowerModeService.connect() () Live boolean — plain value (React/Vue Ref), Signal<boolean> (Angular), seeded false

Angular’s connect() returns a Signal — read it as batteryLevel() in code or in a template, same as every other connect()-based service in this project.

Member Value Description
UNKNOWN 0 Battery state is unknown or inaccessible
UNPLUGGED 1 Discharging, not connected to power (Android: BATTERY_STATUS_DISCHARGING)
CHARGING 2 Battery is charging
FULL 3 Battery level is full
NOT_CHARGING 4 Power connected (AC/USB/wireless) but not actually charging, e.g. a charge-limit or optimized-charging pause — Android only
Field Type Description
batteryLevel number 0..1, inclusive, or -1 if the battery level is unknown
batteryState BatteryState The current charging state, see BatteryState above
lowPowerMode boolean true if Low Power Mode / Power Saver is on, false otherwise
  • Android stops delivering events while the app is backgrounded. The native module unregisters its broadcast receivers when the activity enters the background and registers them again on return, and emits no catch-up event — so after a return to the foreground your listener still holds the last value it saw until the next real change. Re-seed from getPowerStateAsync() if you need to be current at that moment.
  • addBatteryLevelListener fires far less often on Android. The Android receiver subscribes to ACTION_BATTERY_LOW and ACTION_BATTERY_OKAY only, so it reports crossing those thresholds and nothing in between; iOS listens to UIDevice.batteryLevelDidChangeNotification, which fires on every reported percentage change. Don’t build a percentage readout on the Android event alone.
  • The iOS Simulator resolves sentinels instead of failing. isSupported is compiled to false under targetEnvironment(simulator), but the level and state functions have no simulator branch at all — they read UIDevice.current straight through and resolve whatever it reports there. Gate on isAvailableAsync() rather than treating a -1 level as a real reading.

@symbiote-native/battery ships zero React/Vue/Angular logic in expo-battery itself — that package’s own JS hard-imports the expo meta-package (which this project never installs), so its free functions and event-name constants are hand-ported, verbatim, into this package’s own core/, changing only the one import line that now pulls EventSubscription from expo-modules-core instead of expo:

packages/battery/src/
├── core/ isAvailableAsync/getBatteryLevelAsync/getBatteryStateAsync/
│ isLowPowerModeEnabledAsync/isBatteryOptimizationEnabledAsync/
│ getPowerStateAsync + addBatteryLevelListener/addBatteryStateListener/
│ addLowPowerModeListener; native-module.ts resolves the single
│ `ExpoBattery` native module via expo-modules-core's requireNativeModule
├── react/hooks/ @symbiote-native/battery/react — useBatteryLevel, useBatteryState, useLowPowerMode
├── vue/composables/ @symbiote-native/battery/vue — useBatteryLevel, useBatteryState, useLowPowerMode (same names)
├── svelte/runes/ @symbiote-native/battery/svelte — useBatteryLevel, useBatteryState, useLowPowerMode (same names)
└── angular/services/ @symbiote-native/battery/angular — BatteryLevelService, BatteryStateService, LowPowerModeService

Each adapter’s hook/composable/rune/service is a thin lifecycle wrapper — seed from the one-shot call, subscribe on mount, unsubscribe on unmount — over the same core functions; the subscription and seeding logic is written once and shared by all four, the same logic/lifecycle split as every other SymbioteNative component (see how it works). The native code itself is never vendored or copied — expo-modules-autolinking resolves it straight out of node_modules (see the native setup guide).