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
EventEmittersurface, no Fabric view orViewConfiginvolved. Like sensors, battery exposes three independent subscriptions (level, state, low-power-mode) rather than one combined hook — matching upstream’s ownuseBatteryLevel/useBatteryState/useLowPowerModebeing three separate hooks, not one.
| OS platform | Support |
|---|---|
| iOS | ✅ live |
| Android | ✅ live |
| Framework adapter | Support |
|---|---|
| React | ✅ live |
| Vue | ✅ live |
| Angular | ✅ live |
| Svelte | ✅ live |
Installation
Section titled “Installation”npm install @symbiote-native/batteryexpo-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).
Battery level
Section titled “Battery level”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>;}<script setup lang="ts">import { useBatteryLevel } from '@symbiote-native/battery/vue';
const batteryLevel = useBatteryLevel(); // Ref<number>, -1 until the first reading arrives</script>
<template> <Text>{{ batteryLevel }}</Text></template>import { Component, inject } from '@angular/core';import { Text } from '@symbiote-native/angular';import { BatteryLevelService } from '@symbiote-native/battery/angular';
@Component({ standalone: true, imports: [Text], template: `<Text>{{ batteryLevel() }}</Text>`,})export class BatteryLevelDisplay { readonly batteryLevel = inject(BatteryLevelService).connect();}<script lang="ts"> import { Text } from '@symbiote-native/svelte'; import { useBatteryLevel } from '@symbiote-native/battery/svelte';
const batteryLevel = useBatteryLevel(); // { readonly current: number }, -1 until the first reading arrives</script>
<Text>{batteryLevel.current}</Text>Battery state
Section titled “Battery state”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>;}<script setup lang="ts">import { useBatteryState } from '@symbiote-native/battery/vue';import { BatteryState } from '@symbiote-native/battery';
const batteryState = useBatteryState();</script>
<template> <Text>{{ batteryState === BatteryState.CHARGING ? 'Charging' : 'Not charging' }}</Text></template>import { Component, inject } from '@angular/core';import { Text } from '@symbiote-native/angular';import { BatteryStateService } from '@symbiote-native/battery/angular';import { BatteryState } from '@symbiote-native/battery';
@Component({ standalone: true, imports: [Text], template: `<Text>{{ batteryState() === BatteryState.CHARGING ? 'Charging' : 'Not charging' }}</Text>`,})export class ChargingIndicator { // Angular templates can't reach a module-level import directly — expose the enum as a field. readonly BatteryState = BatteryState; readonly batteryState = inject(BatteryStateService).connect();}<script lang="ts"> import { Text } from '@symbiote-native/svelte'; import { useBatteryState } from '@symbiote-native/battery/svelte'; import { BatteryState } from '@symbiote-native/battery';
const batteryState = useBatteryState(); // { readonly current: BatteryState }, seeded UNKNOWN</script>
<Text>{batteryState.current === BatteryState.CHARGING ? 'Charging' : 'Not charging'}</Text>Low power mode
Section titled “Low power mode”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>;}<script setup lang="ts">import { useLowPowerMode } from '@symbiote-native/battery/vue';
const lowPowerMode = useLowPowerMode();</script>
<template> <Text>{{ lowPowerMode ? 'Low Power Mode is on' : null }}</Text></template>import { Component, inject } from '@angular/core';import { Text } from '@symbiote-native/angular';import { LowPowerModeService } from '@symbiote-native/battery/angular';
@Component({ standalone: true, imports: [Text], template: `<Text>{{ lowPowerMode() ? 'Low Power Mode is on' : null }}</Text>`,})export class LowPowerBadge { readonly lowPowerMode = inject(LowPowerModeService).connect();}<script lang="ts"> import { Text } from '@symbiote-native/svelte'; import { useLowPowerMode } from '@symbiote-native/battery/svelte';
const lowPowerMode = useLowPowerMode(); // { readonly current: boolean }, seeded false</script>
<Text>{lowPowerMode.current ? 'Low Power Mode is on' : null}</Text>One-shot functions
Section titled “One-shot functions”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();Stateless functions
Section titled “Stateless functions”| 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
Section titled “Hooks / composables / services”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.
BatteryState
Section titled “BatteryState”| 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 |
PowerState
Section titled “PowerState”| 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. addBatteryLevelListenerfires far less often on Android. The Android receiver subscribes toACTION_BATTERY_LOWandACTION_BATTERY_OKAYonly, so it reports crossing those thresholds and nothing in between; iOS listens toUIDevice.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.
isSupportedis compiled tofalseundertargetEnvironment(simulator), but the level and state functions have no simulator branch at all — they readUIDevice.currentstraight through and resolve whatever it reports there. Gate onisAvailableAsync()rather than treating a-1level as a real reading.
How the wrapper works
Section titled “How the wrapper works”@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, LowPowerModeServiceEach 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).