Flutter: Cómo hacer CRUD con Firebase RTDB

Introducción

En la publicación anterior de Flutter: Cómo hacer un inicio de sesión de usuario con Firebase, hablamos sobre cómo implementar el inicio de sesión de usuario o la pantalla de registro con la autenticación de Firebase. Usando el mismo proyecto, vamos a mostrar CRUD o crear, leer, actualizar y eliminar operaciones con Firebase RTDB o base de datos en tiempo real en esta publicación.

Empezando

Este proyecto requiere que registre su proyecto con Firebase e incluya el archivo de configuración descargado en su proyecto. Puede obtener los pasos necesarios en la publicación anterior mencionada anteriormente. Este paso solo es necesario si prefiere configurar su propia base de datos Firebase, de lo contrario, puede usar el mío con el archivo de configuración que también he incluido en el proyecto github. Encuentra el enlace al proyecto al final de esta publicación.

Paso 1: Crear clase de modelo

Entonces, de vuelta a donde nos hemos ido anteriormente, logramos mostrar el mensaje de bienvenida después de que el usuario inicia sesión con éxito en su cuenta. Esto se muestra dentro de home_page.dart. Para que nuestra aplicación de tareas sea simple, solo vamos a almacenar el nombre de la tarea y permitir que el usuario marque como completada o no. Para almacenar información sobre cada tarea pendiente, necesitamos tener una clase de modelo. Una clase de modelo para una tarea pendiente se vería así:

/models/todo.dart

clase Todo {
  Clave de cadena;
  Sujeto de cuerda;
  bool completado;
  String userId;

  Todo (this.subject, this.userId, this.completed);

  Todo.fromSnapshot (instantánea de DataSnapshot):
    clave = instantánea.key,
    userId = snapshot.value ["userId"],
    subject = snapshot.value ["subject"],
    complete = snapshot.value ["completado"];

  toJson () {
    regreso {
      "userId": userId,
      "sujeto": sujeto,
      "completado": completado,
    };
  }
}

Cada elemento de tareas es único y tiene su propia clave. Cada elemento tiene un nombre o tema, una bandera para realizar un seguimiento de su finalización o completado y un ID de usuario de quién creó este elemento. Para crear un nuevo todo, todos los parámetros, excepto la clave, deben pasar al constructor Todo (). La clave es generada automáticamente por RTDB y almacenada cuando se agrega nuevo todo.

Cuando los datos se obtienen de Firebase RTDB, están en formato json. Por lo tanto, tenemos Todo.fromSnapshot (instantánea de DataSnapshot) que nos permite asignar datos desde el formato json al formato Todo. ToJson () hace lo contrario: asignar los datos nuevamente al formato json antes de cargarlos en Firebase RTDB.

Paso 2: Inicializar consulta

De vuelta en nuestro home_page.dart, creamos una lista de todos utilizando List _todoList = new List (). Cuando se obtiene una lista de todos de Firebase, la almacenaremos en variables de lista local.

Usamos FirebaseDatabase final _database = FirebaseDatabase.instance; para obtener acceso a la instancia de Firebase. Luego construimos una consulta desde esta instancia usando:

Consulta _todoQuery = _database
    .referencia()
    .child ("todo")
    .orderByChild ("userId")
    .equalTo (widget.userId);

En esta consulta, utilizando la instancia de FirebaseDatabase, recuperamos una referencia a todos los datos en ruta / todo. Si tiene otro nivel en todo, su consulta sería _database.reference (). Child ("todo"). Child ("otro nivel"). Tanto .orderByChild ("xx xx") como .equalTo ("xx xx") es lo que uso para decirle a Firebase que quiero una lista de todos donde el ID de usuario de cada tarea es el que le doy. ¿Tiene sentido?

Así es como se ve en la RTDB:

Paso 3: Configurar oyentes

Usando la consulta que acabamos de construir arriba, vamos a adjuntarle 2 tipos de suscripciones de flujo. Uno es onChildAdded y otro es onChildChanged. Lo que hace onChildAdded.listen () es escuchar cualquier nuevo elemento de tarea agregado en Firebase y recibe un evento y pasa a la función de devolución de llamada que en este caso es _onEntryAdded. Lo mismo ocurre con onChildChanged.listen (), que escucha cualquier cambio de datos en Firebase, como marcar todo como hecho.

_onTodoAddedSubscription = _todoQuery.onChildAdded.listen (_onEntryAdded);
_onTodoChangedSubscription = _todoQuery.onChildChanged.listen (_onEntryChanged);

Entonces, ¿cuál es la función de _onEntryAdded? Captura la instantánea del evento y la convierte de json a formato de modelo todo y se agrega a la lista de todos.

_onEntryAdded (evento evento) {
  setState (() {
    _todoList.add (Todo.fromSnapshot (event.snapshot));
  });
}

Para la función _onEntryChanged, recupera la clave de la instantánea del evento y obtiene el índice de la lista de tareas pendientes. Luego, desde el índice de la lista, actualiza ese trabajo en particular con el de la instantánea del evento.

_onEntryChanged (evento evento) {
  var oldEntry = _todoList.singleWhere ((entrada) {
    return entry.key == event.snapshot.key;
  });

  setState (() {
    _todoList [_todoList.indexOf (oldEntry)] = Todo.fromSnapshot (event.snapshot);
  });
}

Para cancelar la suscripción a StreamSubscription, simplemente usamos .cancel () dentro del método dispose ()

@anular
anular disponer () {
  _onTodoAddedSubscription.cancel ();
  _onTodoChangedSubscription.cancel ();
  super.dispose ();
}

Paso 4: construir esa vista de lista

Me gusta usar ListView cuando sea necesario para iterar sobre una lista de elementos que cambia de tamaño dinámicamente y mostrarlos en una lista. Entonces, en este caso, vamos a iterar sobre cada elemento de tarea en _todoList. ListView toma itemCount, que es simplemente el tamaño de la lista de tareas pendientes, es decir, _todoList.count. ListView también incluye itemBuilder, que es la parte que construirá el único mosaico para mostrar un solo elemento de tarea. Vamos a utilizar el widget ListTile para mostrar un solo elemento de tarea. ListTile acepta algunos parámetros, como el seguimiento para colocar el icono u otro widget en el lado derecho de ListTile, el título y los subtítulos para mostrar texto de 2 contextos y tamaños diferentes, lo que lleva a un seguimiento similar pero para el lado izquierdo de ListTile y otros.

En cada ListTile, vamos a mostrar una marca gris si no se ha completado todo y una marca verde si se ha completado. Para esto, podemos usar el operador ternario que es? , similar a una declaración if-else.

Para usarlo, proporcionamos una verificación de bool en ciertas condiciones (en este caso, verificamos el indicador completado para un elemento en Firebase) y terminamos con?

(_todoList [index] .completed)? [Hacer algo si se completa]: [Hacer algo si no se completa]

Por lo tanto, nuestro ListTile se ve así:

hijo: ListTile (
  texto del título(
    tema,
    estilo: TextStyle (fontSize: 20.0),
  ),
  Trailing: IconButton (
      icono: (_todoList [index] .completed)
          ? Icono(
        Icons.done_outline,
        color: Colors.green,
        tamaño: 20.0,
      )
          : Icono (Icons.done, color: Colors.grey, tamaño: 20.0),
      onPressed: () {
        _updateTodo (_todoList [index]);
      }),
)

Y en general ListView:

Widget _showTodoList () {
  if (_todoList.length> 0) {
    return ListView.builder (
        shrinkWrap: verdadero,
        itemCount: _todoList.length,
        itemBuilder: (contexto BuildContext, int index) {
          Cadena todoId = _todoList [index] .key;
          String subject = _todoList [index] .subject;
          bool complete = _todoList [index] .completed;
          Cadena userId = _todoList [index] .userId;
          volver Descartable (
            clave: Clave (todoId),
            Fondo: Contenedor (color: Colors.red),
            onDismissed: (dirección) asíncrono {
              _deleteTodo (todoId, index);
            },
            hijo: ListTile (
              texto del título(
                tema,
                estilo: TextStyle (fontSize: 20.0),
              ),
              Trailing: IconButton (
                  icono: (completado)
                      ? Icono(
                    Icons.done_outline,
                    color: Colors.green,
                    tamaño: 20.0,
                  )
                      : Icono (Icons.done, color: Colors.grey, tamaño: 20.0),
                  onPressed: () {
                    _updateTodo (_todoList [index]);
                  }),
            ),
          );
        });
  } más {
    return Center (child: Text ("Bienvenido. Su lista está vacía",
      textAlign: TextAlign.center,
      estilo: TextStyle (fontSize: 30.0),));
  }
}

Observe que ListTile está envuelto con otra llamada de widget Dismissible. Este es un widget que permite al usuario deslizar todo el ListTile para imitar el deslizamiento de acción para eliminar.

Paso 5: Fabuloso FAB

Todavía en home_page.dart, en el método de construcción que devuelve un Andamio, debajo del cuerpo, vamos a crear un botón de acción flotante o FAB. El propósito de este botón es permitir que el usuario agregue nuevas tareas a la lista. La FAB mostrará un cuadro de diálogo de alerta que contiene un campo de texto para que el usuario ingrese el nombre de la nueva tarea.

floatingActionButton: FloatingActionButton (
  onPressed: () {
    _showDialog (contexto);
  },
  información sobre herramientas: 'Incremento',
  hijo: Icon (Icons.add),
)

Para que aparezca el diálogo de alerta, no puede simplemente devolver un AlertDialog y esperar que se muestre. En su lugar, debemos usar await showDialog () y devolver AlertDialog dentro de este generador. AlertDialog albergará un campo de texto cuyo valor será contenido por textEditingController con 2 FlatButtons de guardar y cancelar. El botón Guardar obviamente obtendrá el nuevo nombre del elemento de tarea y creará una nueva instancia de tarea antes de cargarlo en Firebase.

_showDialog (contexto BuildContext) asíncrono {
  _textEditingController.clear ();
  aguarda showDialog  (
      contexto: contexto,
      constructor: (contexto BuildContext) {
        volver AlertDialog (
          contenido: nueva fila (
            hijos:  [
              nuevo expandido (
                  hijo: nuevo TextField (
                controlador: _textEditingController,
                enfoque automático: verdadero,
                decoración: nueva InputDecoration (
                  labelText: 'Agregar nuevo todo',
                ),
              ))
            ],
          ),
          acciones:  [
            nuevo FlatButton (
                hijo: const Text ('Cancelar'),
                onPressed: () {
                  Navigator.pop (contexto);
                }),
            nuevo FlatButton (
                hijo: const Text ('Guardar'),
                onPressed: () {
                  _addNewTodo (_textEditingController.text.toString ());
                  Navigator.pop (contexto);
                })
          ],
        );
      });
}

Paso 5: CRUDOS

Crear

Para crear un nuevo elemento de tarea, tomaremos el nombre ingresado por el usuario en TextField dentro de AlertDialog cuando toquen FloatingActionButton. Instanciamos un nuevo objeto todo con la entrada de nombre. Finalmente subimos a Firebase usando _database.reference (). Child ("todo"). Push (). Set (todo.toJson ())

_addNewTodo (String todoItem) {
  if (todoItem.length> 0) {
    Todo todo = nuevo Todo (todoItem.toString (), widget.userId, false);
    _database.reference (). child ("todo"). push (). set (todo.toJson ());
  }
}

Leer

Para leer, se ha mencionado anteriormente que necesitará crear una consulta que sea:

_todoQuery = _database
    .referencia()
    .child ("todo")
    .orderByChild ("userId")
    .equalTo (widget.userId);

A partir de la consulta, adjuntaremos 2 escuchas que son onChildAdded y onChildChanged que activarán cada método de devolución de llamada respectivo con instantáneas de eventos. De la instantánea del evento, simplemente los convertimos en clase de todo y agregamos a la lista

_onEntryAdded (evento evento) {
  setState (() {
    _todoList.add (Todo.fromSnapshot (event.snapshot));
  });
}
_onEntryChanged (evento evento) {
  var oldEntry = _todoList.singleWhere ((entrada) {
    return entry.key == event.snapshot.key;
  });

  setState (() {
    _todoList [_todoList.indexOf (oldEntry)] =
        Todo.fromSnapshot (event.snapshot);
  });
}

Para ayudar a mejorar las consultas basadas en userId, se recomienda establecer una regla en las reglas de Firebase RTDB. Esta es la indexación de llamadas y ayuda a Firebase a optimizar su disposición de datos para mejorar el tiempo de respuesta. Para obtener más información, consulte Indexar sus datos por Firebase.

{
  / * Visite https://firebase.google.com/docs/database/security para obtener más información sobre las reglas de seguridad. * /
  "reglas": {
    "que hacer": {
      ".indexOn": "userId",
    },
    ".read": verdadero,
    ".write": verdadero
  }
}

Actualizar

El usuario puede marcar cada elemento de tarea como completado o deshacer este paso. Simplemente pueden tocar el ícono de marca en el lado derecho de cada todoListTile. Para la actualización, necesitamos obtener todo.key ya que necesitamos acceder a path / todo / todo-unique-key para poder actualizar lo que está en esa ruta. El método es similar a Crear en el sentido de que está usando .set () pero la diferencia es la adición de .child (todo.key) en la ruta.

_updateTodo (Todo todo) {
  // Alternar completado
  todo.completed =! todo.completed;
  if (todo! = null) {
    _database.reference (). child ("todo"). child (todo.key) .set (todo.toJson ());
  }
}

Eliminar

Eliminar elementos de Firebase es sencillo. Similar a la Actualización, necesitamos obtener el todo.key correcto pero usaremos el método .remove ().

Tenga en cuenta que no hay escucha para la eliminación de elementos, a diferencia de la escucha para el elemento agregado o modificado. Por lo tanto, no hay forma de que esos 2 oyentes se activen y obtengan la última instantánea de la base de datos. Para esto, necesitamos eliminar manualmente el elemento de nuestra variable local _todoList solo cuando la eliminación de Firebase sea exitosa.
_deleteTodo (String todoId, int index) {
  _database.reference (). child ("todo"). child (todoId) .remove (). then ((_) {
    print ("Eliminar $ todoId exitoso");
    setState (() {
      _todoList.removeAt (index);
    });
  });
}

Manifestación

Así es como se ve la aplicación

Demo de la aplicación final

Github

Código fuente disponible:

https://github.com/tattwei46/flutter_login_demo

Apreciación

Gracias por tomarse el tiempo de leer esta publicación. Espero que te ayude en tu maravilloso viaje con Flutter. Si lo encuentra útil, por favor para alentarme a escribir más artículos como este