Saltar al contenido principal

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

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

Cómo detectar el estado de red en una app 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 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 none antes 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:

  1. Sirve lecturas desde el almacenamiento local. Una base de datos SQLite como @capgo/capacitor-fast-sql gestiona datasets grandes mejor que localStorage.
  2. Escribe las acciones del usuario en una tabla outbox con un timestamp y una idempotency key.
  3. En networkStatusChange a conectado, ejecuta canReachApi(), luego vacía el outbox en orden.
  4. 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.

Live Updates para apps de Capacitor

Si hay un bug en la capa web, publica el fix con 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 proceso de revisión normal.

Lo último de nuestro blog

Capgo te da los mejores insights para crear una app móvil realmente profesional.