Saltar al contenido principal

Cómo detectar el estado de red en una app con Capacitor

Detecta el estado de la red en una app Capacitor: lee el estado online/offline, escucha los cambios, verifica el acceso real a internet y gestiona redes con límite de datos.

Detectar el estado de red online y offline en una app móvil de Capacitor

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 actualizaciones. Esto te indica si el dispositivo tiene conexión y si es Wi-Fi o celular. Para saber si internet realmente funciona o si la conexión es limitada, añade una comprobación de accesibilidad 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 dice nada sobre el acceso a internet
¿Qué tipo de enlace es? connectionType wifi, cellular, none, unknown en el plugin oficial
¿La internet realmente funciona? Verificación HTTP o internetReachable Requiere una petición o validación del SO
¿Debería evitar descargas pesadas? expensive, constrained Redes con límite de datos y Low Data Mode en 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.

Consulta 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 la caché o desde la red. Responde en pocos milisegundos porque lee el estado del SO, no genera tráfico.

Escuchar 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 none antes de que el nuevo enlace esté listo. Un debounce de 1 a 2 segundos antes de mostrar el banner de “offline” evita el parpadeo.
  • 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, pero el offline espera 1,5 segundos. Este patrón tambié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. Se mantiene en true cuando:

  • la red Wi-Fi tiene un portal cautivo (aeropuertos, hoteles, trenes)
  • el router no tiene conexión upstream
  • DNS está roto o filtrado
  • un firewall corporativo o una VPN bloquea tu dominio API
  • tu propio backend está caído

La solución es una comprobación de accesibilidad contra algo que tú controlas:

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 informes connected: true, y después de que una solicitud falle. No hagas polling cada pocos segundos: agota la batería y los datos. Asegúrate de que el endpoint de salud permita el origen 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 completo con diagnósticos de red

@capgo/capacitor-network-diagnostics lee más de lo que sabe el SO y puede ejecutar comprobaciones nativas que evitan el CORS de 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 comprobó 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 reportado
vpn tipo Sí Sí

En Android, internetReachable es una señal sólida porque el SO ya comprobó la conexión. En iOS está más cerca de connected, así que mantén la comprobación HTTP para flujos críticos.

Comprobaciones de conectividad nativa

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 portal cautivo o una conexión lenta.

Adaptarse a conexiones con límite de datos 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 gratis.

Crea un banner 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 específica de cada framework, consulta 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:

  1. Sirve las lecturas desde el almacenamiento local. Una base de datos SQLite como @capgo/capacitor-fast-sql gestiona grandes datasets mejor que localStorage.
  2. Escribe las acciones del usuario en una tabla outbox con un timestamp y una idempotency key.
  3. En networkStatusChange pase a conectado, ejecuta canReachApi(), luego procesa la bandeja de salida en orden.
  4. Reintenta las solicitudes fallidas con backoff exponencial, limitado a unos pocos minutos.

Para subidas que deben sobrevivir al cierre de la app, pásalas a un uploader nativo en lugar de fetch.

Fallback web

@capacitor/network también se ejecuta en el navegador, donde se basa en navigator.onLine y la online y offline events. Eso significa que tu code funciona en bun run dev y en una PWA sin cambios, pero connectionType a menudo será unknown en la web. Chrome navigator.connection exposes 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 simulador de iOS: el simulador comparte la red del Mac. Activa o desactiva el Wi-Fi en el Mac o prueba en un dispositivo con 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 debounce a la transición a modo offline, como se muestra arriba.

La comprobación de estado falla en el dispositivo pero funciona en el navegador: CORS. Permite capacitor://localhost y https://localhost, o usa NetworkDiagnostics.testUrl, que se ejecuta de forma nativa.

navigator.onLine es true mientras el plugin indica que está offline: confía en el plugin. Algunos WebViews de Android no actualizan navigator.onLine de forma fiable.

Publica correcciones offline over the air

El manejo offline es mayormente JavaScript: umbrales, banners, lógica de reintento, reglas de caché. Con actualizaciones Live Update de Capgo, puedes ajustar esos parámetros para cada usuario el mismo día. Capgo descarga bundles de actualización en segundo plano y mantiene la versión actual si la descarga falla, por lo que una mala conexión nunca deja a los usuarios con una actualización a medio instalar. La documentación de diagnóstico de red enumera todas las opciones.

Live updates para apps Capacitor

Cuando un bug de la capa web está en producción, publica el fix a través de Capgo en lugar de esperar días a la aprobación de la app store. Los usuarios reciben la actualización en segundo plano mientras los cambios nativos siguen el flujo de revisión habitual.

Lo último de nuestro blog

Capgo te da la información clave que necesitas para crear una app móvil realmente profesional.