Android 17 corta el acceso a la red local: qué cambia para tu app Flutter (y a quién no le afecta)

Desde Android 17 (API 37), una app que quiera descubrir o conectarse a dispositivos de la red local del usuario necesita un permiso nuevo, ACCESS_LOCAL_NETWORK, y Google Play pide una declaración para publicarla. Si tu app solo habla con tu backend por internet, no tienes que hacer nada. Si descubre impresoras, Chromecasts, sensores o PLCs en la Wi-Fi del usuario, te afecta y tienes trabajo por delante.

Qué ha pasado

El 29 de octubre de 2026, Google publicó su tanda de cambios de políticas de Play. Entre ellos, restringe el permiso de red local a las apps que lo necesitan para su funcionalidad principal "cuando los selectores de dispositivos no son suficientes", y obliga a las apps que apuntan a Android 17 (API 37+) a enviar una declaración en Play Console para usarlo. Google da al menos 30 días desde el 29 de octubre para adaptarse.

Por debajo está el cambio de plataforma. En Android 17, las protecciones de red local pasan a ser obligatorias para cualquier app que suba su target SDK a 37. El acceso a la LAN queda bloqueado por defecto y hay que pedir el permiso ACCESS_LOCAL_NETWORK, que vive en el grupo NEARBY_DEVICES. En Android 16 esto era opt-in y se probaba con NEARBY_WIFI_DEVICES; ahora se aplica de verdad.

Qué cuenta como "red local"

El bloqueo está metido en el stack de red, así que afecta a todas las APIs: sockets de plataforma, OkHttp, Cronet y cualquier cosa construida encima. Necesitan permiso:

  • Conexiones TCP salientes y entrantes a direcciones de la LAN.
  • UDP unicast, multicast y broadcast, en ambos sentidos.
  • mDNS, SSDP y resolver nombres con sufijo .local.
  • NsdManager y el descubrimiento de servicios.

Queda fuera el tráfico a internet y a la red móvil, que siguen funcionando igual, y el DNS local en el puerto 53. En Dart, un Socket.connect() a una IP local sin permiso no revienta con un error claro: se queda en un timeout en TCP (o un EPERM en UDP) que aflora como SocketException. Es un fallo silencioso, y por eso es peligroso: no lo ves en un crash, lo ve tu usuario cuando la impresora "no aparece".

A quién afecta y a quién no

No te afecta si tu app es cien por cien cloud, que son la mayoría. Hablar con tu API por HTTPS no toca la red local.

Te afecta si descubres o conectas con hardware en la misma Wi-Fi: casting y media, domótica e IoT, impresión local, terminales de almacén que buscan su base, instalaciones industriales que leen un PLC por MQTT o Modbus en la LAN de planta, o puntos de recarga que hablan OCPP por la red local. Cualquier descubrimiento por mDNS entra aquí.

Hay un matiz que ahorra sustos. Si tu app todavía apunta a target 36 o inferior, con el permiso INTERNET mantiene el acceso implícito a la red local. Es temporal: en cuanto subas el target a 37, se corta. La consecuencia práctica es clara: no añadas ACCESS_LOCAL_NETWORK ni lo pidas en runtime hasta que targetees 37, porque antes no hace nada.

Los dos caminos

Android deja dos salidas, y la elección cambia bastante el coste.

Camino A, el selector del sistema. Si lo que necesitas es que el usuario elija un dispositivo, el sistema lo descubre por ti y te ahorras el permiso. Para casting, el Output Switcher de Google Cast. Para descubrimiento general e IoT, NsdManager con el flag FLAG_SHOW_PICKER: el sistema muestra un diálogo, el usuario elige, y te conectas a la IP de ese dispositivo sin ACCESS_LOCAL_NETWORK.

val request = DiscoveryRequest.Builder("_http._tcp")
    .setFlags(DiscoveryRequest.FLAG_SHOW_PICKER)
    .build()

nsdManager.registerServiceInfoCallback(request, executor, callback)
// La IP que devuelve el callback se conecta sin pedir permiso

Desde Flutter no hay plugin estándar para esto: va por platform channel, con el código Kotlin del lado nativo. A cambio te libras del permiso y de la declaración de Play.

Camino B, el permiso amplio. Para los casos que de verdad necesitan escanear toda la red de forma persistente (un hub de domótica, gestionar una flota de dispositivos), declaras el permiso, lo pides en runtime y escribes la declaración en Play Console.

<!-- android/app/src/main/AndroidManifest.xml -->
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
import 'dart:io';
import 'package:permission_handler/permission_handler.dart';

Future<Socket?> conectar(String host, int port) async {
  if (Platform.isAndroid) {
    final status = await Permission.accessLocalNetwork.request();
    if (status.isPermanentlyDenied) {
      await openAppSettings();
      return null;
    }
    if (!status.isGranted) return null; // sin permiso el socket va a fallar
  }
  return Socket.connect(host, port, timeout: const Duration(seconds: 5));
}

Un detalle que juega a favor: ACCESS_LOCAL_NETWORK está en el grupo NEARBY_DEVICES, así que una app que ya tenga concedido un permiso de ese grupo (Bluetooth, por ejemplo) no vuelve a preguntar.

Qué haríamos nosotros

La decisión cabe en una pregunta: ¿necesitas que el usuario elija un dispositivo, o escanear la red entera?

Si la respuesta es "elegir uno" (una impresora, un receptor de Cast, un sensor concreto), camino A. Menos fricción, sin permiso, sin declaración de Play y, de paso, sin un motivo más de rechazo en la revisión. Para la mayoría de apps de consumo que tocan la red local, esto basta.

Camino B solo cuando lo amplio es el requisito, no la comodidad. Una app de domótica que mantiene un mapa vivo de la casa lo necesita de verdad, y entonces la declaración de Play hay que redactarla bien: explicar la funcionalidad principal y por qué un selector no sirve. Vale la pena ser honesto con el coste del camino A: los selectores del sistema todavía tienen huecos (la guía de casting avanzado llegará "en una versión futura") y cambian tu UX, porque el usuario elige en un diálogo del sistema y no en tu pantalla.

Y una cosa que no dejaríamos para el final: pruébalo antes de subir el target. En un dispositivo con Android 16 puedes forzar el bloqueo con adb shell am compat enable RESTRICT_LOCAL_NETWORK <paquete> y ver qué se rompe. Es mejor descubrir el timeout tú que leerlo en una reseña de una estrella.

Esto va en la misma línea que el botón de ubicación obligatorio y la restricción del permiso de contactos: Google cierra permisos amplios y empuja hacia selectores donde el usuario decide dispositivo a dispositivo. Si tu app conecta con hardware, el patrón ya es predecible.

En Dribba llevamos desde 2011 montando apps Flutter que hablan con dispositivos reales, de terminales de almacén a instalaciones industriales. Si tienes una app que descubre cosas en la red local y no sabes por qué camino tirar, cuéntanos el caso.