Para detectar el estado de red en una app de Capacitor, instala @capacitor/network, llama a Network.getStatus() para el estado actual, y suscríbete a networkStatusChange para recibir actualizaciones. Esto te indica si el dispositivo tiene conexión y si es Wi-Fi o datos móviles. Para saber si el internet funciona realmente, o si la conexión tiene límites de datos, añade un check de reachability o @capgo/capacitor-network-diagnostics. Esta guía cubre las tres capas con Capacitor 8 code.
Las tres preguntas detrás de “¿está el usuario online?”
| Pregunta | API | Notas |
|---|---|---|
| ¿Hay una interfaz de red activa? | Network.getStatus().connected |
Rápido, basado en eventos, no indica el acceso a internet |
| ¿Qué tipo de conexión es? | connectionType |
wifi, cellular, none, unknown en el plugin oficial |
| ¿Realmente funciona el internet? | Comprobación HTTP o internetReachable |
Requiere una petición o validación del SO |
| ¿Debería evitar descargas grandes? | expensive, constrained |
Redes con límite de datos y Low Data Mode de iOS |
La mayoría de los bugs offline ocurren al confundir la primera fila con la tercera. Un Wi-Fi de hotel con portal cautivo reporta connected: true mientras cada llamada a API da timeout.
Instala el plugin Network
bun add @capacitor/network
bunx cap sync
No requiere configuración. En Android, el plugin añade ACCESS_NETWORK_STATE al manifest combinado, un permiso normal concedido al instalar. iOS no requiere nada.
Lee el estado actual
import { Network } from '@capacitor/network';
const status = await Network.getStatus();
console.log(status.connected); // true or false
console.log(status.connectionType); // 'wifi' | 'cellular' | 'none' | 'unknown'
Llámalo al iniciar la app antes de decidir si cargar desde cache o red. Devuelve en milisegundos porque lee el estado del SO, no genera tráfico.
Escucha cambios de red
import { Network, type ConnectionStatus } from '@capacitor/network';
import type { PluginListenerHandle } from '@capacitor/core';
let handle: PluginListenerHandle | undefined;
export async function watchNetwork(onChange: (s: ConnectionStatus) => void) {
onChange(await Network.getStatus());
handle = await Network.addListener('networkStatusChange', onChange);
}
export async function stopWatchingNetwork() {
await handle?.remove();
}
Dos detalles prácticos:
- Aplica un debounce al evento. Al cambiar de Wi-Fi a datos móviles, ambas plataformas pueden reportar brevemente
noneantes de que el nuevo enlace esté activo. Un debounce de 1 a 2 segundos evita el parpadeo del banner de "offline". - Elimina los listeners cuando un componente se desmonte, o ejecutarás el callback varias veces tras hot reloads y navegación.
Hook de React
import { useEffect, useState } from 'react';
import { Network, type ConnectionStatus } from '@capacitor/network';
export function useNetworkStatus() {
const [status, setStatus] = useState<ConnectionStatus>({ connected: true, connectionType: 'unknown' });
useEffect(() => {
let timer: ReturnType<typeof setTimeout>;
const listener = Network.addListener('networkStatusChange', (s) => {
clearTimeout(timer);
timer = setTimeout(() => setStatus(s), s.connected ? 0 : 1500);
});
Network.getStatus().then(setStatus);
return () => {
clearTimeout(timer);
listener.then((l) => l.remove());
};
}, []);
return status;
}
El estado online se aplica de inmediato, el offline espera 1.5 segundos. El mismo patrón funciona como un composable de Vue o un servicio de Angular con un BehaviorSubject.
Por qué “connected” no significa online
connected: true proviene del stack de red del SO. Esto ocurre cuando:
- la red Wi-Fi tiene un captive portal (aeropuertos, hoteles, trenes)
- el router no tiene conexión de subida
- El DNS está roto o filtrado
- un firewall corporativo o VPN bloquea tu dominio API
- tu propio backend está caído
La solución es un check de conectividad contra algo que controles:
export async function canReachApi(timeoutMs = 4000): Promise<boolean> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch('https://api.example.com/health', {
method: 'HEAD',
cache: 'no-store',
signal: controller.signal,
});
return res.ok;
} catch {
return false;
} finally {
clearTimeout(timer);
}
}
Ejecútalo al iniciar la app, cuando networkStatusChange reporta connected: true, y tras un error en una petición. No hagas polling cada pocos segundos, consume batería y datos. Asegúrate de que el endpoint de health permita el origin de tu app en CORS ("capacitor://localhost en iOS, https://localhost en Android por defecto), o usa CapacitorHttp para enviarlo desde code nativo.
Obtén un estado más detallado con network diagnostics
@capgo/capacitor-network-diagnostics lee más información del OS y puede ejecutar checks nativos que evitan el CORS del WebView:
bun add @capgo/capacitor-network-diagnostics
bunx cap sync
import { NetworkDiagnostics } from '@capgo/capacitor-network-diagnostics';
const status = await NetworkDiagnostics.getNetworkStatus();
// {
// connected: true,
// connectionType: 'wifi' | 'cellular' | 'ethernet' | 'vpn' | 'other' | 'unknown' | 'none',
// internetReachable: true,
// expensive: false, // metered / cellular
// constrained: false, // iOS Low Data Mode
// captivePortal: false, // Android only
// details: { ... }
// }
Cómo se mapean los campos al SO:
| Campo | Android | iOS |
|---|---|---|
internetReachable |
NET_CAPABILITY_VALIDATED (Android sondeó internet) |
Refleja el NWPathMonitor estado de la ruta |
expensive |
isActiveNetworkMetered() |
NWPath.isExpensive |
constrained |
Siempre false |
NWPath.isConstrained (Modo de datos reducidos) |
captivePortal |
NET_CAPABILITY_CAPTIVE_PORTAL |
No se reporta |
vpn tipo |
Sí | Sí |
En Android, internetReachable es una señal sólida porque el SO ya probó la conexión. En iOS es más parecido a connected, así que mantén el check de HTTP para flujos críticos.
Los checks de reachability nativos
const api = await NetworkDiagnostics.testUrl({
url: 'https://api.example.com/health',
method: 'HEAD',
timeoutMs: 4000,
});
// api.reachable, api.statusCode, api.durationMs, api.errorCode
const socket = await NetworkDiagnostics.testWebSocket({ url: 'wss://realtime.example.com' });
const port = await NetworkDiagnostics.testPort({ host: 'mqtt.example.com', port: 8883 });
Las peticiones nativas no están sujetas a CORS, así que también puedes probar hosts de terceros.
Una pantalla de "reporte de red" para soporte
Cuando los usuarios dicen "la app no funciona", un reporte de diagnóstico con un solo toque ahorra horas de ida y vuelta:
const report = await NetworkDiagnostics.runDiagnostics({
urls: [{ url: 'https://api.example.com/health' }, { url: 'https://cdn.example.com/ping' }],
websockets: [{ url: 'wss://realtime.example.com' }],
download: { url: 'https://cdn.example.com/1mb.bin', maxBytes: 1_048_576 },
packetLoss: { url: 'https://api.example.com/health', count: 5 },
});
console.log(report.issues); // human-readable list of problems
console.log(report.download?.mbps);
console.log(report.packetLoss?.lossPercent);
Adjunta el JSON a un ticket de soporte. Verás de inmediato si el problema es DNS, un WebSocket bloqueado, un captive portal o una conexión lenta.
Adaptarse a conexiones con datos limitados y Low Data Mode
Respetar los límites de datos es una victoria rápida para las reseñas en mercados con datos móviles caros:
export async function shouldPrefetchMedia() {
const s = await NetworkDiagnostics.getNetworkStatus();
if (!s.connected) return false;
if (s.constrained) return false; // user asked iOS to save data
if (s.expensive) return false; // cellular or metered Wi-Fi
return true;
}
Úsalo para decidir el autoplay de video, la calidad de imagen, la sincronización en segundo plano y las descargas grandes. En Android, los usuarios pueden marcar una red Wi-Fi como limitada, así que no asumas que el Wi-Fi es gratuito.
Crea un banner de modo offline
Un banner pequeño es más claro que las peticiones fallidas. Usando el hook anterior:
export function OfflineBanner() {
const { connected } = useNetworkStatus();
if (connected) return null;
return (
<div role="status" aria-live="polite" className="offline-banner">
You are offline. Changes will sync when you reconnect.
</div>
);
}
role="status" con aria-live="polite" hace que los lectores de pantalla anuncien el cambio. Para UI de un framework específico, mira nuestra guía para crear una pantalla offline en Vue, Angular y React.
Encola tareas mientras estés offline
La detección es solo la mitad del soporte offline. Cuando el estado pase a offline:
- Sirve lecturas desde el almacenamiento local. Una base de datos SQLite como
@capgo/capacitor-fast-sqlgestiona datasets grandes mejor quelocalStorage. - Escribe las acciones del usuario en una tabla outbox con un timestamp y una idempotency key.
- En
networkStatusChangea conectado, ejecutacanReachApi(), luego vacía el outbox en orden. - Reintenta las peticiones fallidas con exponential backoff, limitado a pocos minutos.
Para uploads que deban persistir aunque se cierre la app, usa un uploader nativo en vez de fetch.
Fallback web
@capacitor/network también se ejecuta en el navegador, donde se basa en navigator.onLine y el online y offline eventos. Eso significa que tu code funciona en bun run dev y en una PWA sin cambios, pero connectionType will often be unknown en la web. El de Chrome navigator.connection expone effectiveType y saveData, Safari no lo hace, así que trátalos como sugerencias opcionales.
Solución de problemas
El listener nunca se dispara en el iOS Simulator: el simulador comparte la red del Mac. Activa/desactiva el Wi-Fi en el Mac o prueba en un dispositivo en modo avión.
connectionType es unknown en una VPN: el plugin oficial solo reporta wifi, cellular, none y unknown. Usa el plugin de diagnóstico, que devuelve vpn y ethernet.
La app aparece offline un segundo al cambiar de red: aplica un debounce a la transición offline como se muestra arriba.
El health check falla en el dispositivo pero funciona en el navegador: CORS. Permitir capacitor://localhost y https://localhost, o usa NetworkDiagnostics.testUrl, que se ejecuta de forma nativa.
navigator.onLine es true mientras el plugin indica offline: confía en el plugin. Algunos WebViews de Android no actualizan navigator.onLine de forma fiable.
Lanza fixes offline over the air
El manejo de offline es mayormente JavaScript: umbrales, banners, lógica de reintento y reglas de caché. Con Live Updates de Capgo, puedes ajustarlos para cada usuario el mismo día. Capgo descarga los bundles de actualización en segundo plano y mantiene la versión actual si falla la descarga, así una mala conexión nunca dejará a los usuarios con una actualización a medias. La documentación de diagnóstico de red enumera cada opción.