Flutter para Principiantes: Construyendo una Lista de Tareas Interactivas y Persistentes con Isar DB
Este tutorial te guiará paso a paso en la creación de una aplicación de lista de tareas (To-Do List) interactiva y persistente usando Flutter. Aprenderás a diseñar la interfaz de usuario, gestionar el estado de la aplicación y almacenar datos localmente con Isar DB, una base de datos NoSQL ultrarrápida y fácil de usar.

¡Bienvenido a este tutorial completo donde construiremos una aplicación de lista de tareas con Flutter y persistiremos los datos usando Isar DB! 🚀 Aprenderás los fundamentos de la creación de interfaces de usuario en Flutter, la gestión de estado simple y cómo integrar una base de datos local para que tus tareas no se pierdan al cerrar la app.
Flutter es un framework de UI desarrollado por Google para construir aplicaciones compiladas nativamente para móvil, web y escritorio desde una única base de código. Isar DB, por su parte, es una base de datos NoSQL ligera, rápida y fácil de usar, ideal para proyectos Flutter.
🎯 Objetivos del Tutorial
Al finalizar este tutorial, serás capaz de:
- Diseñar una interfaz de usuario básica para una aplicación de lista de tareas.
- Añadir, editar y eliminar tareas.
- Marcar tareas como completadas.
- Persistir tus datos de tareas localmente usando Isar DB.
- Comprender el ciclo de vida básico de una aplicación Flutter con datos persistentes.
🛠️ Requisitos Previos
Para seguir este tutorial, necesitarás:
- Flutter SDK instalado y configurado. Si no lo tienes, puedes seguir la guía oficial de instalación de Flutter. (
- Un editor de código como VS Code o Android Studio con los plugins de Flutter y Dart instalados.
- Conocimientos básicos de Dart y de los conceptos fundamentales de Flutter (widgets, stateful/stateless).
flutter doctor en tu terminal.1. 🏗️ Configuración del Proyecto y Dependencias
Primero, crearemos un nuevo proyecto Flutter y añadiremos las dependencias necesarias para Isar DB.
Crear un Nuevo Proyecto Flutter
Abre tu terminal o símbolo del sistema y ejecuta el siguiente comando:
flutter create my_todo_app
cd my_todo_app
Esto creará un nuevo proyecto llamado my_todo_app y te moverá al directorio del proyecto.
Añadir Dependencias de Isar DB
Necesitaremos dos paquetes principales para Isar: isar para la base de datos principal y isar_flutter_libs para las bibliotecas nativas específicas de Flutter. Además, path_provider nos ayudará a encontrar la ruta correcta para almacenar la base de datos, y isar_generator junto con build_runner serán esenciales para generar el código necesario para Isar.
Abre el archivo pubspec.yaml y añade las siguientes dependencias bajo dependencies: y dev_dependencies::
dependencies:
flutter:
sdk: flutter
isar: ^3.1.0+1 # Versión actual de Isar
isar_flutter_libs: ^3.1.0+1 # Para Flutter
path_provider: ^2.0.11 # Para obtener rutas de directorio
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
isar_generator: ^3.1.0+1 # Para generar código Isar
build_runner: ^2.3.3 # Para ejecutar los generadores de código
Después de guardar el archivo pubspec.yaml, ejecuta en tu terminal:
flutter pub get
Esto descargará todas las dependencias y las hará disponibles en tu proyecto.
2. 📝 Definición del Modelo de Datos (Esquema Isar)
Antes de empezar con la UI, necesitamos definir cómo se verá una tarea en nuestra aplicación y cómo se almacenará en Isar DB. Utilizaremos anotaciones de Isar para esto.
Crea un nuevo archivo en lib/models/todo.dart con el siguiente contenido:
import 'package:isar/isar.dart';
part 'todo.g.dart'; // Isar generará este archivo
@collection
class Todo {
Id id = Isar.autoIncrement; // Campo ID auto-incremental
@Index(type: IndexType.hash)
late String title;
late bool isCompleted;
late DateTime createdAt;
late DateTime? completedAt;
Todo({
required this.title,
this.isCompleted = false,
DateTime? createdAt,
this.completedAt,
}) : createdAt = createdAt ?? DateTime.now();
}
Ahora, necesitamos generar el código para nuestro esquema de Isar. Abre tu terminal en la raíz del proyecto y ejecuta:
flutter pub run build_runner build
Si realizas cambios en el modelo Todo, deberás ejecutar este comando de nuevo. Para que se regenere automáticamente cada vez que cambie un archivo, puedes usar:
flutter pub run build_runner watch
Este comando creará el archivo lib/models/todo.g.dart que contiene la implementación necesaria para que Isar trabaje con nuestro modelo Todo.
3. ⚙️ Inicialización de Isar DB
Para poder usar Isar, necesitamos inicializarlo en nuestra aplicación. Lo haremos al inicio de la aplicación para que la base de datos esté lista antes de que se cargue cualquier widget.
Crea un nuevo archivo lib/services/isar_service.dart:
import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';
import 'package:my_todo_app/models/todo.dart'; // Importa tu modelo Todo
class IsarService {
late Future<Isar> db;
IsarService() {
db = openIsar();
}
Future<Isar> openIsar() async {
if (Isar.instanceNames.isNotEmpty) {
return Future.value(Isar.getInstance());
}
final dir = await getApplicationDocumentsDirectory();
final isar = await Isar.open(
[TodoSchema], // Aquí registramos nuestro esquema
directory: dir.path,
inspector: true, // Habilitar Isar Inspector para depuración
);
return isar;
}
// --- Operaciones CRUD para Todo --- //
// Añadir/Actualizar una tarea
Future<void> saveTodo(Todo todo) async {
final isar = await db;
await isar.writeTxn(() async {
await isar.todos.put(todo); // put() inserta si no existe, actualiza si existe
});
}
// Obtener todas las tareas
Stream<List<Todo>> listenToAllTodos() async* {
final isar = await db;
yield* isar.todos.where().watch(fireImmediately: true);
}
// Eliminar una tarea
Future<void> deleteTodo(Id id) async {
final isar = await db;
await isar.writeTxn(() async {
await isar.todos.delete(id);
});
}
// Marcar/desmarcar tarea como completada
Future<void> toggleTodoCompletion(Todo todo) async {
final isar = await db;
await isar.writeTxn(() async {
todo.isCompleted = !todo.isCompleted;
todo.completedAt = todo.isCompleted ? DateTime.now() : null;
await isar.todos.put(todo);
});
}
// Limpiar todas las tareas
Future<void> cleanDb() async {
final isar = await db;
await isar.writeTxn(() async {
await isar.clear();
});
}
}
Ahora, inicializaremos IsarService en nuestro main.dart. Abre lib/main.dart y modifícalo para que se vea así:
import 'package:flutter/material.dart';
import 'package:my_todo_app/screens/home_screen.dart'; // Crearemos esto más tarde
import 'package:my_todo_app/services/isar_service.dart';
import 'package:provider/provider.dart'; // Usaremos Provider para inyectar IsarService
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); // Asegura que Flutter esté inicializado
final isarService = IsarService(); // Inicializa el servicio Isar
runApp(MyApp(isarService: isarService));
}
class MyApp extends StatelessWidget {
final IsarService isarService;
const MyApp({Key? key, required this.isarService}) : super(key: key);
@override
Widget build(BuildContext context) {
return Provider<IsarService>.value(
value: isarService,
child: MaterialApp(
title: 'Mi App de Tareas',
theme: ThemeData(
primarySwatch: Colors.blueGrey,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const HomeScreen(), // Nuestra pantalla principal
debugShowCheckedModeBanner: false,
),
);
}
}
Hemos introducido Provider para inyectar IsarService a través del árbol de widgets. Esto nos permitirá acceder a IsarService desde cualquier parte de la aplicación de forma eficiente. Si no estás familiarizado con Provider, es un paquete popular para la gestión de estado en Flutter. Añade provider: ^6.0.5 a tu pubspec.yaml bajo dependencies: y ejecuta flutter pub get.
4. 🖼️ Diseño de la Interfaz de Usuario (UI)
Ahora vamos a construir la interfaz de usuario de nuestra aplicación de lista de tareas. Necesitaremos una pantalla principal (HomeScreen) y un widget para cada elemento de la tarea (TodoTile).
HomeScreen
Crea un nuevo archivo lib/screens/home_screen.dart:
import 'package:flutter/material.dart';
import 'package:my_todo_app/models/todo.dart';
import 'package:my_todo_app/services/isar_service.dart';
import 'package:my_todo_app/widgets/todo_tile.dart';
import 'package:provider/provider.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final TextEditingController _taskController = TextEditingController();
Todo? _editingTodo; // Para almacenar la tarea que se está editando
@override
void dispose() {
_taskController.dispose();
super.dispose();
}
void _showAddTaskDialog(BuildContext context, {Todo? todo}) {
_editingTodo = todo;
_taskController.text = todo?.title ?? '';
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text(todo == null ? 'Añadir Nueva Tarea' : 'Editar Tarea'),
content: TextField(
controller: _taskController,
autofocus: true,
decoration: const InputDecoration(
hintText: 'Escribe tu tarea aquí',
border: OutlineInputBorder(),
),
onSubmitted: (_) => _saveTask(dialogContext), // Guardar al presionar Enter
),
actions: <Widget>[
TextButton(
child: const Text('Cancelar'),
onPressed: () {
Navigator.of(dialogContext).pop();
_taskController.clear();
_editingTodo = null;
},
),
ElevatedButton(
child: Text(todo == null ? 'Añadir' : 'Guardar'),
onPressed: () => _saveTask(dialogContext),
),
],
);
},
);
}
Future<void> _saveTask(BuildContext dialogContext) async {
final isarService = Provider.of<IsarService>(context, listen: false);
if (_taskController.text.isNotEmpty) {
if (_editingTodo == null) {
// Crear nueva tarea
final newTodo = Todo(title: _taskController.text);
await isarService.saveTodo(newTodo);
} else {
// Actualizar tarea existente
_editingTodo!.title = _taskController.text;
await isarService.saveTodo(_editingTodo!);
}
_taskController.clear();
_editingTodo = null;
Navigator.of(dialogContext).pop();
}
}
void _deleteTask(BuildContext context, Todo todo) {
final isarService = Provider.of<IsarService>(context, listen: false);
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: const Text('Eliminar Tarea'),
content: Text('¿Estás seguro de que quieres eliminar "${todo.title}"?'),
actions: <Widget>[
TextButton(
child: const Text('Cancelar'),
onPressed: () => Navigator.of(dialogContext).pop(),
),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('Eliminar'),
onPressed: () async {
await isarService.deleteTodo(todo.id);
Navigator.of(dialogContext).pop();
},
),
],
);
},
);
}
void _toggleCompletion(BuildContext context, Todo todo) async {
final isarService = Provider.of<IsarService>(context, listen: false);
await isarService.toggleTodoCompletion(todo);
}
@override
Widget build(BuildContext context) {
final isarService = Provider.of<IsarService>(context);
return Scaffold(
appBar: AppBar(
title: const Text('Mi Lista de Tareas 📝'),
actions: [
IconButton(
icon: const Icon(Icons.cleaning_services),
tooltip: 'Limpiar todas las tareas',
onPressed: () async {
// Implementar un diálogo de confirmación si se desea
await isarService.cleanDb();
},
),
],
),
body: StreamBuilder<List<Todo>>(
stream: isarService.listenToAllTodos(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('¡No hay tareas aún! Añade una nueva.'));
}
final todos = snapshot.data!;
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return TodoTile(
todo: todo,
onToggleCompleted: (value) => _toggleCompletion(context, todo),
onEdit: () => _showAddTaskDialog(context, todo: todo),
onDelete: () => _deleteTask(context, todo),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddTaskDialog(context),
tooltip: 'Añadir Tarea',
child: const Icon(Icons.add),
),
);
}
}
TodoTile (Widget de una Tarea Individual)
Crea un nuevo archivo lib/widgets/todo_tile.dart:
import 'package:flutter/material.dart';
import 'package:my_todo_app/models/todo.dart';
import 'package:intl/intl.dart'; // Para formatear la fecha
// Añade intl a tu pubspec.yaml: dependencies: intl: ^0.18.1
class TodoTile extends StatelessWidget {
final Todo todo;
final ValueChanged<bool?> onToggleCompleted;
final VoidCallback onEdit;
final VoidCallback onDelete;
const TodoTile({
Key? key,
required this.todo,
required this.onToggleCompleted,
required this.onEdit,
required this.onDelete,
}) : super(key: key);
String _formatDate(DateTime date) {
return DateFormat('dd/MM/yyyy HH:mm').format(date);
}
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
elevation: 3,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: Checkbox(
value: todo.isCompleted,
onChanged: onToggleCompleted,
activeColor: Colors.blueGrey,
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isCompleted
? TextDecoration.lineThrough
: TextDecoration.none,
color: todo.isCompleted ? Colors.grey[600] : Colors.black,
fontSize: 18,
fontWeight: todo.isCompleted ? FontWeight.normal : FontWeight.w500,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Creada: ${_formatDate(todo.createdAt)}'),
if (todo.isCompleted && todo.completedAt != null)
Text('Completada: ${_formatDate(todo.completedAt!)}', style: const TextStyle(fontStyle: FontStyle.italic)),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit, color: Colors.blueAccent),
onPressed: onEdit,
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.redAccent),
onPressed: onDelete,
),
],
),
onTap: () => onToggleCompleted(!todo.isCompleted), // Alternar estado al tocar la tarea
),
);
}
}
¡No olvides añadir intl a tu pubspec.yaml!
dependencies:
flutter:
sdk: flutter
isar: ^3.1.0+1
isar_flutter_libs: ^3.1.0+1
path_provider: ^2.0.11
provider: ^6.0.5 # Asegúrate de que este también está
intl: ^0.18.1 # ¡Nuevo!
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
isar_generator: ^3.1.0+1
build_runner: ^2.3.3
Luego, ejecuta flutter pub get.
5. 🚀 Ejecutando la Aplicación
¡Felicidades! Has configurado el modelo de datos, la base de datos y la interfaz de usuario. Ahora es el momento de ver tu aplicación en acción.
Asegúrate de tener un emulador o un dispositivo físico conectado y ejecuta el siguiente comando en tu terminal desde la raíz del proyecto:
flutter run
Deberías ver tu aplicación de lista de tareas aparecer. Prueba a:
- Añadir nuevas tareas usando el botón
+. - Marcar tareas como completadas haciendo clic en la casilla o tocando la tarea.
- Editar tareas existentes usando el icono de lápiz.
- Eliminar tareas usando el icono de la papelera.
- Cerrar y reabrir la aplicación para verificar que tus tareas persisten.
- Usar el botón de
Limpiar todas las tareaspara borrar todo (¡con precaución!).
🔍 Isar Inspector
Una de las grandes ventajas de Isar es su inspector integrado. Si habilitaste inspector: true al abrir la base de datos (como hicimos en IsarService), puedes acceder a él a través de la consola de desarrollo de Flutter. Si usas VS Code, busca la pestaña "Run and Debug" y luego "Isar Inspector". Esto te permitirá ver y manipular los datos en tu base de datos en tiempo real, lo cual es increíblemente útil para la depuración.
graph TD
A[Inicio de la App]
B[main.dart: WidgetsFlutterBinding.ensureInitialized()]
C[IsarService(): Inicializar Isar DB]
D[runApp(MyApp)]
E[MyApp: Provider<IsarService>]
F[HomeScreen]
G[HomeScreen: StreamBuilder<List<Todo>>]
H[IsarService.listenToAllTodos()]
I[Lista de TodoTile]
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
G --> H
H --> G
G --> I
I --> F
subgraph Acciones del Usuario
J[Añadir Tarea]
K[Editar Tarea]
L[Eliminar Tarea]
M[Marcar/Desmarcar Tarea]
end
J --> H_update
K --> H_update
L --> H_update
M --> H_update
H_update[IsarService.saveTodo(), deleteTodo(), toggleTodoCompletion()]
H_update --> H
style A fill:#f9f,stroke:#333,stroke-width:2px
style C fill:#ccf,stroke:#333,stroke-width:2px
style E fill:#fcf,stroke:#333,stroke-width:2px
style F fill:#cfc,stroke:#333,stroke-width:2px
style G fill:#ffc,stroke:#333,stroke-width:2px
style H fill:#cff,stroke:#333,stroke-width:2px
style I fill:#fcc,stroke:#333,stroke-width:2px
style J fill:#f9f,stroke:#333,stroke-width:1px
style K fill:#f9f,stroke:#333,stroke-width:1px
style L fill:#f9f,stroke:#333,stroke-width:1px
style M fill:#f9f,stroke:#333,stroke-width:1px
El diagrama anterior muestra el flujo principal de la aplicación. Desde el inicio, IsarService se inicializa y se provee a MyApp. HomeScreen utiliza un StreamBuilder para escuchar los cambios en la lista de tareas de IsarService, y cada cambio en la base de datos (añadir, editar, eliminar) gatilla una actualización de la UI automáticamente.
💡 Mejoras y Próximos Pasos
Has construido una aplicación de lista de tareas completamente funcional con persistencia de datos. Aquí hay algunas ideas para llevarla al siguiente nivel:
- Filtros de Tareas: Añadir la capacidad de filtrar tareas por estado (completadas, pendientes).
- Ordenación: Permitir al usuario ordenar las tareas por fecha de creación, título o estado de completado.
- Animaciones: Animar la adición o eliminación de tareas de la lista para una experiencia de usuario más fluida.
- Tema Oscuro/Claro: Implementar un selector de tema para personalizar la apariencia de la aplicación.
- Notificaciones: Añadir notificaciones locales para recordar al usuario las tareas pendientes.
- Validación de Entrada: Mejorar la validación en el cuadro de diálogo para añadir/editar tareas.
- Más Campos en
Todo: Añadir campos comopriorityodueDateal modeloTodo. - Gestión de Estado más Avanzada: Para aplicaciones más grandes, podrías explorar soluciones como Riverpod, BLoC o GetX en lugar de
Providerpara una gestión de estado más compleja.
¿Qué es el `StreamBuilder` y por qué es útil aquí?
`StreamBuilder` es un widget de Flutter que construye la UI basándose en el último valor emitido por un `Stream`. En nuestro caso, `IsarService.listenToAllTodos()` devuelve un `Stream` de `List¿Por qué elegimos Isar DB en lugar de SQLite o Hive?
Isar DB es una excelente opción por varias razones:- Rendimiento: Isar es extremadamente rápido, a menudo superando a otras bases de datos locales en Flutter. Está optimizado para dispositivos móviles.
- Fácil de Usar: La API de Isar es muy intuitiva y orientada a objetos, lo que facilita la definición de esquemas y la realización de operaciones CRUD.
- Seguridad de Tipo: Gracias a su generador de código, Isar proporciona una seguridad de tipo excelente, lo que reduce los errores en tiempo de ejecución.
- Reactividad: Sus Streams incorporados (
.watch()) se integran perfectamente con los widgets reactivos de Flutter comoStreamBuilder, permitiendo actualizaciones de UI en tiempo real con poco esfuerzo. - Inspector: El Isar Inspector es una herramienta poderosa para depurar y visualizar los datos de tu base de datos en tiempo de desarrollo.
Aunque SQLite (a través de sqflite) y Hive son también buenas opciones, Isar ofrece un excelente equilibrio entre rendimiento, facilidad de uso y características modernas para el ecosistema Flutter.
Conclusión
¡Felicidades! Has completado este tutorial y has creado una aplicación de lista de tareas completamente funcional en Flutter con persistencia de datos utilizando Isar DB. Has aprendido a:
- Configurar un proyecto Flutter para la persistencia de datos.
- Definir un modelo de datos y generar esquemas Isar.
- Inicializar y realizar operaciones CRUD con Isar DB.
- Diseñar una interfaz de usuario reactiva con
StreamBuilder.
Espero que este tutorial te haya proporcionado una base sólida para tus futuros proyectos de Flutter. ¡Sigue experimentando y construyendo cosas increíbles! Si tienes alguna pregunta, no dudes en revisar la documentación oficial de Flutter e Isar.
Tutorial Completado
Tutoriales relacionados
- Flutter al Detalle: Animaciones Implícitas y Explícitas para Interfaces de Usuario Fluidas y Atractivasintermediate15 min
- Gestión de Estado Reactiva con BLoC en Flutter: Un Tutorial Completointermediate25 min
- Navegación Avanzada en Flutter: Rutas Dinámicas y Deep Linking con GoRouterintermediate18 min
- Flutter para Principiantes: Construyendo tu Primera Aplicación Interfaz de Usuario Moderna con Widgetsbeginner20 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!