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), each with its own hook, plus usePowerState, which merges all three into one PowerState value.
OS platform Support
iOS ✅ live
Android ✅ live
Framework adapter Support
React ✅ live
Vue ✅ live
Angular ✅ live
Svelte ✅ live
Solid ✅ live
Terminal window
npm install @symbiote-native/battery

Scaffolding or extending a SymbioteNative app? npx @symbiote-native/cli new --battery (or add --battery in an existing app) installs and wires this for you — see @symbiote-native/cli.

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 { 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 { 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 { 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

Hooks / composables / services / primitives

Section titled “Hooks / composables / services / primitives”
React (/react) Vue (/vue) Angular (/angular) Svelte (/svelte) Solid (/solid) Signature Returns
useBatteryLevel useBatteryLevel BatteryLevelService.connect() useBatteryLevel createBatteryLevel () Live battery level — number (React), Ref<number> (Vue), Signal<number> (Angular), { readonly current: number } (Svelte), Accessor<number> (Solid), seeded -1
useBatteryState useBatteryState BatteryStateService.connect() useBatteryState createBatteryState () Live BatteryState — plain value (React/Vue Ref), Signal<BatteryState> (Angular), { readonly current: BatteryState } (Svelte), Accessor<BatteryState> (Solid), seeded UNKNOWN
useLowPowerMode useLowPowerMode LowPowerModeService.connect() useLowPowerMode createLowPowerMode () Live boolean — plain value (React/Vue Ref), Signal<boolean> (Angular), { readonly current: boolean } (Svelte), Accessor<boolean> (Solid), seeded false
usePowerState usePowerState PowerStateService.connect() usePowerState createPowerState () Live PowerState ({ lowPowerMode, batteryLevel, batteryState }) in each adapter’s own box, seeded { false, -1, UNKNOWN }

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. Solid’s create* primitives return an Accessor for the same reason: call it as batteryLevel(), never destructure it, since a Solid component body runs once and a destructured value would freeze at its first reading. Svelte’s use* runes return a boxed getter, { readonly current }, read as batteryLevel.current — a bare $state handed out of a plain function loses its reactivity once destructured, since Svelte 5 runes are lexically scoped to the file that declares them.

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.
  • The level reads -1 (or the UI shows “-100%”). The level is unknown, typically on a simulator or unsupported device. Check for -1 before formatting and never treat it as low battery.
  • How do I keep it live? Read the initial level, then subscribe with the level listener and remove the subscription on unmount.
  • Low Power Mode? Use the low power mode query and its listener to reduce background work.

Sources: Expo docs: Battery, Battery -100% bug report, Expo Battery guide.

@symbiote-native/battery ships zero React/Vue/Angular/Svelte/Solid 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; watchPowerState merges the three for usePowerState;
| native-module.ts resolves the single `ExpoBattery` native module via
| expo-modules-core's requireNativeModule
|-- react/hooks/ @symbiote-native/battery/react - useBatteryLevel, useBatteryState, useLowPowerMode, usePowerState
|-- vue/composables/ @symbiote-native/battery/vue - same four names
|-- svelte/runes/ @symbiote-native/battery/svelte - same four names
|-- solid/primitives/ @symbiote-native/battery/solid - createBatteryLevel, createBatteryState, createLowPowerMode, createPowerState
`-- angular/services/ @symbiote-native/battery/angular - BatteryLevelService, BatteryStateService, LowPowerModeService, PowerStateService

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).