Skip to main content

Conectar Bases de Datos con Flutter mediante una API en PHP

En la actualidad todos los hostings comerciales que podemos encontrar cuentan con un servidor de base de datos y PHP, con estos dos elementos podemos construir una base de datos que de soporte a nuestra aplicación de Flutter y una API que permita a la App de Flutter realizar consultas a la Base de Datos, ya que Flutter no permite la interacción directa con bases de datos.

Para ejemplificar este proceso vamos a crear una App simple que contenga información sobre libros y las reseñas que realizan los usuarios que los han leído. Para ello pensamos en una App que contenga dos pantallas, una primera pantalla mostrará todos los libros que tenemos almacenados en la base de datos de una forma visual, mostrando su portada, cuando el usuario presione sobre una de las portadas, la App lo lleva a una ventana donde se ven los detalles del libro y las reseñas que han escrito los usuarios.

Empezamos construyendo la base de datos que se compondrá de dos tablas, por una parte la tabla libros que contendrá toda la información de los libros, y por otra parte la tabla de comentarios que contendrá las reseñas que han introducido los usuarios. La estructura de la base de datos en SQL será la siguiente.

CREATE TABLE `libros` (
  `id` int(11) NOT NULL,
  `autor` varchar(200) NOT NULL,
  `titulo` varchar(200) NOT NULL,
  `genero` varchar(100) NOT NULL,
  `portada` mediumblob DEFAULT NULL,
  `audiolibro` tinyint(1) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

ALTER TABLE `libros`
  ADD PRIMARY KEY (`id`);

ALTER TABLE `libros`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
COMMIT;

CREATE TABLE `comentarios` (
  `id_comentario` int(11) NOT NULL,
  `id_libro` int(11) NOT NULL,
  `usuario` varchar(100) NOT NULL,
  `texto_comentario` varchar(500) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;


ALTER TABLE `comentarios`
  ADD PRIMARY KEY (`id_comentario`);


ALTER TABLE `comentarios`
  MODIFY `id_comentario` int(11) NOT NULL AUTO_INCREMENT;
COMMIT;

Lo único a destacar de ambas tablas es que el campo id de la tabla libro funciona como clave primaria, de la misma manera que el campo id_comentario de la tabla comentarios también actúa como clave primaria y el campo id_libro como clave foránea para relacionar ambas tablas.

IMPORTANTE: para evitar problemas de codificación, es importante declarar todos los campos de texto con la misma codificación, por ejemplo «utf8_bin».

En este punto hemos de pensar en las consultas a la base de datos que necesitaremos para nuestra App, por un lado queremos mostrar en la pantalla principal el listado de todas las portadas de los libros que tenemos en nuestra base de datos, para ello utilizaremos la siguiente consulta:

SELECT * FROM libros WHERE 1

Por otro lado para obtener todos los comentarios de un libro determinado, como sabemos que los libros se identifican por su id, necesitaremos la siguiente consulta:

SELECT * FROM comentarios WHERE id_libro=<identificador del libro>

Una vez definidas las consultas necesarias, podemos crear la API en PHP con la ayuda de ChatGPT, para ello realizaremos al chat bot una consulta como la que sigue:

Como respuesta a este consulta obtenemos dos archivos php, el primero es para configurar los parámetros de la base de datos y el segundo la implementación de la API.

<?php
$host = 'localhost';
$db_name = 'tu_base_de_datos';
$user = 'tu_usuario';
$password = 'tu_contraseña';

try {
    $conexion = new PDO("mysql:host=$host;dbname=$db_name;charset=utf8", $user, $password);
    $conexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    echo 'Error de conexión: ' . $e->getMessage();
}
?>
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');

include 'config.php';

function obtenerLibros($conexion) {
    $sql = 'SELECT * FROM libros WHERE 1';
    $stmt = $conexion->prepare($sql);
    $stmt->execute();
    $result = $stmt->fetchAll(PDO::FETCH_ASSOC);

    // Convierte los campos de texto a UTF-8 y codifica las imágenes en base64
    foreach ($result as &$book) {
        $book['autor'] = utf8_encode($book['autor']);
        $book['titulo'] = utf8_encode($book['titulo']);
        $book['genero'] = utf8_encode($book['genero']);
        $book['portada'] = base64_encode($book['portada']);
    }

    return $result;
}

function obtenerComentariosPorLibro($conexion, $id_libro) {
    $sql = 'SELECT * FROM comentarios WHERE id_libro = :id_libro';
    $stmt = $conexion->prepare($sql);
    $stmt->bindParam(':id_libro', $id_libro, PDO::PARAM_INT);
    $stmt->execute();
    $result = $stmt->fetchAll(PDO::FETCH_ASSOC);

    // Convierte los campos de texto a UTF-8
    foreach ($result as &$comment) {
        $comment['usuario'] = utf8_encode($comment['usuario']);
        $comment['texto_comentario'] = utf8_encode($comment['texto_comentario']);
    }

    return $result;
}

$action = isset($_GET['action']) ? $_GET['action'] : null;
$id_libro = isset($_GET['id_libro']) ? $_GET['id_libro'] : null;

switch ($action) {
    case 'obtenerLibros':
        echo json_encode(obtenerLibros($conexion));
        break;

    case 'obtenerComentariosPorLibro':
        if ($id_libro) {
            echo json_encode(obtenerComentariosPorLibro($conexion, $id_libro));
        } else {
            echo json_encode(['error' => 'Se requiere el identificador del libro.']);
        }
        break;

    default:
        echo json_encode(['error' => 'Acción no válida.']);
        break;
}
?>

En el archivo php de configuración de la base de datos, hemos de poner los parámetros de nuestro servidor de bases de datos. Una vez introducidos los datos, subimos al servidor en la misma carpeta ambos archivos, el primero con el nombre config.php y el segundo con el nombre api.php.

Finalmente implementamos la APP en Flutter que como ya teníamos pensado se compondrá de dos pantallas, una pantalla inicial donde se muestren todos los libros y una segunda pantalla que le aparecerá al usuario al presionar sobre uno de los libros y en donde se mostrarán todos los comentarios que los usuarios hayan escrito sobre dicho libro.

import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

Future<List<Map<String, dynamic>>> fetchBooks() async {
  final response = await http.get(Uri.parse('https://www.ignacio-lopez.com/api-libros/api.php?action=obtenerLibros'));

  if (response.statusCode == 200) {
    try {
      print('Respuesta de la API: ${response.body}');

      List<dynamic> jsonResponse = json.decode(response.body);
      print('Respuesta de la API: ${response.body}');
      List<Map<String, dynamic>> books = jsonResponse.cast<Map<String, dynamic>>();

      for (Map<String, dynamic> book in books) {
        book['portada'] = base64Decode(book['portada']);
        book['id'] = (book['id']);
      }

      return books;
    } catch (error) {
      print('Error al decodificar el JSON: $error');
      throw Exception('Error al cargar los libros desde la API');
    }
  } else {
    throw Exception('Error al cargar los libros desde la API');
  }
}

class BookCoversScreen extends StatefulWidget {
  @override
  _BookCoversScreenState createState() => _BookCoversScreenState();
}

class _BookCoversScreenState extends State<BookCoversScreen> {
  late Future<List<Map<String, dynamic>>> _booksFuture;

  @override
  void initState() {
    super.initState();
    _booksFuture = fetchBooks();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Portadas de libros'),
      ),
      body: FutureBuilder<List<Map<String, dynamic>>>(
        future: _booksFuture,
        builder: (BuildContext context, AsyncSnapshot<List<Map<String, dynamic>>> snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          } else if (snapshot.hasError) {
            return Center(child: Text('Ha ocurrido un error: ${snapshot.error}'));
          } else {
            return ListView.builder(
              itemCount: snapshot.data!.length,
              itemBuilder: (BuildContext context, int index) {
                final item = snapshot.data![index];

                return ListTile(
                  title: Text(item['titulo']),
                  leading: Image.memory(item['portada']),
                  onTap: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) => BookDetailsScreen(id: item['id'], image: item['portada'], title: item['titulo']),
                      ),
                    );
                  },
                );
              },
            );
          }
        },
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    home: BookCoversScreen(),
  ));
}

class BookDetailsScreen extends StatefulWidget {
  final int id;
  final Uint8List image;
  final String title;

  BookDetailsScreen({required this.id, required this.image, required this.title});

  @override
  _BookDetailsScreenState createState() => _BookDetailsScreenState();
}

class _BookDetailsScreenState extends State<BookDetailsScreen> {
  late Future<List<Map<String, dynamic>>> _commentsFuture;

  @override
  void initState() {
    super.initState();
    _commentsFuture = fetchComments(widget.id);
  }

  Future<List<Map<String, dynamic>>> fetchComments(int bookId) async {
    final response = await http.get(Uri.parse('https://www.ignacio-lopez.com/api-libros/api.php?action=obtenerComentariosPorLibro&id_libro=$bookId'));

    if (response.statusCode == 200) {
      List<dynamic> jsonResponse = json.decode(response.body);
      List<Map<String, dynamic>> comments = jsonResponse.cast<Map<String, dynamic>>();
      return comments;
    } else {
      throw Exception('Error al cargar los comentarios desde la API');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Column(
        children: [
          Container(
            height: MediaQuery.of(context).size.height / 2,
            width: double.infinity,
            color: Colors.grey[300], // Fondo para la imagen, puedes elegir otro color si lo deseas
            child: FittedBox(
              fit: BoxFit.contain,
              child: Image.memory(
                widget.image,
              ),
            ),
          ),
          Expanded(
            child: FutureBuilder<List<Map<String, dynamic>>>(
              future: _commentsFuture,
              builder: (BuildContext context, AsyncSnapshot<List<Map<String, dynamic>>> snapshot) {
                if (snapshot.connectionState == ConnectionState.waiting) {
                  return Center(child: CircularProgressIndicator());
                } else if (snapshot.hasError) {
                  return Center(child: Text('Ha ocurrido un error: ${snapshot.error}'));
                } else {
                  return ListView.builder(
                    itemCount: snapshot.data!.length,
                    itemBuilder: (BuildContext context, int index) {
                      final item = snapshot.data![index];

                      return ListTile(
                        title: Text(item['texto_comentario']),
                        subtitle: Text('Usuario: ${item['usuario']}'),
                      );
                    },
                  );
                }
              },
            ),
          ),
        ],
      ),
    );
  }
}

IMPORTANTE: para poder realizar interacciones con la API necesitamos la biblioteca http, por lo que dentro del archivo pubspec.yalm hemos de añadir esta dependencia. Como se ve a continuación.

dependencies:
  flutter:
    sdk: flutter
  http: ^0.13.3