Guardar en base de datos con Flutter

Guardar en base de datos con Flutter

Photo by Tim Evans on Unsplash.

Almacenar texto en la base de datos es junto a las llamadas a red, una de las operaciones que como desarrolladores m谩s tenemos que realizar. Algunas veces, necesitamos guardar la informaci贸n de los servidores en nuestra app para que los usuarios puedan abrir la app y ver informaci贸n previamente guardada antes de que se actualice con nuevo contenido, o podemos tambi茅n tener que implementar un modo offline.

En este art铆culo, vamos a hablar sobre c贸mo almacenar informaci贸n en una app con Flutter. Nuestra app ser谩 una aplicaci贸n que almacenar谩 unas Tareas por realizar (Todo’s), las cuales podremos consultar luego.

Una lista de las cosas que vamos a necesitar:

  • Una clase de Tareas por realizar (De ahora en adelante las llamaremos Todo).
  • Una Screen, la cual es un Widget que nos mostrar谩 unos campos donde podremos crear nuestro Todo.
  • Agregar las dependencias de los paquetes que utilizaremos. Un paquete es lo que en otros lenguajes puede conocerse como librer铆a, nosotros utilizaremos sqflite.
  • Una clase DatabaseHelper la cual se encargar谩 de gestionar la base de datos.

https://www.codingpizza.com/wp-content/uploads/2020/01/Screenshot_1578338767.png

Primero, vamos a crear nuestra clase Todo.

class Todo {
  final int id;
  final String content;
  final String title;
  static const String TABLENAME = "todos";

  Todo({this.id, this.content, this.title});

  Map<String, dynamic> toMap() {
    return {'id': id, 'content': content, 'title': title};
  }
}

Esta es la clase Todo la cual contiene un id, el contenido del Todo y el t铆tulo. Adem谩s, agregamos una constante que incluye el nombre de la tabla de Todos, as铆 como tambi茅n una funci贸n toMap que utilizaremos m谩s adelante.

Con la clase Todo creada, vamos a agregar nuestra dependencia de la siguiente forma en el archivo pubspec.yml.

dependencies:
  flutter:
    sdk: flutter
  sqflite:
  path:

Ahora tenemos que crear nuestra clase DatabaseHelper, la cual tiene la funci贸n de tratar con la base de datos.

class DatabaseHelper {
  //Create a private constructor
  DatabaseHelper._();

  static const databaseName = 'todos_database.db';
  static final DatabaseHelper instance = DatabaseHelper._();
  static Database _database;

  Future<Database> get database async {
    if (_database == null) {
      return await initializeDatabase();
    }
    return _database;
  }

  initializeDatabase() async {
    return await openDatabase(join(await getDatabasesPath(), databaseName),
        version: 1, onCreate: (Database db, int version) async {
      await db.execute(
          "CREATE TABLE todos(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, title TEXT, content TEXT)");
    });
  }
}

Tenemos varias cosas de las que hablar sobre esta clase. Para empezar, acceder a una base de datos es una operaci贸n as铆ncrona, lo que significa que debemos utilizar Futures para inicializar la propiedad database.

En la funci贸n initializeDatabase vamos a inicializar una base de datos siempre y cuando no tengamos una ya creada previamente, para inicializarla vamos a llamar la funci贸n initializeDatabase y dentro de ella llamaremos a la funci贸n openDatabase de la librer铆a de sqflite.

La funci贸n openDatabase necesita los siguientes par谩metros:

  • Un String que ser铆a el Path de la base de datos. Para esto, debido a que tratamos con sistemas operativos diversos, hay que utilizar la funci贸n join() que se encargar谩 de unir los path que vamos a obtener con la funci贸n getDatabasesPath().
  • Una funci贸n an贸nima que ser谩 ejecutada cuando la base de datos sea creada. En nuestro caso, una funci贸n an贸nima con una query crear谩 una tabla dentro de la base de datos.

La funci贸n insertTodo() toma como par谩metro un objeto Todo. Obtiene la base de datos que hab铆amos creado previamente y utiliza la funci贸n de la librer铆a sqflite execute() para insertar los datos en nuestra base de datos. A esta funci贸n le vamos a pasar los siguientes par谩metros.

  • El nombre de la tabla en la cual vamos a insertar la informaci贸n. En nuestro caso tenemos el nombre declarado como una constante en nuestra clase de Todo.
  • Un map que incluye la informaci贸n que queremos insertar. Para esto hemos creado la funci贸n toMap de nuestra clase Todo que convierte nuestro objeto Todo a un Map.
  • Y una constante definida por la librer铆a que especificar谩 qu茅 acci贸n tomar cuando se encuentre un conflicto. Nosotros vamos a tomar la acci贸n de reemplazar siempre que tenga un conflicto.

Finalmente, podemos crear nuestra Screen la cual nos servir谩 para crear nuestro Todo object. Dado que los campos ser谩n editables necesitaremos utilizar un StatefulWidget con su respectivo State. Los widgets que vamos a utilizar son Textfields y FloatingActionButton para guardar el Todo.

class CreateTodoScreen extends StatefulWidget {

  @override
  State<StatefulWidget> createState() {
    return _CreateTodoState();
  }
}

class CreateTodoScreen extends StatefulWidget {

  @override
  State<StatefulWidget> createState() {
    return _CreateTodoState();
  }
}

class _CreateTodoState extends State<CreateTodoScreen> {
  final descriptionTextController = TextEditingController();
  final titleTextController = TextEditingController();

  @override
  void dispose() {
    super.dispose();
    descriptionTextController.dispose();
    titleTextController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Retrieve Text Input'),
      ),
      body: Column(
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: TextField(
              decoration: InputDecoration(
                  border: OutlineInputBorder(), labelText: "Title"),
              maxLines: 1,
              controller: titleTextController,
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: TextField(
              decoration: InputDecoration(
                  border: OutlineInputBorder(), labelText: "Description"),
              maxLines: 10,
              controller: descriptionTextController,
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
          child: Icon(Icons.check),
          onPressed: () {
            DatabaseHelper.instance.insertTodo(Todo(
              title: titleTextController.text,
              content: descriptionTextController.text));
            Navigator.pop(context, "Your todo has been saved.");
          }),
    );
  }
}

Luego de crear nuestro State necesitamos utilizar un TextEditingController para cada uno de nuestros TextFields, estos los declaramos al principio de nuestro Widget y hacemos un override de la funci贸n dispose() para llamar a la respectiva funci贸n dispose de cada TextEditingController.

Al momento de crear nuestros TextFields, le pasamos a cada TextField su respectivo controller. Finalmente, para llamar al DatabaseHelper e indicarle que llame a la funci贸n insertTodo debemos pasarle a la propiedad onPressed del floatingActionButton una funci贸n an贸nima con un Todo creado usando el texto obtenido de los TextEditingController.

https://www.codingpizza.com/wp-content/uploads/2020/01/Screenshot_1578338787.png

Actualizando nuestros Todo’s

Para actualizar nuestros Todo’s necesitamos hacer ciertas modificaciones. Primero, vamos a agregar esta nueva funci贸n a nuestro DatabaseHelper.

updateTodo(Todo todo) async {
    final db = await database;

    await db.update(Todo.TABLENAME, todo.toMap(),
        where: 'id = ?',
        whereArgs: [todo.id],
        conflictAlgorithm: ConflictAlgorithm.replace);
  }

En esta funci贸n obtenemos el objeto database y luego llamamos a la funci贸n update() de la librer铆a y le pasamos los siguientes par谩metros.

  • El nombre de la tabla que hemos declarado previamente en nuestro objeto Todo.
  • El objeto Todo convertido a un map usando la funci贸n toMap.
  • La cl谩usula where la cual aplicar谩 cambios en cualquier fila que cumpla con esa condici贸n
  • El par谩metro whereArgs que sustituir谩 el signo de interrogaci贸n en nuestra cl谩usula where.
  • Y finalmente, una constante que especifica el tipo de algoritmo a usar en caso de que haya un conflicto.

Ahora debemos actualizar nuestra UI. Primero, vamos a modificar nuestra DetailTodoScreen para poder editar nuestro Todo. Necesitaremos agregar nuestro Todo como un par谩metro opcional en el constructor del DetailTodoScreen, luego crear una property Todo en nuestro Widget y debido a que es un StatefulWidget debemos pasarle este Todo al State.

class DetailTodoScreen extends StatefulWidget {
    final Todo todo;

  const DetailTodoScreen({Key key, this.todo}) : super(key: key);

  @override
  State<StatefulWidget> createState() => _CreateTodoState(todo);
}

En nuestro State necesitamos agregar tambi茅n nuestro Todo al constructor, crear la property Todo y sobrescribir la funci贸n initState de nuestro Widget para que podamos asignar el T铆tulo y la descripci贸n de nuestro Todo a los respectivos TextField.

class _CreateTodoState extends State<DetailTodoScreen> {
  Todo todo;
  final descriptionTextController = TextEditingController();
  final titleTextController = TextEditingController();

  _CreateTodoState(this.todo);

  @override
  void initState() {
    super.initState();
    if (todo != null) {
      descriptionTextController.text = todo.content;
      titleTextController.text = todo.title;
    }
  }

Finalmente, en la funci贸n an贸nima que le pasamos como par谩metro onPressed vamos a llamar a otra funci贸n llamada saveTodo, la cual se encargar谩 de insertar un nuevo Todo en caso de que no tengamos ning煤n Todo para editar o de modificar un Todo existente que hayamos pasado como par谩metro a nuestro State. Al final debemos llamar tambi茅n a la funci贸n setState.

As铆 es como se ver谩 nuestro State luego de esas modificaciones.

class _CreateTodoState extends State<DetailTodoScreen> {
  Todo todo;
  final descriptionTextController = TextEditingController();
  final titleTextController = TextEditingController();

  _CreateTodoState(this.todo);

  @override
  void initState() {
    super.initState();
    if (todo != null) {
      descriptionTextController.text = todo.content;
      titleTextController.text = todo.title;
    }
  }

  @override
  void dispose() {
    super.dispose();
    descriptionTextController.dispose();
    titleTextController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Retrieve Text Input'),
      ),
      body: Column(
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: TextField(
              decoration: InputDecoration(
                  border: OutlineInputBorder(), labelText: "Title"),
              maxLines: 1,
              controller: titleTextController,
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: TextField(
              decoration: InputDecoration(
                  border: OutlineInputBorder(), labelText: "Description"),
              maxLines: 10,
              controller: descriptionTextController,
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
          child: Icon(Icons.check),
          onPressed: () async {
            _saveTodo(titleTextController.text, descriptionTextController.text);
            setState(() {});
          }),
    );
  }

  _saveTodo(String title, String content) async {
    if (todo == null) {
      DatabaseHelper.instance.insertTodo(Todo(
          title: titleTextController.text,
          content: descriptionTextController.text));
      Navigator.pop(context, "Your todo has been saved.");
    } else {
      await DatabaseHelper.instance
          .updateTodo(Todo(id: todo.id, title: title, content: content));
      Navigator.pop(context);
    }
  }
}

Deleting a Todo

Para borrar un Todo necesitamos comenzar por borrar una funci贸n de nuestra clase DatabaseHelper.

deleteTodo(int id) async {
    var db = await database;
    db.delete(Todo.TABLENAME, where: 'id = ?', whereArgs: [id]);
  }

Esta funci贸n llama a la funci贸n delete de la librer铆a Sqflite a la cual le pasamos el nombre de la tabla la cl谩usula where y el id del Todo a eliminar.

Luego, para agregar una funci贸n de borrado en nuestra lista de todos vamos a necesitar realizar los siguientes pasos. Primero, en nuestro ListTile widget vamos a necesitar a utilizar un par谩metro opcional llamado trailing, en este par谩metro vamos a pasarle un IconButton, dentro de ese par谩metro onPressed vamos a pasarle una funci贸n an贸nima que llamar谩 a la funci贸n del DatabaseHelper encargada de borrar el Todo.

class  ReadTodoScreen extends StatefulWidget {
  @override
  _ReadTodoScreenState createState() => _ReadTodoScreenState();
}

class _ReadTodoScreenState extends State<ReadTodoScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Saved Todos'),
      ),
      body: FutureBuilder<List<Todo>>(
        future: DatabaseHelper.instance.retrieveTodos(),
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            return ListView.builder(
              itemCount: snapshot.data.length,
              itemBuilder: (BuildContext context, int index) {
                return ListTile(
                  title: Text(snapshot.data[index].title),
                  leading: Text(snapshot.data[index].id.toString()),
                  subtitle: Text(snapshot.data[index].content),
                  onTap: () => _navigateToDetail(context, snapshot.data[index]),
                  trailing: IconButton(
                      alignment: Alignment.center,
                      icon: Icon(Icons.delete),
                      onPressed: () async {
                        _deleteTodo(snapshot.data[index]);
                        setState(() {});
                      }),
                );
              },
            );
          } else if (snapshot.hasError) {
            return Text("Oops!");
          }
          return Center(child: CircularProgressIndicator());
        },
      ),
    );
  }
}

_deleteTodo(Todo todo) {
  DatabaseHelper.instance.deleteTodo(todo.id);
}

_navigateToDetail(BuildContext context, Todo todo) async {
  Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => DetailTodoScreen(todo: todo)),
  );
}

https://www.codingpizza.com/wp-content/uploads/2020/01/Screenshot_1578338793.png

隆Y eso es todo! Ahora tenemos un app que puede almacenar un Todo, leerlo desde base de datos, actualizarlo y borrarlo.

El c贸digo est谩 disponible aqu铆. Si quieres leer m谩s art铆culos como estos o tienes alguna duda, puedes encontrarme en las siguientes redes sociales.

Instagram: www.instagram.com/codingpizza
Twitter: www.twitter.com/coding__pizza
Facebook: www.facebook.com/codingpizza
Site: www.codingpizza.com

Sin comentarios

Deja un comentario

Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si contin煤a navegando est谩 dando su consentimiento para la aceptaci贸n de las mencionadas cookies y la aceptaci贸n de nuestra pol铆tica de cookies, pinche el enlace para mayor informaci贸n.

ACEPTAR
Aviso de cookies