Cómo depurar peticiones HTTP en Flutter con DevTools

may. 1, 2026

Este artículo forma parte de una serie:

A lo largo de la serie de Flutter hemos visto cómo se estructura una aplicación, cómo construir interfaces con widgets, cómo apoyarnos en Dart, cómo resolver operaciones de asincronía, cómo integrar paquetes, cómo dar más vida a la UI con animaciones y cómo adaptar los textos con internacionalización. El esta ocasión veremos uno de los escenarios más habituales de cualquier app real: la comunicación con APIs y la depuración de errores cuando algo falla en el proceso.

En Flutter no basta con “hacer una petición” y esperar que todo encaje. Muchas veces la interfaz parece correcta, el estado se actualiza bien y el código compila sin problemas, pero la aplicación sigue sin funcionar porque el error está en la conversación entre cliente y servidor. Justamente ahí es donde Flutter DevTools resulta especialmente útil: no solo sirve para layout, rendimiento o memoria, también permite inspeccionar el tráfico de red y entender qué salió de la app y qué devolvió realmente el backend.

En este artículo veremos qué es una API, qué tipos existen, cómo se consumen desde Flutter a nivel conceptual y, sobre todo, cómo detectar y corregir un fallo real de comunicación HTTP usando DevTools.

Flutter casi nunca vive aislado

Cuando empezamos a trabajar con Flutter es normal concentrarse primero en la interfaz, en la composición de widgets, en la navegación o en la gestión del estado. Es lógico: son las piezas más visibles y las que nos permiten sentir que la aplicación “ya funciona”. Sin embargo, en cuanto salimos de los ejemplos puramente locales, descubrimos que Flutter rara vez vive aislado.

La mayoría de aplicaciones reales dependen de uno o varios servicios externos o internos para cumplir su función. Un inicio de sesión necesita validar credenciales en un servidor. Un catálogo de productos necesita cargar datos que cambian con el tiempo. Un panel administrativo necesita consultar métricas actualizadas. Una app de logística necesita refrescar estados de pedidos. Incluso una aplicación aparentemente sencilla puede depender de contenido dinámico, sincronización entre dispositivos o configuración remota.

Eso cambia por completo la naturaleza del desarrollo. En ese momento ya no basta con construir una pantalla bonita o con hacer que un botón dispare una acción. La aplicación pasa a depender de una conversación constante con sistemas que viven fuera del dispositivo. Y cuando esa conversación falla, el usuario no suele percibirlo como “un problema de red” o “un error de contrato con la API”; simplemente siente que la app no funciona.

Qué es una API y por qué importa

Una API (Application Programming Interface) es, en esencia, una interfaz que permite que dos sistemas se comuniquen entre sí. En una app Flutter eso suele traducirse en peticiones hacia un servidor para leer, crear o actualizar datos.

Desde el punto de vista de Flutter, esa comunicación toma casi siempre la misma forma: una petición HTTP hacia un endpoint, una respuesta con un código de estado y un cuerpo —normalmente JSON—, y un proceso de transformación de ese JSON en modelos Dart que la interfaz puede consumir.

Cuando una app Flutter “no funciona”, muchas veces el problema no está en el widget que muestra la información, sino en la URL base, en el endpoint concreto, en los parámetros enviados, en las cabeceras HTTP, en el cuerpo de la respuesta, en el código de estado o incluso en el mapeo del JSON hacia nuestros modelos Dart.

Por eso consumir una API es importante, pero saber depurar esa comunicación lo es todavía más.

Tipos de APIs más habituales

Existen varias formas de diseñar APIs. No necesitamos profundizar en todas, pero sí conviene entender el contexto:

Tipo Formato habitual Ventajas Inconvenientes Uso típico
REST JSON sobre HTTP Simple, ampliamente adoptado Puede devolver más o menos datos de los necesarios Apps web y móviles generales
GraphQL Consultas sobre HTTP El cliente pide exactamente lo que necesita Mayor complejidad inicial Apps con necesidades de datos complejas
SOAP XML Muy estandarizado en entornos enterprise Verboso y pesado Banca, administración, sistemas legacy
RPC / gRPC Procedimientos, a menudo con Protobuf Muy rápido y eficiente Menos amigable para inspección manual simple Microservicios y backend interno

También podríamos mencionar WebSockets para comunicación en tiempo real, aunque no será el foco de este artículo. Para el ejemplo práctico vamos a usar REST porque es fácil de entender, sigue siendo muy común en aplicaciones móviles y, además, se presta muy bien a la depuración visual desde DevTools.

Recomendaciones antes de empezar

Antes de entrar en el ejemplo conviene fijar algunas buenas prácticas. No deberíamos asumir que el backend responde exactamente como esperamos, así que merece la pena revisar siempre el endpoint, el método HTTP, los parámetros, las cabeceras, el status code, el cuerpo y los tiempos de respuesta. También es recomendable separar la lógica de red de la UI, manejar de forma explícita los estados de loading, error y success, capturar excepciones de red y evitar exponer datos sensibles durante la depuración. Y, sobre todo, conviene no inundar la app de print() caóticos cuando DevTools puede ofrecernos una vista mucho más precisa.

Por qué estos errores son tan comunes en Flutter

Una de las razones por las que la depuración de red es tan importante en Flutter es que este tipo de errores suele ser engañoso. La aplicación compila, la interfaz se renderiza correctamente, el TextField recoge el valor esperado, el botón responde y el estado parece cambiar bien. Desde fuera, todo da la impresión de estar en orden. Sin embargo, los datos no llegan, llegan con una forma distinta a la esperada o provocan un fallo silencioso que termina rompiendo la experiencia.

En muchos casos el problema no está en el layout ni en la lógica visual, sino en la capa HTTP. A veces se trata de una ruta equivocada, otras de un parámetro mal formado, de una cabecera ausente, de un 401 o un 404 que no estamos gestionando correctamente, o de una respuesta JSON cuyo contrato no coincide exactamente con el modelo que hemos creado en Dart. Son errores pequeños en apariencia, pero con un impacto enorme, porque cortan el flujo completo entre la app y la fuente real de datos.

Esto explica por qué limitarse a leer excepciones o a encadenar print() rara vez es suficiente. Esos recursos pueden ayudar en un primer momento, pero no siempre muestran con claridad qué salió del cliente, qué recibió el servidor o en qué punto exacto se rompió la comunicación. Y si no vemos el tráfico real, el diagnóstico suele convertirse en una cadena de suposiciones.

DevTools como herramienta de observabilidad

Aquí es donde Flutter DevTools gana protagonismo. Muchas veces se piensa en DevTools como un conjunto de utilidades para medir rendimiento, analizar memoria o revisar aspectos visuales de la interfaz, y desde luego sirve para todo eso. Pero en aplicaciones que consumen APIs también cumple otro papel igual de valioso: el de herramienta de observabilidad.

Poder inspeccionar una petición real cambia por completo la forma de depurar. Ya no dependemos solo de lo que creemos haber enviado ni de cómo interpretamos una excepción genérica. Podemos ver con precisión qué URL salió del cliente, qué método se utilizó, qué cabeceras acompañaban la solicitud, cuánto tardó la respuesta, qué status code devolvió el servidor y cuál fue el cuerpo exacto que regresó a la app. Esa visibilidad acorta muchísimo la distancia entre el síntoma y la causa real del problema.

En otras palabras, DevTools no solo ayuda a confirmar que algo falla; ayuda a entender por qué falla. Y esa diferencia es la que convierte una depuración lenta y basada en intuiciones en un proceso técnico mucho más fiable.

Con este contexto claro, ya podemos pasar a un caso pequeño pero realista en el que una app Flutter aparentemente correcta falla por un problema de comunicación HTTP, y donde Flutter DevTools nos permitirá encontrar la causa exacta y corregirla.

Ejemplo práctico: seguimiento de pedidos con una API REST local

Para mantener el ejemplo autónomo y reproducible, vamos a montar una aplicación Flutter muy sencilla que consulta el estado de un pedido contra una API REST local. La idea es que el usuario introduzca un código como ZX-1001 y la app muestre el código del pedido, su estado, la fecha estimada de entrega, el centro logístico, la última actualización y el destino final.

En esta versión del ejemplo utilizaremos una API Express JS dockerizada, datos dummy algo más generosos y un único endpoint principal:

GET /api/v1/orders/:orderCode

La respuesta esperada será algo como:


	{
	  "order_code": "ZX-1001",
	  "status": "in_transit",
	  "estimated_delivery": "2026-04-29",
	  "warehouse": "Madrid East Hub",
	  "destination_city": "Valencia",
	  "last_update": "Package arrived at sorting center"
	}
    

Estructura completa del proyecto

En lugar de dejar piezas implícitas, aquí tienes todos los ficheros mínimos necesarios para poder levantar el ejemplo:


	order-status-demo/
	├── backend/
	│   ├── data/
	│   │   └── orders.json
	│   ├── .dockerignore
	│   ├── Dockerfile
	│   ├── package.json
	│   └── server.js
	├── flutter_app/
	│   ├── lib/
	│   │   ├── models/
	│   │   │   └── order_status.dart
	│   │   ├── services/
	│   │   │   └── order_service.dart
	│   │   └── main.dart
	│   └── pubspec.yaml
	└── docker-compose.yml
    

Backend Express dockerizado

La API no pretende ser sofisticada. Lo que buscamos es un servidor local estable, legible y reproducible para poder inspeccionar bien las peticiones desde Flutter DevTools.

docker-compose.yml


	services:
	  orders-api:
	    build:
	      context: ./backend
	    container_name: orders-api
	    ports:
	      - "3000:3000"
    

backend/.dockerignore


	node_modules
	npm-debug.log
    

backend/package.json


	{
	  "name": "orders-api",
	  "version": "1.0.0",
	  "description": "Dummy Express API for Flutter DevTools debugging article",
	  "main": "server.js",
	  "scripts": {
	    "start": "node server.js"
	  },
	  "dependencies": {
	    "cors": "^2.8.5",
	    "express": "^4.19.2"
	  }
	}
    

backend/Dockerfile


	FROM node:22-alpine
	
	WORKDIR /app
	
	COPY package.json ./
	RUN npm install
	
	COPY . .
	
	EXPOSE 3000
	
	CMD ["npm", "start"]
    

backend/server.js


	const express = require('express');
	const cors = require('cors');
	const fs = require('fs');
	const path = require('path');
	
	const app = express();
	const PORT = 3000;
	
	app.use(cors());
	app.use(express.json());
	
	function loadOrders() {
	  const ordersPath = path.join(__dirname, 'data', 'orders.json');
	  const raw = fs.readFileSync(ordersPath, 'utf8');
	  return JSON.parse(raw);
	}
	
	app.get('/health', (_request, response) => {
	  response.json({ status: 'ok' });
	});
	
	app.get('/api/v1/orders', (_request, response) => {
	  response.json(loadOrders());
	});
	
	app.get('/api/v1/orders/:orderCode', (request, response) => {
	  const { orderCode } = request.params;
	  const orders = loadOrders();
	
	  const order = orders.find(
	    (item) => item.order_code.toLowerCase() === orderCode.toLowerCase(),
	  );
	
	  if (!order) {
	    return response.status(404).json({
	      message: 'Order not found',
	      order_code: orderCode,
	    });
	  }
	
	  return response.json(order);
	});
	
	app.listen(PORT, () => {
	  console.log(`Orders API running on http://localhost:${PORT}`);
	});
    

backend/data/orders.json

Para que el ejemplo se parezca más a un entorno real trabajaremos con un conjunto de datos como el que se muestra a continuación, por lo general, en entornos finales estos datos pueden venir de múltiples tipos de fuentes, aquí, por simplicidad vamos en un único fichero JSON:


	[
	  {
	    "order_code": "ZX-1001",
	    "status": "in_transit",
	    "estimated_delivery": "2026-04-29",
	    "warehouse": "Madrid East Hub",
	    "destination_city": "Valencia",
	    "last_update": "Package arrived at sorting center"
	  },
	  {
	    "order_code": "ZX-1002",
	    "status": "processing",
	    "estimated_delivery": "2026-05-01",
	    "warehouse": "Barcelona Logistics Center",
	    "destination_city": "Bilbao",
	    "last_update": "Order packed and ready for dispatch"
	  },
	  {
	    "order_code": "ZX-1003",
	    "status": "delivered",
	    "estimated_delivery": "2026-04-25",
	    "warehouse": "Seville South Hub",
	    "destination_city": "Seville",
	    "last_update": "Delivered to recipient"
	  },
	  {
	    "order_code": "ZX-1004",
	    "status": "pending_payment",
	    "estimated_delivery": "2026-05-03",
	    "warehouse": "Madrid East Hub",
	    "destination_city": "Zaragoza",
	    "last_update": "Awaiting payment confirmation"
	  },
	  {
	    "order_code": "ZX-1005",
	    "status": "ready_for_dispatch",
	    "estimated_delivery": "2026-04-30",
	    "warehouse": "Porto Cross-Dock",
	    "destination_city": "A Coruna",
	    "last_update": "Shipment label created"
	  },
	  {
	    "order_code": "ZX-1006",
	    "status": "in_transit",
	    "estimated_delivery": "2026-05-02",
	    "warehouse": "Lisbon Atlantic Hub",
	    "destination_city": "Vigo",
	    "last_update": "Left origin facility"
	  },
	  {
	    "order_code": "ZX-1007",
	    "status": "delayed",
	    "estimated_delivery": "2026-05-04",
	    "warehouse": "Barcelona Logistics Center",
	    "destination_city": "Malaga",
	    "last_update": "Delivery delayed due to weather conditions"
	  },
	  {
	    "order_code": "ZX-1008",
	    "status": "processing",
	    "estimated_delivery": "2026-05-05",
	    "warehouse": "Madrid East Hub",
	    "destination_city": "Granada",
	    "last_update": "Items being consolidated"
	  },
	  {
	    "order_code": "ZX-1009",
	    "status": "out_for_delivery",
	    "estimated_delivery": "2026-04-27",
	    "warehouse": "Murcia Local Depot",
	    "destination_city": "Murcia",
	    "last_update": "Courier assigned to final route"
	  },
	  {
	    "order_code": "ZX-1010",
	    "status": "returned",
	    "estimated_delivery": "2026-05-06",
	    "warehouse": "Valencia Returns Center",
	    "destination_city": "Alicante",
	    "last_update": "Recipient unavailable, package returning to sender"
	  },
	  {
	    "order_code": "ZX-1011",
	    "status": "customs_review",
	    "estimated_delivery": "2026-05-07",
	    "warehouse": "Madrid Airport Cargo",
	    "destination_city": "Pamplona",
	    "last_update": "Shipment under customs inspection"
	  },
	  {
	    "order_code": "ZX-1012",
	    "status": "in_transit",
	    "estimated_delivery": "2026-05-01",
	    "warehouse": "Bilbao North Hub",
	    "destination_city": "Santander",
	    "last_update": "Transferred to regional carrier"
	  }
	]
    

Con esto ya tenemos una API REST local reproducible y lo bastante rica como para probar búsquedas correctas, errores 404, cambios de estado y respuestas algo más realistas.

Flutter: aplicación cliente mínima pero completa

La parte Flutter seguirá siendo pequeña a propósito. Nos basta con un TextField para introducir el código de pedido, un botón para lanzar la consulta, un indicador de carga, una tarjeta con el resultado y un mensaje de error cuando la llamada falle.

flutter_app/pubspec.yaml


	name: order_status_demo
	description: Demo app for debugging HTTP requests with Flutter DevTools
	publish_to: "none"
	version: 1.0.0+1
	
	environment:
	  sdk: ^3.8.0
	
	dependencies:
	  flutter:
	    sdk: flutter
	  http: ^1.2.1
	
	dev_dependencies:
	  flutter_test:
	    sdk: flutter
	  flutter_lints: ^5.0.0
	
	flutter:
	  uses-material-design: true
    

flutter_app/lib/models/order_status.dart


	class OrderStatus {
	  final String orderCode;
	  final String status;
	  final String estimatedDelivery;
	  final String warehouse;
	  final String destinationCity;
	  final String lastUpdate;
	
	  const OrderStatus({
	    required this.orderCode,
	    required this.status,
	    required this.estimatedDelivery,
	    required this.warehouse,
	    required this.destinationCity,
	    required this.lastUpdate,
	  });
	
	  factory OrderStatus.fromJson(Map<String, dynamic> json) {
	    return OrderStatus(
	      orderCode: json['order_code'] as String,
	      status: json['status'] as String,
	      estimatedDelivery: json['estimated_delivery'] as String,
	      warehouse: json['warehouse'] as String,
	      destinationCity: json['destination_city'] as String,
	      lastUpdate: json['last_update'] as String,
	    );
	  }
	}
    

Este mapeo es importante porque deja claro que el backend devuelve claves en snake_case, mientras que en Dart solemos trabajar con propiedades en camelCase.

flutter_app/lib/services/order_service.dart

Aquí introduciremos el fallo intencionado del artículo. La app va a llamar a una ruta incorrecta:


	import 'dart:convert';
	
	import 'package:http/http.dart' as http;
	
	import '../models/order_status.dart';
	
	class OrderService {
	  final String baseUrl;
	
	  const OrderService(this.baseUrl);
	
	  Future<OrderStatus> fetchOrder(String orderCode) async {
	    final uri = Uri.parse('$baseUrl/api/v1/order/$orderCode');
	    final response = await http.get(uri);
	
	    if (response.statusCode == 200) {
	      final data = jsonDecode(response.body) as Map<String, dynamic>;
	      return OrderStatus.fromJson(data);
	    }
	
	    if (response.statusCode == 404) {
	      throw Exception('Pedido no encontrado');
	    }
	
	    throw Exception('Error inesperado: ${response.statusCode}');
	  }
	}
    

El detalle es pequeño, pero decisivo: la app usa /api/v1/order/ en singular, mientras que el backend expone /api/v1/orders/ en plural. Este tipo de error es muy común en proyectos reales y precisamente por eso resulta tan útil como ejercicio de depuración.

flutter_app/lib/main.dart


	import 'package:flutter/material.dart';
	
	import 'models/order_status.dart';
	import 'services/order_service.dart';
	
	void main() {
	  runApp(const OrderStatusApp());
	}
	
	class OrderStatusApp extends StatelessWidget {
	  const OrderStatusApp({super.key});
	
	  @override
	  Widget build(BuildContext context) {
	    return MaterialApp(
	      title: 'Order tracker',
	      debugShowCheckedModeBanner: false,
	      theme: ThemeData(
	        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
	        useMaterial3: true,
	      ),
	      home: const OrderLookupPage(),
	    );
	  }
	}
	
	class OrderLookupPage extends StatefulWidget {
	  const OrderLookupPage({super.key});
	
	  @override
	  State<OrderLookupPage> createState() => _OrderLookupPageState();
	}
	
	class _OrderLookupPageState extends State<OrderLookupPage> {
	  final controller = TextEditingController(text: 'ZX-1001');
	  final service = const OrderService('http://localhost:3000');
	
	  bool loading = false;
	  String? errorMessage;
	  OrderStatus? order;
	
	  Future<void> searchOrder() async {
	    setState(() {
	      loading = true;
	      errorMessage = null;
	      order = null;
	    });
	
	    try {
	      final result = await service.fetchOrder(controller.text.trim());
	      setState(() {
	        order = result;
	      });
	    } catch (error) {
	      setState(() {
	        errorMessage = error.toString();
	      });
	    } finally {
	      setState(() {
	        loading = false;
	      });
	    }
	  }
	
	  @override
	  void dispose() {
	    controller.dispose();
	    super.dispose();
	  }
	
	  @override
	  Widget build(BuildContext context) {
	    return Scaffold(
	      appBar: AppBar(title: const Text('Order tracker')),
	      body: Padding(
	        padding: const EdgeInsets.all(16),
	        child: Column(
	          crossAxisAlignment: CrossAxisAlignment.stretch,
	          children: [
	            TextField(
	              controller: controller,
	              decoration: const InputDecoration(
	                border: OutlineInputBorder(),
	                labelText: 'Código de pedido',
	                hintText: 'ZX-1001',
	              ),
	            ),
	            const SizedBox(height: 12),
	            ElevatedButton(
	              onPressed: loading ? null : searchOrder,
	              child: const Text('Consultar pedido'),
	            ),
	            const SizedBox(height: 24),
	            if (loading) const Center(child: CircularProgressIndicator()),
	            if (errorMessage != null)
	              Text(
	                errorMessage!,
	                style: TextStyle(
	                  color: Theme.of(context).colorScheme.error,
	                  fontWeight: FontWeight.w600,
	                ),
	              ),
	            if (order != null)
	              Card(
	                child: Padding(
	                  padding: const EdgeInsets.all(16),
	                  child: Column(
	                    crossAxisAlignment: CrossAxisAlignment.start,
	                    children: [
	                      Text(
	                        order!.orderCode,
	                        style: Theme.of(context).textTheme.titleLarge,
	                      ),
	                      const SizedBox(height: 8),
	                      Text('Estado: ${order!.status}'),
	                      Text('Entrega estimada: ${order!.estimatedDelivery}'),
	                      Text('Centro logístico: ${order!.warehouse}'),
	                      Text('Destino: ${order!.destinationCity}'),
	                      Text('Última actualización: ${order!.lastUpdate}'),
	                    ],
	                  ),
	                ),
	              ),
	          ],
	        ),
	      ),
	    );
	  }
	}
    

La app es deliberadamente simple porque el foco del artículo no es la arquitectura, sino la inspección del tráfico HTTP y la localización del error.

Cómo levantar el ejemplo

Primero arrancamos la API:


	docker compose up --build
    

Después, desde flutter_app/, instalamos dependencias:


	flutter pub get
    

Resumen por entorno

Como este ejemplo está montado a mano desde un editor, en desktop no basta con tener lib/ y pubspec.yaml: Flutter también necesita generar el runner nativo de cada plataforma con flutter create . o flutter create --platforms=... ..

Entorno Preparación previa Ejecución Base URL recomendada
Linux Desktop flutter config --enable-linux-desktop y flutter create --platforms=linux . flutter run -d linux http://localhost:3000
macOS Desktop flutter config --enable-macos-desktop y flutter create --platforms=macos . flutter run -d macos http://localhost:3000
Windows Desktop flutter config --enable-windows-desktop y flutter create --platforms=windows . flutter run -d windows http://localhost:3000
Android Emulator Ninguna extra si el proyecto ya existe flutter run http://10.0.2.2:3000
iOS Simulator Ninguna extra si el proyecto ya existe flutter run http://localhost:3000

Si quieres comprobar antes qué dispositivos reconoce Flutter, puedes ejecutar:


	flutter devices
    

Cómo abrir Flutter DevTools en el navegador

Una vez que la app esté ejecutándose en modo debug con flutter run, la propia terminal suele mostrar tanto la Dart VM Service URL como la URL de Flutter DevTools ya preparada para abrir en el navegador:


	A Dart VM Service on Linux is available at: http://127.0.0.1:39829/9tOEZegyRMs=/
	The Flutter DevTools debugger and profiler on Linux is available at:
	http://127.0.0.1:9100?uri=http://127.0.0.1:39829/9tOEZegyRMs=/
    
Dato Para qué sirve Qué hacer
Dart VM Service URL Es el endpoint interno de depuración de la app Solo la necesitas si vas a conectar DevTools manualmente
URL de Flutter DevTools Abre DevTools ya enlazado con la app en ejecución Es la URL que debes copiar al navegador

Si tu terminal no muestra el enlace, o quieres abrir DevTools por tu cuenta, puedes arrancarlo explícitamente en otra terminal:


	dart devtools
    

Ese comando levantará una instancia local de DevTools y te devolverá otra URL para abrir en el navegador. Cuando la abras, podrás pegar la Dart VM Service URL que muestra flutter run en consola para conectar DevTools con la app que estás depurando.

El fallo: la app compila, pero la petición no funciona

Imaginemos el flujo habitual: el usuario introduce ZX-1001, pulsa el botón para consultar y la app responde con un error. A simple vista podríamos pensar que el backend no está levantado, que el pedido no existe, que el parseo del JSON falla o incluso que el widget no se está reconstruyendo bien. Pero ninguna de esas hipótesis nos da certeza. Necesitamos observar la petición real, y ahí es donde entra DevTools.

Depuración con Flutter DevTools

Esta es la parte más importante del artículo. Una vez ejecutando la app en modo desarrollo y con DevTools abierto en el navegador, podemos entrar en la vista de red para inspeccionar las peticiones HTTP emitidas por la aplicación. En esa vista lo que nos interesa revisar es el método HTTP usado, la URL final, el código de estado, las cabeceras, el tiempo de respuesta y el cuerpo devuelto por el servidor.

Flujo de diagnóstico

El proceso recomendado sería este:

  1. Introducimos ZX-1001 en la app.
  2. La petición falla.
  3. Abrimos DevTools en el navegador y vamos al panel de red.
  4. Localizamos la solicitud HTTP recién lanzada.
  5. Revisamos la URL exacta y el status code.

En ese momento veremos algo parecido a esto:


	GET http://localhost:3000/api/v1/order/ZX-1001
	Status: 404 Not Found
    

Ese dato cambia completamente el diagnóstico. El problema no está en el TextField, ni en setState, ni en el modelo Dart. El problema es que la ruta enviada por el cliente no coincide con la publicada por el servidor.

Esto es precisamente lo que hace tan útil a DevTools: no obliga a adivinar. Nos permite ver exactamente qué salió del cliente y qué contestó el backend.

Corrigiendo el error

La solución consiste en cambiar una sola línea:


	final uri = Uri.parse('$baseUrl/api/v1/orders/$orderCode');
    

El servicio completo corregido quedaría así:


	Future<OrderStatus> fetchOrder(String orderCode) async {
	  final uri = Uri.parse('$baseUrl/api/v1/orders/$orderCode');
	  final response = await http.get(uri);
	
	  if (response.statusCode == 200) {
	    final data = jsonDecode(response.body) as Map<String, dynamic>;
	    return OrderStatus.fromJson(data);
	  }
	
	  if (response.statusCode == 404) {
	    throw Exception('Pedido no encontrado');
	  }
	
	  throw Exception('Error inesperado: ${response.statusCode}');
	}
    

Ahora repetimos la prueba. Volvemos a consultar ZX-1001, revisamos otra vez la petición en DevTools, confirmamos que la URL es correcta, verificamos que el servidor devuelve 200 OK y comprobamos que la UI ya muestra los datos. Lo que veremos ahora será algo similar a:


	GET http://localhost:3000/api/v1/orders/ZX-1001
	Status: 200 OK
    

Si además inspeccionamos el cuerpo JSON desde DevTools, podremos validar que las claves del backend coinciden con el mapeo usado en OrderStatus.fromJson.

Un segundo error posible: el mapeo del JSON

Si quisieras enriquecer el ejemplo, podrías introducir un segundo fallo pequeño en el modelo cliente. Por ejemplo, esperar estimatedDelivery y lastUpdate directamente desde el JSON:


	estimatedDelivery: json['estimatedDelivery'] as String,
	lastUpdate: json['lastUpdate'] as String,
    

Eso no funcionaría porque la API devuelve:


	{
	  "estimated_delivery": "2026-04-29",
	  "last_update": "Package arrived at sorting center"
	}
    

Este tipo de error también se detecta bien cuando inspeccionamos la respuesta real y la comparamos con el modelo Dart. Es una buena demostración de que no todos los fallos de red son “problemas de conexión”; muchas veces son desajustes de contrato entre cliente y servidor.

Qué deberías llevarte de este ejemplo

Este ejercicio, aunque pequeño, resume bastante bien un flujo real de trabajo. Desde Flutter consumimos APIs constantemente en aplicaciones de producción, y la mayoría de esas operaciones son asíncronas porque dependen de I/O y red. También deja claro que un error visual en la app no siempre nace en la UI y que DevTools permite confirmar con rapidez si el problema está en la pantalla o en la comunicación HTTP. Ver la URL final, el status code y el cuerpo de respuesta acelera muchísimo el diagnóstico.

Conclusión

En Flutter no basta con saber construir interfaces bonitas o estructurar bien los widgets. Tarde o temprano cualquier aplicación real necesita hablar con servicios remotos, y por eso aprender a depurar tráfico HTTP es una habilidad esencial.

Flutter DevTools no sirve solo para rendimiento, rendering o consumo de memoria. También es una herramienta muy útil para entender cómo se está comunicando nuestra app con una API y para localizar antes la causa real de un fallo.

Como siguiente paso, puedes ampliar este ejemplo añadiendo más pedidos, forzando otros errores, incorporando autenticación con tokens, probando timeouts, implementando reintentos o incluso añadiendo un historial de consultas. Todo eso te acercará todavía más al tipo de problemas que aparecen en aplicaciones Flutter reales.

Happy Building!!

¿Necesitas ayuda?

En BetaZetaDev transformamos ideas en soluciones digitales reales. Más de 10 años desarrollando aplicaciones móviles, web, automatizaciones y sistemas personalizados que impactan a miles de usuarios. Desde el concepto hasta el despliegue, creamos tecnología que resuelve problemas específicos de tu negocio con código limpio, arquitecturas escalables y experiencia probada.

Hablemos de tu proyecto

Artículos relacionados

Quizá te puedan interesar

September 15, 2024

Asincronía en Flutter

Introducción a la Asincronía en Programación En el mundo de la programación, uno de los mayores …

leer más