🚀 Betuko Offline Sync v2.0.0
Una librería Flutter offline-first completa y súper fácil de usar para aplicaciones que necesitan sincronización automática con servidores. Diseñada para funcionar perfectamente tanto online como offline.
✨ Características Principales
- 🔄 Sincronización automática cuando hay conexión
- 📱 Offline-first: La app funciona sin internet
- 🌐 Detección inteligente de respuestas del servidor (anidadas y simples)
- 🎯 API súper simple - solo crear y usar
- 🔧 Auto-inicialización - sin configuración compleja
- 📊 Streams reactivos para UI en tiempo real
- 🛡️ Manejo robusto de errores
- 🧪 Completamente testeable
🎯 Casos de Uso Perfectos
- ✅ Apps que necesitan funcionar sin internet
- ✅ Formularios que se envían cuando hay conexión
- ✅ Listas que se actualizan automáticamente
- ✅ Aplicaciones con datos críticos
- ✅ Apps con sincronización en background
📦 Instalación
Agrega a tu pubspec.yaml:
dependencies:
betuko_offline_sync: ^2.0.0
Luego ejecuta:
flutter pub get
🚀 Uso Básico - ¡3 Pasos!
1. Configuración Inicial (main.dart)
import 'package:flutter/material.dart';
import 'package:betuko_offline_sync/betuko_offline_sync.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// ¡Solo una línea de configuración!
GlobalConfig.init(
baseUrl: 'https://tu-api.com/api',
token: 'tu-token-de-autenticacion',
);
runApp(MyApp());
}
2. Crear Manager (¡Auto-inicializado!)
class DataService {
// ¡Se inicializa automáticamente!
static final manager = OnlineOfflineManager(
boxName: 'usuarios',
endpoint: 'users',
);
}
3. ¡Usar en tu UI!
class MiListaWidget extends StatefulWidget {
@override
_MiListaWidgetState createState() => _MiListaWidgetState();
}
class _MiListaWidgetState extends State<MiListaWidget> {
List<Map<String, dynamic>> usuarios = [];
bool isLoading = false;
@override
void initState() {
super.initState();
_cargarDatos();
}
// Cargar datos (con sincronización automática)
Future<void> _cargarDatos() async {
setState(() { isLoading = true; });
try {
// ¡Una línea! Sincroniza y retorna datos actualizados
final datos = await DataService.manager.getAllWithSync();
setState(() {
usuarios = datos;
isLoading = false;
});
} catch (e) {
setState(() { isLoading = false; });
print('Error: $e');
}
}
// Guardar nuevo usuario
Future<void> _guardarUsuario() async {
await DataService.manager.save({
'nombre': 'Juan Pérez',
'email': 'juan@ejemplo.com',
'edad': 30,
});
_cargarDatos(); // Recargar lista
}
@override
Widget build(BuildContext context) {
if (isLoading) {
return Center(child: CircularProgressIndicator());
}
return Scaffold(
body: RefreshIndicator(
onRefresh: () => _cargarDatos(),
child: ListView.builder(
itemCount: usuarios.length,
itemBuilder: (context, index) {
final usuario = usuarios[index];
return ListTile(
title: Text(usuario['nombre'] ?? 'Sin nombre'),
subtitle: Text(usuario['email'] ?? 'Sin email'),
trailing: usuario['sync'] == 'true'
? Icon(Icons.cloud_done, color: Colors.green)
: Icon(Icons.cloud_upload, color: Colors.orange),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: _guardarUsuario,
child: Icon(Icons.add),
),
);
}
}
🎯 Métodos Principales
📥 Obtener Datos
// 1. Datos locales (súper rápido)
final datosLocales = await manager.getAll();
// 2. Datos frescos del servidor (requiere internet)
final datosFrescos = await manager.getFromServer();
// 3. Datos con sincronización automática (recomendado)
final datosActualizados = await manager.getAllWithSync();
// 4. Un registro específico
final usuario = await manager.getById('user_123');
// 5. Datos ordenados por fecha (NUEVO)
final datosOrdenados = await manager.getAll(); // Ya vienen ordenados
// 6. Últimos 50 registros por temporada (NUEVO)
final ultimosPorTemporada = await manager.getLatestBySeason('season_id', limit: 50);
// 7. Datos agrupados por temporada (NUEVO)
final datosPorTemporada = await manager.getLatestByAllSeasons(limit: 50);
💾 Guardar Datos
// Guardar (se sincroniza automáticamente cuando hay internet)
await manager.save({
'nombre': 'Ana García',
'email': 'ana@ejemplo.com',
'departamento': 'Ventas',
});
🗑️ Eliminar Datos
// Eliminar
await manager.delete('user_123');
🔄 Sincronización Manual
// Sincronización inteligente (recomendado)
await manager.sync(); // Solo sincroniza si es necesario
// Sincronización forzada (ignora caché)
await manager.forceSync(); // Siempre sincroniza
// Sincronización inmediata (bypasa verificaciones)
await manager.syncNow(); // Sincroniza inmediatamente
📊 Filtros Útiles
// Solo datos pendientes de sincronizar
final pendientes = await manager.getPending();
// Solo datos ya sincronizados
final sincronizados = await manager.getSynced();
// Limpiar registros duplicados
await manager.cleanDuplicates();
// Limpiar todo
await manager.clear();
🌊 UI Reactiva con Streams
Datos en Tiempo Real
StreamBuilder<List<Map<String, dynamic>>>(
stream: manager.dataStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
final datos = snapshot.data!;
return ListView.builder(
itemCount: datos.length,
itemBuilder: (context, index) {
final item = datos[index];
return ListTile(
title: Text(item['titulo'] ?? 'Sin título'),
subtitle: Text('Creado: ${item['created_at']}'),
);
},
);
}
return CircularProgressIndicator();
},
)
Estado de Sincronización
StreamBuilder<SyncStatus>(
stream: manager.statusStream,
builder: (context, snapshot) {
switch (snapshot.data) {
case SyncStatus.idle:
return Icon(Icons.sync, color: Colors.grey);
case SyncStatus.syncing:
return CircularProgressIndicator();
case SyncStatus.success:
return Icon(Icons.check_circle, color: Colors.green);
case SyncStatus.error:
return Icon(Icons.error, color: Colors.red);
default:
return Container();
}
},
)
Estado de Conectividad
StreamBuilder<bool>(
stream: manager.connectivityStream,
builder: (context, snapshot) {
final isOnline = snapshot.data ?? false;
return Container(
padding: EdgeInsets.all(8),
color: isOnline ? Colors.green : Colors.red,
child: Text(
isOnline ? '🌐 Conectado' : '📱 Sin conexión',
style: TextStyle(color: Colors.white),
),
);
},
)
🔧 Configuración Avanzada
Variables de Entorno
class AppConfig {
static String get baseUrl {
return const String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'https://api-dev.miapp.com',
);
}
static String get apiToken {
return const String.fromEnvironment('API_TOKEN');
}
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
GlobalConfig.init(
baseUrl: AppConfig.baseUrl,
token: AppConfig.apiToken,
syncMinutes: 10, // Sincronizar cada 10 minutos
maxPagesPerSync: 5, // Máximo 5 páginas por sincronización
syncTimeoutMinutes: 30, // Usar descarga completa si han pasado más de 30 minutos
);
runApp(MyApp());
}
Configuración por Entorno
enum Environment { development, staging, production }
class EnvironmentConfig {
static const environment = Environment.development;
static String get baseUrl {
switch (environment) {
case Environment.development:
return 'https://dev-api.miapp.com';
case Environment.staging:
return 'https://staging-api.miapp.com';
case Environment.production:
return 'https://api.miapp.com';
}
}
}
🚀 Configuración Optimizada para Evitar Descargas Masivas
Si experimentas descargas masivas en cada reinicio, usa esta configuración optimizada:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Configuración optimizada para evitar descargas masivas
GlobalConfig.init(
baseUrl: 'https://tu-api.com',
token: 'tu-token',
syncMinutes: 15, // Sincronizar cada 15 minutos (más tiempo)
useIncrementalSync: true, // Usar sincronización incremental
pageSize: 50, // Páginas más grandes para menos requests
lastModifiedField: 'lastModifiedAt', // Campo de timestamp
syncOnReconnect: true, // Sincronizar al reconectar
maxLocalRecords: 1000, // Límite de registros locales
maxDaysToKeep: 7, // Mantener registros por 7 días
maxPagesPerSync: 5, // Máximo 5 páginas por sincronización
syncTimeoutMinutes: 30, // Usar descarga completa si han pasado más de 30 minutos
);
runApp(MyApp());
}
🎯 Configuraciones Recomendadas por Tipo de App
📱 Para aplicaciones móviles:
GlobalConfig.init(
baseUrl: '...',
token: '...',
syncMinutes: 15,
maxPagesPerSync: 3,
syncTimeoutMinutes: 30,
pageSize: 50,
);
💻 Para aplicaciones web:
GlobalConfig.init(
baseUrl: '...',
token: '...',
syncMinutes: 5,
maxPagesPerSync: 10,
syncTimeoutMinutes: 15,
pageSize: 25,
);
🏢 Para aplicaciones empresariales:
GlobalConfig.init(
baseUrl: '...',
token: '...',
syncMinutes: 30,
maxPagesPerSync: 20,
syncTimeoutMinutes: 60,
pageSize: 100,
);
🌐 Soporte para APIs Anidadas
La librería detecta automáticamente diferentes formatos de respuesta:
Respuesta Anidada (Extrae automáticamente)
{
"data": [
{"id": 1, "nombre": "Juan"},
{"id": 2, "nombre": "Ana"}
],
"total": 2,
"page": 1
}
→ Resultado: [{"id": 1, "nombre": "Juan"}, {"id": 2, "nombre": "Ana"}]
Respuesta Simple (Sin modificación)
[
{"id": 1, "nombre": "Juan"},
{"id": 2, "nombre": "Ana"}
]
→ Resultado: [{"id": 1, "nombre": "Juan"}, {"id": 2, "nombre": "Ana"}]
🌾 Manejo de Datos por Temporada (NUEVO)
Últimos 50 Registros por Temporada
class HarvestService {
static final manager = OnlineOfflineManager(
boxName: 'harvest_delivery',
endpoint: 'harvest-delivery',
enableAutoCleanup: true, // Limpieza automática habilitada
);
/// Obtener los últimos 50 registros de una temporada específica
static Future<List<Map<String, dynamic>>> getLatestHarvests(String seasonId) async {
return await manager.getLatestBySeason(seasonId, limit: 50);
}
/// Obtener datos agrupados por todas las temporadas
static Future<Map<String, List<Map<String, dynamic>>>> getAllSeasonsData() async {
return await manager.getLatestByAllSeasons(limit: 50);
}
/// Crear un nuevo registro de cosecha
static Future<void> createHarvestRecord({
required String seasonId,
required String folio,
required int quantity,
required String driverName,
required String crewName,
}) async {
await manager.save({
'seasonId': seasonId,
'folio': folio,
'quantity': quantity,
'date': DateTime.now().toIso8601String(),
'timestamp': DateTime.now().millisecondsSinceEpoch.toString(),
'driver': {'name': driverName},
'crew': {'name': crewName},
'isActive': true,
});
}
/// Obtener estadísticas de sincronización
static Future<Map<String, int>> getSyncStats() async {
final allData = await manager.getAll();
final syncedData = await manager.getSync();
final localData = await manager.getLocal();
return {
'total': allData.length,
'synced': syncedData.length,
'pending': localData.length,
};
}
}
UI Reactiva para Datos de Cosecha
class HarvestListWidget extends StatefulWidget {
final String seasonId;
const HarvestListWidget({Key? key, required this.seasonId}) : super(key: key);
@override
_HarvestListWidgetState createState() => _HarvestListWidgetState();
}
class _HarvestListWidgetState extends State<HarvestListWidget> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Cosechas - Temporada ${widget.seasonId}'),
actions: [
StreamBuilder<SyncStatus>(
stream: HarvestService.manager.statusStream,
builder: (context, snapshot) {
switch (snapshot.data) {
case SyncStatus.syncing:
return Padding(
padding: EdgeInsets.all(16),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
);
case SyncStatus.success:
return Icon(Icons.cloud_done, color: Colors.green);
case SyncStatus.error:
return Icon(Icons.cloud_off, color: Colors.red);
default:
return Icon(Icons.sync, color: Colors.grey);
}
},
),
],
),
body: StreamBuilder<List<Map<String, dynamic>>>(
stream: HarvestService.manager.dataStream,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
// Filtrar por temporada y ordenar por fecha
final allData = snapshot.data!;
final seasonData = allData
.where((item) => item['seasonId'] == widget.seasonId)
.toList();
if (seasonData.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inventory_2, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('No hay registros de cosecha',
style: TextStyle(fontSize: 18, color: Colors.grey)),
],
),
);
}
return ListView.builder(
itemCount: seasonData.length,
itemBuilder: (context, index) {
final harvest = seasonData[index];
final isSynced = harvest['sync'] == 'true';
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
leading: CircleAvatar(
backgroundColor: isSynced ? Colors.green : Colors.orange,
child: Icon(
isSynced ? Icons.cloud_done : Icons.cloud_upload,
color: Colors.white,
),
),
title: Text(harvest['folio'] ?? 'Sin folio'),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Cantidad: ${harvest['quantity']}'),
Text('Conductor: ${harvest['driver']?['name'] ?? 'N/A'}'),
Text('Cuadrilla: ${harvest['crew']?['name'] ?? 'N/A'}'),
Text('Fecha: ${_formatDate(harvest['date'])}'),
],
),
trailing: Text(
isSynced ? 'Sincronizado' : 'Pendiente',
style: TextStyle(
color: isSynced ? Colors.green : Colors.orange,
fontWeight: FontWeight.bold,
),
),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showCreateHarvestDialog(),
child: Icon(Icons.add),
),
);
}
String _formatDate(String? dateString) {
if (dateString == null) return 'N/A';
try {
final date = DateTime.parse(dateString);
return '${date.day}/${date.month}/${date.year}';
} catch (e) {
return 'Fecha inválida';
}
}
void _showCreateHarvestDialog() {
// Implementar diálogo para crear nueva cosecha
}
}
🎨 Ejemplos Completos
Ejemplo 1: Lista de Tareas
class TaskManager {
static final manager = OnlineOfflineManager(
boxName: 'tasks',
endpoint: 'tasks',
);
static Future<void> agregarTarea(String titulo, String descripcion) async {
await manager.save({
'titulo': titulo,
'descripcion': descripcion,
'completada': false,
'prioridad': 'media',
});
}
static Future<void> completarTarea(String id) async {
final tarea = await manager.getById(id);
if (tarea != null) {
tarea['completada'] = true;
tarea['fecha_completada'] = DateTime.now().toIso8601String();
await manager.save(tarea);
}
}
static Future<List<Map<String, dynamic>>> getTareasPendientes() async {
final todas = await manager.getAllWithSync();
return todas.where((tarea) => tarea['completada'] != true).toList();
}
}
class TaskListWidget extends StatefulWidget {
@override
_TaskListWidgetState createState() => _TaskListWidgetState();
}
class _TaskListWidgetState extends State<TaskListWidget> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Mis Tareas')),
body: StreamBuilder<List<Map<String, dynamic>>>(
stream: TaskManager.manager.dataStream,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
final tareas = snapshot.data!;
final pendientes = tareas.where((t) => t['completada'] != true).toList();
return ListView.builder(
itemCount: pendientes.length,
itemBuilder: (context, index) {
final tarea = pendientes[index];
return CheckboxListTile(
title: Text(tarea['titulo'] ?? 'Sin título'),
subtitle: Text(tarea['descripcion'] ?? 'Sin descripción'),
value: tarea['completada'] == true,
onChanged: (bool? value) {
if (value == true) {
TaskManager.completarTarea(tarea['id']);
}
},
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _mostrarDialogoNuevaTarea(),
child: Icon(Icons.add),
),
);
}
void _mostrarDialogoNuevaTarea() {
// Implementar diálogo para nueva tarea
}
}
Ejemplo 2: Sistema de Comentarios
class CommentSystem {
static final manager = OnlineOfflineManager(
boxName: 'comments',
endpoint: 'posts/123/comments',
);
static Future<void> agregarComentario(String autor, String mensaje) async {
await manager.save({
'autor': autor,
'mensaje': mensaje,
'timestamp': DateTime.now().toIso8601String(),
'likes': 0,
});
}
static Future<List<Map<String, dynamic>>> getComentariosRecientes() async {
final comentarios = await manager.getAllWithSync();
comentarios.sort((a, b) => b['timestamp'].compareTo(a['timestamp']));
return comentarios;
}
}
🧪 Testing
import 'package:flutter_test/flutter_test.dart';
import 'package:betuko_offline_sync/betuko_offline_sync.dart';
void main() {
group('OnlineOfflineManager Tests', () {
late OnlineOfflineManager manager;
setUp(() {
GlobalConfig.init(
baseUrl: 'https://test-api.com',
token: 'test-token',
);
manager = OnlineOfflineManager(
boxName: 'test_box',
endpoint: 'test',
);
});
tearDown(() {
manager.dispose();
GlobalConfig.clear();
});
test('debería guardar datos localmente', () async {
final testData = {
'nombre': 'Test User',
'email': 'test@ejemplo.com',
};
await manager.save(testData);
final allData = await manager.getAll();
expect(allData.length, 1);
expect(allData.first['nombre'], 'Test User');
});
test('debería obtener datos del servidor', () async {
// Mock del servidor aquí
final serverData = await manager.getFromServer();
expect(serverData, isA<List<Map<String, dynamic>>>());
});
});
}
📚 API Completa
OnlineOfflineManager
| Método | Descripción | Ejemplo |
|---|---|---|
getAll() |
Datos locales ordenados | await manager.getAll() |
getFromServer() |
Datos frescos del servidor | await manager.getFromServer() |
getAllWithSync() |
Datos con sincronización | await manager.getAllWithSync() |
getById(id) |
Un registro específico | await manager.getById('123') |
save(data) |
Guardar datos | await manager.save({...}) |
delete(id) |
Eliminar registro | await manager.delete('123') |
sync() |
Sincronización inteligente | await manager.sync() |
forceSync() |
Sincronización forzada | await manager.forceSync() |
syncNow() |
Sincronización inmediata | await manager.syncNow() |
clear() |
Limpiar todo | await manager.clear() |
getPending() |
Datos pendientes | await manager.getPending() |
getSynced() |
Datos sincronizados | await manager.getSynced() |
cleanDuplicates() |
Limpiar registros duplicados | await manager.cleanDuplicates() |
getLatestBySeason(seasonId, limit) |
Últimos N registros por temporada | await manager.getLatestBySeason('season_123', limit: 50) |
getLatestByAllSeasons(limit) |
Datos agrupados por temporada | await manager.getLatestByAllSeasons(limit: 50) |
Streams Reactivos
| Stream | Tipo | Descripción |
|---|---|---|
dataStream |
List<Map<String, dynamic>> |
Datos en tiempo real |
statusStream |
SyncStatus |
Estado de sincronización |
connectivityStream |
bool |
Estado de conectividad |
Estados de Sincronización
enum SyncStatus {
idle, // Sin actividad
syncing, // Sincronizando
success, // Éxito
error, // Error
}
🎯 Guía de Uso: ¿Cuándo usar cada método?
getAll() - Datos Locales Rápidos
✅ Usar cuando:
- Necesitas mostrar datos inmediatamente en la UI
- Trabajas en modo offline
- No requieres los datos más actualizados
getFromServer() - Datos Frescos
✅ Usar cuando:
- Necesitas los datos más recientes del servidor
- Implementas "pull to refresh"
- Quieres verificar cambios
getAllWithSync() - Lo Mejor de Ambos Mundos
✅ Usar cuando:
- Quieres datos actualizados con fallback local
- Implementas carga inicial de pantallas importantes
- Necesitas sincronización inteligente
🔄 Métodos de Sincronización
sync() - Sincronización Manual
✅ Usar cuando:
- Quieres sincronización manual que siempre sincroniza
- Necesitas datos frescos del servidor
- El usuario hace "pull to refresh" o botón de actualizar
Comportamiento:
- Siempre sincroniza (no verifica tiempo)
- Usa la misma lógica que
forceSync()ysyncNow() - Logs: "Sincronización manual iniciada..."
forceSync() - Sincronización Forzada
✅ Usar cuando:
- Quieres ignorar el caché y sincronizar siempre
- Necesitas datos frescos del servidor
- El usuario hace "pull to refresh"
Comportamiento:
- Ignora caché de tiempo
- Usa sincronización incremental optimizada
- Logs: "Sincronización forzada iniciada..."
syncNow() - Sincronización Inmediata
✅ Usar cuando:
- Necesitas sincronización inmediata sin verificaciones
- Quieres bypasar todas las optimizaciones de tiempo
- Es para casos especiales donde necesitas control total
Comportamiento:
- Omite todas las verificaciones
- Usa sincronización incremental optimizada
- Logs: "Sincronización inmediata iniciada..."
🤖 Sincronización Automática vs Manual
Sincronización Automática (por tiempo)
- Se activa en:
getAll(), timer automático, reconexión - Comportamiento: Verifica tiempo transcurrido antes de sincronizar
- Lógica: Solo sincroniza si han pasado los minutos configurados
- Método usado:
_smartSync()→_downloadFromServer() - Logs: "Sincronización automática iniciada..." o "Sincronización omitida (datos recientes)"
Sincronización Manual
- Se activa en:
sync(),forceSync(),syncNow() - Comportamiento: Siempre sincroniza sin verificar tiempo
- Lógica: Usa
_downloadFromServerManual()que siempre sincroniza - Método usado:
_syncService.sync(),_syncService.forceSync(),_syncService.syncNow() - Logs: "Sincronización manual/forzada/inmediata iniciada..."
🔧 Diferencia Técnica
Sincronización Automática:
// Usa _downloadFromServer() que verifica tiempo
if (timeSinceLastSync.inMinutes > GlobalConfig.syncTimeoutMinutes) {
await _downloadFull(); // Descarga completa
} else {
await _downloadUltraSmart(); // Verifica si es muy reciente
}
Sincronización Manual:
// Usa _downloadFromServerManual() que siempre sincroniza
if (GlobalConfig.useIncrementalSync) {
await _downloadIncremental(); // Siempre sincroniza
} else {
await _downloadFull(); // Siempre sincroniza
}
🚨 Manejo de Errores
try {
final datos = await manager.getFromServer();
// Usar datos del servidor
} catch (e) {
// Sin internet o error del servidor
final datosLocales = await manager.getAll();
// Usar datos locales como fallback
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Sin conexión. Mostrando datos locales.'),
backgroundColor: Colors.orange,
),
);
}
🧹 Manejo de Duplicados
Problema de Duplicados
Si experimentas registros duplicados en la interfaz, esto puede deberse a:
- Sincronización incremental que no detecta correctamente registros existentes
- Backend que no respeta el parámetro
sincecorrectamente - Problemas de conectividad durante la sincronización
Solución Automática
La librería ahora incluye limpieza automática de duplicados:
// La sincronización incremental limpia duplicados automáticamente
await manager.sync();
// También puedes limpiar duplicados manualmente
await manager.cleanDuplicates();
Solución Manual
Si necesitas limpiar duplicados manualmente:
class DataService {
static final manager = OnlineOfflineManager(
boxName: 'usuarios',
endpoint: 'users',
);
/// Limpiar duplicados y actualizar UI
static Future<void> cleanupDuplicates() async {
await manager.cleanDuplicates();
// La UI se actualizará automáticamente
}
/// Verificar si hay duplicados
static Future<bool> hasDuplicates() async {
final allData = await manager.getAll();
final idCounts = <String, int>{};
for (final record in allData) {
final id = record['id']?.toString();
if (id != null) {
idCounts[id] = (idCounts[id] ?? 0) + 1;
}
}
return idCounts.values.any((count) => count > 1);
}
}
UI para Limpieza de Duplicados
class DuplicateCleanupWidget extends StatefulWidget {
@override
_DuplicateCleanupWidgetState createState() => _DuplicateCleanupWidgetState();
}
class _DuplicateCleanupWidgetState extends State<DuplicateCleanupWidget> {
bool _isCleaning = false;
Future<void> _cleanDuplicates() async {
setState(() { _isCleaning = true; });
try {
await DataService.cleanupDuplicates();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Duplicados eliminados correctamente'),
backgroundColor: Colors.green,
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error al limpiar duplicados: $e'),
backgroundColor: Colors.red,
),
);
} finally {
setState(() { _isCleaning = false; });
}
}
@override
Widget build(BuildContext context) {
return FloatingActionButton(
onPressed: _isCleaning ? null : _cleanDuplicates,
child: _isCleaning
? CircularProgressIndicator(color: Colors.white)
: Icon(Icons.cleaning_services),
tooltip: 'Limpiar duplicados',
);
}
}
⚡ Optimización de Sincronización
🧠 Sincronización Ultra-Inteligente
La librería ahora incluye sincronización ultra-inteligente que optimiza el rendimiento:
- 🔍 Verificación previa: Hace una consulta pequeña para verificar si hay cambios
- 📊 Comparación local: Compara registros existentes con los del servidor
- ⚡ Procesamiento selectivo: Solo procesa registros nuevos o modificados
- 📈 Logs detallados: Muestra estadísticas de registros procesados
// ✅ Sincronización ultra-inteligente (recomendado)
await manager.sync(); // Verifica cambios antes de descargar
// ⚡ Sincronización forzada (para pull-to-refresh)
await manager.forceSync(); // Ignora caché pero usa optimizaciones
// 🚀 Sincronización inmediata (para casos especiales)
await manager.syncNow(); // Bypasa verificaciones pero optimiza descarga
📊 Comparación de Métodos
| Método | Velocidad | Uso Recomendado | Verifica Tiempo | Optimización |
|---|---|---|---|---|
sync() |
⚡⚡⚡⚡⚡ Muy rápida | Uso general | ✅ Sí | 🧠 Ultra-inteligente |
forceSync() |
⚡⚡⚡ Rápida | Pull to refresh | ❌ No | 🎯 Incremental optimizada |
syncNow() |
⚡⚡ Moderada | Casos especiales | ❌ No | 🎯 Incremental optimizada |
🎯 Cuándo Usar Cada Método
// Para la mayoría de casos (más eficiente)
await manager.sync(); // Ultra-inteligente con verificación previa
// Para "pull to refresh" en la UI
await manager.forceSync(); // Forzada pero optimizada
// Para casos especiales donde necesitas control total
await manager.syncNow(); // Inmediata pero optimizada
🚀 Optimizaciones Implementadas
Sincronización Incremental Inteligente
- Verificación previa: Hace una consulta pequeña para verificar si hay cambios
- Comparación local: Compara registros existentes con los del servidor
- Procesamiento selectivo: Solo procesa registros nuevos o modificados
- Logs detallados: Muestra cuántos registros son nuevos vs actualizados
Mejoras de Rendimiento
- Menos descargas: Solo descarga cuando realmente hay cambios
- Procesamiento eficiente: Evita actualizar registros sin cambios
- Logs informativos: Muestra estadísticas de registros procesados
- Límites de seguridad: Evita bucles infinitos en la descarga
⚡ Mejores Prácticas
1. Inicialización
// ✅ Correcto
void main() async {
WidgetsFlutterBinding.ensureInitialized();
GlobalConfig.init(baseUrl: '...', token: '...');
runApp(MyApp());
}
// ❌ Incorrecto
void main() {
runApp(MyApp());
// GlobalConfig no inicializado
}
2. Manejo de Recursos
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late OnlineOfflineManager manager;
@override
void initState() {
super.initState();
manager = OnlineOfflineManager(boxName: 'datos');
}
@override
void dispose() {
manager.dispose(); // ¡IMPORTANTE!
super.dispose();
}
}
3. Validación de Datos
Future<void> guardarUsuario(Map<String, dynamic> usuario) async {
// Validar antes de guardar
if (usuario['email'] == null || !esEmailValido(usuario['email'])) {
throw Exception('Email inválido');
}
// Agregar metadatos
usuario['created_at'] = DateTime.now().toIso8601String();
usuario['app_version'] = await getAppVersion();
await manager.save(usuario);
}
🔄 Changelog v2.0.0
✨ Nuevas Características
- 🌐 Detección automática de respuestas anidadas
{data: [...]} - 🚀 Nuevo método
getFromServer()para datos frescos del servidor - 🔄 Nuevo método
getAllWithSync()para sincronización inteligente - 📊 Mejor manejo de diferentes formatos de API
- 🛡️ Manejo de errores mejorado
- 🌾 Soporte para datos por temporada con los últimos 50 registros
- 📅 Ordenamiento automático por fecha/timestamp
- 🎯 Métodos especializados para manejo de temporadas
- ⚡ Sincronización inteligente que verifica si es necesario sincronizar
- 🚀 Múltiples métodos de sincronización para diferentes casos de uso
- 🧠 Sincronización ultra-inteligente con verificación previa de cambios
- 🎯 Sincronización incremental optimizada que solo procesa registros nuevos/modificados
- 📈 Logs detallados con estadísticas de sincronización
🔧 Mejoras
- Performance optimizada en procesamiento de respuestas
- Logs más informativos para debugging
- Documentación completa con ejemplos reales
- Mejor soporte para APIs REST estándar
- Sincronización incremental inteligente que evita descargas innecesarias
- Comparación local eficiente para detectar cambios
- Procesamiento selectivo de registros
- Límites de seguridad para evitar bucles infinitos
🐛 Correcciones
- Arreglado procesamiento de respuestas anidadas
- Mejorado manejo de errores de red
- Corregida sincronización automática
- Optimizada sincronización incremental para evitar descargas masivas
- Corregido procesamiento de registros duplicados
🤝 Contribuir
¿Encontraste un bug o tienes una idea? ¡Contribuye!
- Fork el repositorio
- Crea una rama para tu feature (
git checkout -b feature/nueva-funcionalidad) - Commit tus cambios (
git commit -m 'Agregar nueva funcionalidad') - Push a la rama (
git push origin feature/nueva-funcionalidad) - Abre un Pull Request
📄 Licencia
MIT License - ver LICENSE para más detalles.
🆘 Soporte
- 📧 Email: betuko37@gmail.com
- 🐛 Issues: GitHub Issues
- 📖 Docs: Documentación Completa
¡Hecho con ❤️ para la comunidad Flutter!
💡 Tip: ¿Primera vez usándola? Empieza con el ejemplo básico y ve probando los métodos uno por uno. ¡La librería está diseñada para ser súper fácil de usar!