🚀 API FacturaScripts con Flutter/Dart

Aprende a conectar tu aplicación Flutter con la API de FacturaScripts

Descargar Ejemplos Completos

📱 ¿Qué es esta estructura de API?

Conexión API Genérica para FacturaScripts

Esta estructura proporciona una forma simple y reutilizable de conectar aplicaciones Flutter/Dart con la API REST de FacturaScripts. Incluye:

  • Configuración centralizada: URL y tokens en un solo lugar
  • Servicios HTTP genéricos: GET, POST, PUT, DELETE predefinidos
  • Modelos de datos: Serialización/deserialización JSON automática
  • Ejemplo funcional: Obtener clientes desde FacturaScripts

Ventajas de esta arquitectura

  • Reutilizable: Añade nuevos endpoints fácilmente
  • Mantenible: Cambios de configuración en un solo archivo
  • Escalable: Estructura preparada para crecer
  • Tipado seguro: Modelos Dart con validación
  • Gestión de errores: Try-catch en todos los métodos

🔍 Explora los Componentes de la API

Selecciona qué parte del código quieres analizar en detalle:

Componentes de la API

📁 Estructura del Proyecto

lib/
├── config/
│   └── api_config.dart          # Configuración de la API
├── models/
│   └── cliente.dart             # Modelo Cliente
├── services/
│   ├── api_service.dart         # Servicio HTTP genérico
│   └── clientes_service.dart    # Servicio de Clientes
└── main.dart                    # Ejemplo de uso

Organización por capas: Cada carpeta tiene una responsabilidad específica, facilitando el mantenimiento y la escalabilidad del código.

⚙️ 1. Configuración - api_config.dart

📄 lib/config/api_config.dart

class ApiConfig {
  static const String baseUrl = 'http://127.0.0.1';
  static const String apiPath = '/api/3';
  static const String globalApiKey = 'Dx88vjqWnUNSkDS4HFio';
  
  static String getApiUrl(String endpoint) {
    return '$baseUrl$apiPath$endpoint';
  }
  
  static Map<String, String> getHeaders({String? userApiKey}) {
    return {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      'token': userApiKey ?? globalApiKey,
    };
  }
}

🔑 Componentes Explicados

  • baseUrl: Dirección del servidor (localhost en desarrollo, URL real en producción)
  • apiPath: Ruta base de la API de FacturaScripts (/api/3 es la versión 3)
  • globalApiKey: Token de autenticación predeterminado para todas las peticiones
  • getApiUrl(): Construye URLs completas concatenando base + path + endpoint
  • getHeaders(): Genera headers HTTP con Content-Type, Accept y token de autenticación

💡 Ventajas de Centralizar la Configuración

  • ✅ Cambiar de servidor modificando solo una línea
  • ✅ Cambiar versión de API en un solo lugar
  • ✅ Gestionar múltiples tokens (global vs usuario)
  • ✅ Headers consistentes en todas las peticiones

📦 2. Modelos - cliente.dart

📄 lib/models/cliente.dart

class Cliente {
  final String codcliente;
  final String nombre;
  final String? cifnif;
  final String? email;
  final String? telefono1;
  
  Cliente({
    required this.codcliente,
    required this.nombre,
    this.cifnif,
    this.email,
    this.telefono1,
  });
  
  factory Cliente.fromJson(Map<String, dynamic> json) {
    return Cliente(
      codcliente: json['codcliente']?.toString() ?? '',
      nombre: json['nombre']?.toString() ?? '',
      cifnif: json['cifnif']?.toString(),
      email: json['email']?.toString(),
      telefono1: json['telefono1']?.toString(),
    );
  }
  
  Map<String, dynamic> toJson() {
    return {
      'codcliente': codcliente,
      'nombre': nombre,
      'cifnif': cifnif,
      'email': email,
      'telefono1': telefono1,
    };
  }
  
  @override
  String toString() {
    return 'Cliente(codcliente: $codcliente, nombre: $nombre, cifnif: $cifnif, email: $email, telefono1: $telefono1)';
  }
}

🏗️ Anatomía del Modelo

  • Propiedades final: Inmutables una vez creadas (patrón inmutable)
  • Nullable vs Non-nullable:
    • String - Campo obligatorio
    • String? - Campo opcional (puede ser null)
  • Constructor named parameters: Claridad al instanciar objetos
  • required: Obliga a proporcionar valores para campos no opcionales

🔄 Serialización JSON

  • fromJson(): Convierte Map<String, dynamic> (JSON) → Objeto Cliente
    • Usa ?.toString() para manejar valores null
    • Operador ?? proporciona valores por defecto
  • toJson(): Convierte Objeto Cliente → Map<String, dynamic> (JSON)
    • Útil para enviar datos en POST/PUT
  • toString(): Representación legible para debugging y logs

📊 Ejemplo de Uso

// Crear desde JSON (respuesta de API)
final jsonData = {
  'codcliente': '001',
  'nombre': 'Juan Pérez',
  'cifnif': '12345678A',
  'email': 'juan@example.com',
  'telefono1': '600123456'
};

final cliente = Cliente.fromJson(jsonData);

// Convertir a JSON (para enviar a API)
final jsonSalida = cliente.toJson();

// Imprimir (usa toString automáticamente)
print(cliente); // Cliente(codcliente: 001, nombre: Juan Pérez, ...)

🔌 3. Servicios HTTP - api_service.dart

📄 lib/services/api_service.dart

import 'dart:convert';
import 'package:http/http.dart' as http;
import '../config/api_config.dart';

class ApiService {
  final String? userApiKey;
  
  ApiService({this.userApiKey});
  
  /// GET genérico a cualquier endpoint
  Future<Map<String, dynamic>> get(String endpoint, {Map<String, String>? queryParams}) async {
    try {
      String url = ApiConfig.getApiUrl(endpoint);
      
      if (queryParams != null && queryParams.isNotEmpty) {
        final query = queryParams.entries
            .map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}')
            .join('&');
        url = '$url?$query';
      }
      
      final response = await http.get(
        Uri.parse(url),
        headers: ApiConfig.getHeaders(userApiKey: userApiKey),
      );
      
      if (response.statusCode == 200) {
        return json.decode(response.body);
      } else {
        throw Exception('Error ${response.statusCode}: ${response.body}');
      }
    } catch (e) {
      throw Exception('Error en petición GET: $e');
    }
  }
  
  /// POST genérico a cualquier endpoint
  Future<Map<String, dynamic>> post(String endpoint, Map<String, dynamic> data) async {
    try {
      final url = ApiConfig.getApiUrl(endpoint);
      
      final response = await http.post(
        Uri.parse(url),
        headers: ApiConfig.getHeaders(userApiKey: userApiKey),
        body: json.encode(data),
      );
      
      if (response.statusCode == 200 || response.statusCode == 201) {
        return json.decode(response.body);
      } else {
        throw Exception('Error ${response.statusCode}: ${response.body}');
      }
    } catch (e) {
      throw Exception('Error en petición POST: $e');
    }
  }
  
  /// PUT genérico a cualquier endpoint
  Future<Map<String, dynamic>> put(String endpoint, Map<String, dynamic> data) async {
    try {
      final url = ApiConfig.getApiUrl(endpoint);
      
      final response = await http.put(
        Uri.parse(url),
        headers: ApiConfig.getHeaders(userApiKey: userApiKey),
        body: json.encode(data),
      );
      
      if (response.statusCode == 200) {
        return json.decode(response.body);
      } else {
        throw Exception('Error ${response.statusCode}: ${response.body}');
      }
    } catch (e) {
      throw Exception('Error en petición PUT: $e');
    }
  }
  
  /// DELETE genérico a cualquier endpoint
  Future<Map<String, dynamic>> delete(String endpoint) async {
    try {
      final url = ApiConfig.getApiUrl(endpoint);
      
      final response = await http.delete(
        Uri.parse(url),
        headers: ApiConfig.getHeaders(userApiKey: userApiKey),
      );
      
      if (response.statusCode == 200) {
        return json.decode(response.body);
      } else {
        throw Exception('Error ${response.statusCode}: ${response.body}');
      }
    } catch (e) {
      throw Exception('Error en petición DELETE: $e');
    }
  }
}

🎯 Método GET - Obtener Datos

  • Propósito: Recuperar información del servidor (lectura)
  • Query Parameters: Parámetros opcionales para filtrar resultados
    • Ejemplo: /clientes?nombre=Juan&activo=true
    • Se codifican con Uri.encodeComponent() para seguridad
  • Status 200: Respuesta exitosa, decodifica JSON
  • Try-catch: Captura errores de red o parsing

📝 Método POST - Crear Datos

  • Propósito: Enviar datos al servidor para crear recursos
  • Body: Datos en formato JSON (convertidos con json.encode())
  • Status 200/201: Creación exitosa (201 = Created)
  • Uso típico: Crear nuevo cliente, factura, producto, etc.

✏️ Método PUT - Actualizar Datos

  • Propósito: Modificar un recurso existente en el servidor
  • Diferencia con POST: PUT es idempotente (múltiples llamadas = mismo resultado)
  • Endpoint típico: /clientes/001 (incluye ID del recurso)

🗑️ Método DELETE - Eliminar Datos

  • Propósito: Borrar un recurso del servidor
  • Sin body: Solo necesita el endpoint con el ID
  • Precaución: Operación destructiva, implementar confirmaciones en UI

📄 lib/services/clientes_service.dart

import '../models/cliente.dart';
import 'api_service.dart';

class ClientesService {
  final ApiService _apiService;
  
  ClientesService({String? userApiKey}) 
      : _apiService = ApiService(userApiKey: userApiKey);
  
  /// Obtener todos los clientes
  Future<List<Cliente>> getClientes() async {
    try {
      final response = await _apiService.get('/clientes');
      
      if (response.containsKey('clientes')) {
        final List<dynamic> clientesJson = response['clientes'];
        return clientesJson.map((json) => Cliente.fromJson(json)).toList();
      }
      
      return [];
    } catch (e) {
      throw Exception('Error al obtener clientes: $e');
    }
  }
  
  /// Obtener un cliente por código
  Future<Cliente?> getCliente(String codcliente) async {
    try {
      final response = await _apiService.get('/clientes/$codcliente');
      
      if (response.containsKey('cliente')) {
        return Cliente.fromJson(response['cliente']);
      }
      
      return null;
    } catch (e) {
      throw Exception('Error al obtener cliente: $e');
    }
  }
}

🏭 Patrón Service Layer

  • Separación de responsabilidades:
    • ApiService → Peticiones HTTP genéricas
    • ClientesService → Lógica específica de Clientes
  • Reutilización: ApiService se usa internamente sin exponer detalles HTTP
  • Tipado fuerte: Devuelve List<Cliente> en vez de JSON crudo
  • Gestión de errores: Mensajes específicos del dominio (clientes)

🔍 Deserialización de Lista

  • Paso 1: response['clientes'] obtiene el array JSON
  • Paso 2: List<dynamic> lo tipa como lista dinámica
  • Paso 3: .map((json) => Cliente.fromJson(json)) convierte cada elemento
  • Paso 4: .toList() materializa el resultado en List<Cliente>

▶️ 4. Ejemplo de Uso - main.dart

📄 lib/main.dart

import 'services/clientes_service.dart';

void main() async {
  print('=== Prueba de conexión a API ===\n');
  
  // Crear instancia del servicio de clientes
  final clientesService = ClientesService();
  
  try {
    print('Obteniendo clientes...');
    final clientes = await clientesService.getClientes();
    
    print('\nTotal de clientes: ${clientes.length}\n');
    
    if (clientes.isNotEmpty) {
      print('Primeros 5 clientes:');
      for (var i = 0; i < clientes.length && i < 5; i++) {
        print('${i + 1}. ${clientes[i]}');
      }
    } else {
      print('No se encontraron clientes.');
    }
    
  } catch (e) {
    print('Error: $e');
  }
  
  print('\n=== Fin de la prueba ===');
}

🚀 Ejecución del Código

  • void main() async: Punto de entrada, async permite usar await
  • await clientesService.getClientes(): Espera la respuesta de la API (operación asíncrona)
  • Try-catch: Captura errores de red, parsing o API
  • Interpolación: \${variable} inserta valores en strings

💻 Cómo Ejecutar la Prueba

Desde terminal Dart:
dart run lib/main.dart
Desde Flutter:
flutter run lib/main.dart

📊 Salida Esperada

=== Prueba de conexión a API ===

Obteniendo clientes...

Total de clientes: 15

Primeros 5 clientes:
1. Cliente(codcliente: 001, nombre: Juan Pérez, cifnif: 12345678A, ...)
2. Cliente(codcliente: 002, nombre: María García, cifnif: 87654321B, ...)
3. Cliente(codcliente: 003, nombre: Carlos López, cifnif: 11223344C, ...)
4. Cliente(codcliente: 004, nombre: Ana Martínez, cifnif: 55667788D, ...)
5. Cliente(codcliente: 005, nombre: Pedro Sánchez, cifnif: 99887766E, ...)

=== Fin de la prueba ===

📋 Requisitos Previos

1. Dependencia HTTP en pubspec.yaml

dependencies:
  http: ^1.1.0

Instalación: Ejecuta flutter pub get o dart pub get

2. FacturaScripts con API Activa

  • URL: http://127.0.0.1 (o tu servidor)
  • API Path: /api/3 (versión 3 de la API)
  • Token: Generar en FacturaScripts → Administrador → API

3. Configurar CORS (si es necesario)

Si accedes desde web o tienes problemas de CORS, configura el servidor para aceptar peticiones del origen de tu app.

🔧 Cómo Extender esta Estructura

1️⃣ Añadir Nuevo Modelo

Ejemplo: Producto

// lib/models/producto.dart
class Producto {
  final String referencia;
  final String descripcion;
  final double precio;
  
  Producto({
    required this.referencia,
    required this.descripcion,
    required this.precio,
  });
  
  factory Producto.fromJson(Map<String, dynamic> json) {
    return Producto(
      referencia: json['referencia'] ?? '',
      descripcion: json['descripcion'] ?? '',
      precio: (json['precio'] ?? 0.0).toDouble(),
    );
  }
  
  Map<String, dynamic> toJson() {
    return {
      'referencia': referencia,
      'descripcion': descripcion,
      'precio': precio,
    };
  }
}

2️⃣ Crear Servicio Específico

Ejemplo: ProductosService

// lib/services/productos_service.dart
import '../models/producto.dart';
import 'api_service.dart';

class ProductosService {
  final ApiService _apiService;
  
  ProductosService({String? userApiKey}) 
      : _apiService = ApiService(userApiKey: userApiKey);
  
  Future<List<Producto>> getProductos() async {
    final response = await _apiService.get('/productos');
    
    if (response.containsKey('productos')) {
      final List<dynamic> productosJson = response['productos'];
      return productosJson.map((json) => Producto.fromJson(json)).toList();
    }
    
    return [];
  }
  
  Future<Producto?> crearProducto(Producto producto) async {
    final response = await _apiService.post('/productos', producto.toJson());
    
    if (response.containsKey('producto')) {
      return Producto.fromJson(response['producto']);
    }
    
    return null;
  }
}

3️⃣ Usar en Tu Aplicación

Integración en Flutter

// En tu Widget de Flutter
import 'package:flutter/material.dart';
import 'services/productos_service.dart';

class ProductosPage extends StatefulWidget {
  @override
  _ProductosPageState createState() => _ProductosPageState();
}

class _ProductosPageState extends State<ProductosPage> {
  final _productosService = ProductosService();
  List<Producto> _productos = [];
  bool _cargando = true;
  
  @override
  void initState() {
    super.initState();
    _cargarProductos();
  }
  
  Future<void> _cargarProductos() async {
    try {
      final productos = await _productosService.getProductos();
      setState(() {
        _productos = productos;
        _cargando = false;
      });
    } catch (e) {
      print('Error: $e');
      setState(() => _cargando = false);
    }
  }
  
  @override
  Widget build(BuildContext context) {
    if (_cargando) {
      return Center(child: CircularProgressIndicator());
    }
    
    return ListView.builder(
      itemCount: _productos.length,
      itemBuilder: (context, index) {
        final producto = _productos[index];
        return ListTile(
          title: Text(producto.descripcion),
          subtitle: Text(producto.referencia),
          trailing: Text('${producto.precio}€'),
        );
      },
    );
  }
}

✅ Mejores Prácticas Implementadas

🏗️ Arquitectura

  • Separación de capas (config, models, services)
  • Patrón Repository implícito
  • Inyección de dependencias (userApiKey)
  • Single Responsibility Principle

🛡️ Seguridad y Robustez

  • Try-catch en todos los métodos HTTP
  • Validación de status codes
  • Null safety con operadores ? y ??
  • URI encoding para query parameters

📦 Mantenibilidad

  • Código reutilizable y escalable
  • Nombres descriptivos y claros
  • Documentación con comentarios ///
  • Fácil de testear (async/await)

🔧 Solución de Problemas Comunes

❌ Error de Conexión

Síntoma: "Connection refused" o timeout

Solución:

  • Verifica que FacturaScripts esté ejecutándose
  • Comprueba la URL en api_config.dart
  • Usa IP real en dispositivo físico (no localhost)

🔐 Error 401 Unauthorized

Síntoma: "Error 401: Unauthorized"

Solución:

  • Verifica el token en globalApiKey
  • Genera un nuevo token en FacturaScripts
  • Comprueba que la API esté habilitada

🌐 Error CORS (Web)

Síntoma: "CORS policy blocked"

Solución:

  • Configura headers CORS en el servidor
  • Usa proxy durante desarrollo
  • En producción, configura dominio permitido

📦 Error de Parsing JSON

Síntoma: "type 'Null' is not a subtype of..."

Solución:

  • Agrega validación ?.toString()
  • Usa operador ?? para valores por defecto
  • Imprime la respuesta cruda para debug

⚡ Dependencia HTTP no Encontrada

Síntoma: "Error: Cannot find package 'http'"

Solución:

  • Añade http: ^1.1.0 a pubspec.yaml
  • Ejecuta flutter pub get
  • Reinicia el IDE si es necesario

🐛 Lista Vacía sin Errores

Síntoma: clientes.length == 0 pero no hay error

Solución:

  • Verifica la clave JSON: response['clientes']
  • Imprime response para ver estructura
  • Comprueba que hay datos en FacturaScripts

🚀 Próximos Pasos

1. Añadir Más Modelos

Crea modelos para otros recursos de FacturaScripts:

  • Facturas (factura.dart)
  • Productos (producto.dart)
  • Artículos (articulo.dart)
  • Proveedores (proveedor.dart)

2. Implementar CRUD Completo

Añade operaciones CREATE, UPDATE, DELETE a tus servicios:

  • crearCliente() usando POST
  • actualizarCliente() usando PUT
  • eliminarCliente() usando DELETE

3. Mejorar Gestión de Estados

Integra con soluciones de estado de Flutter:

  • Provider para gestión simple
  • Riverpod para arquitectura más robusta
  • BLoC para aplicaciones complejas

4. Añadir Caché y Persistencia

Optimiza el rendimiento y experiencia offline:

  • SharedPreferences para datos simples
  • Hive o SQLite para datos estructurados
  • Estrategia cache-first o network-first