feat(bd): implementar ServicioFavoritos con sqflite
This commit is contained in:
251
lib/servicios/servicio_favoritos.dart
Normal file
251
lib/servicios/servicio_favoritos.dart
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import 'package:path/path.dart';
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
|
||||||
|
import '../modelos/emisora.dart';
|
||||||
|
|
||||||
|
/// Servicio de persistencia de emisoras favoritas con SQLite (sqflite).
|
||||||
|
///
|
||||||
|
/// ### Inicialización lazy
|
||||||
|
/// La base de datos se abre en el primer acceso; no es necesario llamar
|
||||||
|
/// a ningún método `init()` explícito.
|
||||||
|
///
|
||||||
|
/// ### Migration-ready
|
||||||
|
/// El esquema está versionado (`_dbVersion`). Para añadir columnas en una
|
||||||
|
/// versión futura, implementa el caso correspondiente en `_onUpgrade`.
|
||||||
|
/// La versión actual es **1**.
|
||||||
|
///
|
||||||
|
/// ### Eliminación lógica
|
||||||
|
/// No se borran filas físicas sin confirmación explícita. El método
|
||||||
|
/// [eliminar] hace `DELETE` por `uuid` (la tabla de favoritos es propiedad
|
||||||
|
/// del usuario y el borrado es una acción explícita y reversible vía
|
||||||
|
/// [agregar]). Si en el futuro se requiere eliminación lógica, añade la
|
||||||
|
/// columna `eliminado` en la migración a versión 2.
|
||||||
|
///
|
||||||
|
/// ### Uso básico
|
||||||
|
/// ```dart
|
||||||
|
/// final servicio = ServicioFavoritos();
|
||||||
|
///
|
||||||
|
/// await servicio.agregar(emisora);
|
||||||
|
/// final lista = await servicio.obtenerTodos();
|
||||||
|
/// final esFav = await servicio.esFavorito('some-uuid');
|
||||||
|
/// await servicio.reordenar('some-uuid', 3);
|
||||||
|
/// await servicio.eliminar('some-uuid');
|
||||||
|
/// ```
|
||||||
|
class ServicioFavoritos {
|
||||||
|
// ─── Constantes de esquema ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
static const String _dbNombre = 'pluriwave.db';
|
||||||
|
static const int _dbVersion = 1;
|
||||||
|
|
||||||
|
static const String _tabla = 'favoritos';
|
||||||
|
|
||||||
|
// Columnas
|
||||||
|
static const String _colId = 'id';
|
||||||
|
static const String _colUuid = 'uuid';
|
||||||
|
static const String _colNombre = 'nombre';
|
||||||
|
static const String _colUrl = 'url';
|
||||||
|
static const String _colFavicon = 'favicon';
|
||||||
|
static const String _colPais = 'pais';
|
||||||
|
static const String _colIdioma = 'idioma';
|
||||||
|
static const String _colTags = 'tags';
|
||||||
|
static const String _colOrden = 'orden';
|
||||||
|
|
||||||
|
// ─── Estado interno ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Instancia única del servicio (singleton ligero).
|
||||||
|
static final ServicioFavoritos _instancia = ServicioFavoritos._interno();
|
||||||
|
|
||||||
|
factory ServicioFavoritos() => _instancia;
|
||||||
|
|
||||||
|
ServicioFavoritos._interno();
|
||||||
|
|
||||||
|
Database? _db;
|
||||||
|
|
||||||
|
// ─── Acceso a la BD (lazy) ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Devuelve la instancia abierta de la BD.
|
||||||
|
/// La abre y crea las tablas en el primer acceso.
|
||||||
|
Future<Database> get _database async {
|
||||||
|
_db ??= await _abrirBd();
|
||||||
|
return _db!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Database> _abrirBd() async {
|
||||||
|
final ruta = join(await getDatabasesPath(), _dbNombre);
|
||||||
|
return openDatabase(
|
||||||
|
ruta,
|
||||||
|
version: _dbVersion,
|
||||||
|
onCreate: _onCreate,
|
||||||
|
onUpgrade: _onUpgrade,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Callbacks de ciclo de vida de la BD ─────────────────────────────────
|
||||||
|
|
||||||
|
/// Crea el esquema inicial (versión 1).
|
||||||
|
Future<void> _onCreate(Database db, int version) async {
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS $_tabla (
|
||||||
|
$_colId INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
$_colUuid TEXT NOT NULL UNIQUE,
|
||||||
|
$_colNombre TEXT NOT NULL,
|
||||||
|
$_colUrl TEXT NOT NULL,
|
||||||
|
$_colFavicon TEXT,
|
||||||
|
$_colPais TEXT,
|
||||||
|
$_colIdioma TEXT,
|
||||||
|
$_colTags TEXT,
|
||||||
|
$_colOrden INTEGER NOT NULL DEFAULT 0
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
|
||||||
|
// Índice para búsquedas frecuentes por uuid
|
||||||
|
await db.execute(
|
||||||
|
'CREATE UNIQUE INDEX IF NOT EXISTS idx_favoritos_uuid ON $_tabla ($_colUuid)',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Índice para ordenación
|
||||||
|
await db.execute(
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_favoritos_orden ON $_tabla ($_colOrden)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Migraciones futuras.
|
||||||
|
///
|
||||||
|
/// Añade un `case` por cada nueva versión. No uses `DROP COLUMN` —
|
||||||
|
/// marca la columna como obsoleta y elimínala en el siguiente sprint.
|
||||||
|
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
|
||||||
|
for (var v = oldVersion + 1; v <= newVersion; v++) {
|
||||||
|
switch (v) {
|
||||||
|
// Ejemplo para versión 2:
|
||||||
|
// case 2:
|
||||||
|
// await db.execute(
|
||||||
|
// 'ALTER TABLE $_tabla ADD COLUMN nueva_col TEXT',
|
||||||
|
// );
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── API pública ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Devuelve todas las emisoras favoritas ordenadas por [_colOrden] ASC.
|
||||||
|
///
|
||||||
|
/// Nunca devuelve null; devuelve lista vacía si no hay favoritos.
|
||||||
|
Future<List<Emisora>> obtenerTodos() async {
|
||||||
|
final db = await _database;
|
||||||
|
final filas = await db.query(
|
||||||
|
_tabla,
|
||||||
|
orderBy: '$_colOrden ASC, $_colId ASC',
|
||||||
|
);
|
||||||
|
return filas.map(Emisora.fromMap).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Agrega [emisora] a la lista de favoritos.
|
||||||
|
///
|
||||||
|
/// Si ya existe una emisora con el mismo [Emisora.uuid], actualiza todos
|
||||||
|
/// sus campos (upsert). El campo [Emisora.orden] se asigna al final de la
|
||||||
|
/// lista cuando su valor es 0 y la emisora es nueva.
|
||||||
|
///
|
||||||
|
/// Devuelve el [id] de la fila insertada o actualizada.
|
||||||
|
Future<int> agregar(Emisora emisora) async {
|
||||||
|
final db = await _database;
|
||||||
|
|
||||||
|
// Calcular orden automático si viene en 0 y la emisora es nueva
|
||||||
|
Emisora emisoraFinal = emisora;
|
||||||
|
if (emisora.orden == 0) {
|
||||||
|
final existe = await esFavorito(emisora.uuid);
|
||||||
|
if (!existe) {
|
||||||
|
final maxOrden = await _maxOrden(db);
|
||||||
|
emisoraFinal = emisora.copyWith(orden: maxOrden + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.insert(
|
||||||
|
_tabla,
|
||||||
|
emisoraFinal.toMap(),
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Elimina la emisora con [uuid] de la lista de favoritos.
|
||||||
|
///
|
||||||
|
/// Si la emisora no existe, no hace nada (idempotente).
|
||||||
|
/// Devuelve el número de filas eliminadas (0 ó 1).
|
||||||
|
Future<int> eliminar(String uuid) async {
|
||||||
|
final db = await _database;
|
||||||
|
return db.delete(
|
||||||
|
_tabla,
|
||||||
|
where: '$_colUuid = ?',
|
||||||
|
whereArgs: [uuid],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve `true` si la emisora con [uuid] está en la lista de favoritos.
|
||||||
|
Future<bool> esFavorito(String uuid) async {
|
||||||
|
final db = await _database;
|
||||||
|
final resultado = await db.query(
|
||||||
|
_tabla,
|
||||||
|
columns: [_colId],
|
||||||
|
where: '$_colUuid = ?',
|
||||||
|
whereArgs: [uuid],
|
||||||
|
limit: 1,
|
||||||
|
);
|
||||||
|
return resultado.isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Actualiza el [orden] de la emisora con [uuid] al valor [nuevoOrden].
|
||||||
|
///
|
||||||
|
/// Si la emisora no existe, no hace nada (idempotente).
|
||||||
|
/// Devuelve el número de filas actualizadas (0 ó 1).
|
||||||
|
///
|
||||||
|
/// Nota: este método actualiza solo la columna `orden` de la emisora
|
||||||
|
/// indicada. Si necesitas reordenar toda la lista de una vez (drag & drop),
|
||||||
|
/// construye una lista ordenada y llama a [reordenarLista].
|
||||||
|
Future<int> reordenar(String uuid, int nuevoOrden) async {
|
||||||
|
final db = await _database;
|
||||||
|
return db.update(
|
||||||
|
_tabla,
|
||||||
|
{_colOrden: nuevoOrden},
|
||||||
|
where: '$_colUuid = ?',
|
||||||
|
whereArgs: [uuid],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reordena la lista completa de favoritos en una sola transacción.
|
||||||
|
///
|
||||||
|
/// Recibe una lista ordenada de UUIDs y asigna el orden 0, 1, 2... en
|
||||||
|
/// el mismo orden. Ideal para operaciones de drag & drop.
|
||||||
|
Future<void> reordenarLista(List<String> uuidsOrdenados) async {
|
||||||
|
final db = await _database;
|
||||||
|
await db.transaction((txn) async {
|
||||||
|
for (var i = 0; i < uuidsOrdenados.length; i++) {
|
||||||
|
await txn.update(
|
||||||
|
_tabla,
|
||||||
|
{_colOrden: i},
|
||||||
|
where: '$_colUuid = ?',
|
||||||
|
whereArgs: [uuidsOrdenados[i]],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve el número total de emisoras favoritas.
|
||||||
|
Future<int> contarFavoritos() async {
|
||||||
|
final db = await _database;
|
||||||
|
final resultado = await db.rawQuery(
|
||||||
|
'SELECT COUNT(*) AS total FROM $_tabla',
|
||||||
|
);
|
||||||
|
return resultado.first['total'] as int? ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Helpers privados ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Devuelve el valor máximo actual de [_colOrden], o 0 si la tabla está vacía.
|
||||||
|
Future<int> _maxOrden(Database db) async {
|
||||||
|
final resultado = await db.rawQuery(
|
||||||
|
'SELECT MAX($_colOrden) AS max_orden FROM $_tabla',
|
||||||
|
);
|
||||||
|
return resultado.first['max_orden'] as int? ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user