tutoriales.com

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.

Principiante30 min de lectura18 views
Reportar error
Flutter para Principiantes: Construyendo una Lista de Tareas Interactivas y Persistentes con Isar DB

¡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).
💡 Consejo: Asegúrate de que tu entorno de desarrollo está listo ejecutando 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();
}
🔥 Importante: La línea `part 'todo.g.dart';` es crucial. Isar utilizará `build_runner` para generar automáticamente el código necesario para tu modelo.

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();
    });
  }
}
📌 Nota: Hemos añadido los métodos básicos CRUD (Crear, Leer, Actualizar, Borrar) directamente en `IsarService` para simplificar la gestión de datos en este tutorial. En aplicaciones más grandes, podrías querer separar estas lógicas.

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 tareas para 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.

Inicio Inicializar IsarService MyApp (Provider de IsarService) HomeScreen (StreamBuilder de tareas Isar)
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 como priority o dueDate al modelo Todo.
  • Gestión de Estado más Avanzada: Para aplicaciones más grandes, podrías explorar soluciones como Riverpod, BLoC o GetX en lugar de Provider para 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`. Cada vez que hay un cambio en la base de datos de Isar (por ejemplo, se añade una tarea, se edita, se elimina), Isar emite una nueva lista de tareas a través de este `Stream`. `StreamBuilder` captura ese nuevo dato y reconstruye automáticamente la parte de la UI que muestra la lista de tareas, lo que resulta en una interfaz reactiva y actualizada en tiempo real sin tener que gestionar manualmente las actualizaciones con `setState()` en `HomeScreen`.
¿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 como StreamBuilder, 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

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!