Cómo crear una aplicación móvil de pila completa con Flutter, Fauna y GraphQL | Programar Plus

Flutter es el marco de interfaz de usuario de Google que se utiliza para crear aplicaciones móviles multiplataforma expresivas y flexibles. Es uno de los marcos de más rápido crecimiento para el desarrollo de aplicaciones móviles. Por otro lado, Fauna es una base de datos transaccional sin servidor amigable para desarrolladores que soporta GraphQL nativo. Flutter + Fauna es una combinación hecha en el cielo. Si está buscando construir y enviar una aplicación de pila completa rica en funciones en un tiempo récord, Flutter and Fauna es la herramienta adecuada para el trabajo. En este artículo, lo guiaremos a través de la construcción de su primera aplicación Flutter con el back-end de Fauna y GraphQL.

Puede encontrar el código completo de este artículo en GitHub.

Objetivo de aprendizaje

Al final de este artículo, debería saber cómo:

  1. configurar una instancia de Fauna,
  2. componer el esquema GraphQL para Fauna,
  3. configurar el cliente GraphQL en una aplicación Flutter, y
  4. realizar consultas y mutaciones contra el back-end de Fauna GraphQL.

Fauna vs. AWS Amplify vs. Firebase: ¿Qué problemas resuelve Fauna? ¿En qué se diferencia de otras soluciones sin servidor? Si es nuevo en Fauna y desea obtener más información sobre cómo se compara Fauna con otras soluciones, le recomiendo leer este artículo.

¿Qué estamos construyendo?

Crearemos una aplicación móvil simple que permitirá a los usuarios agregar, eliminar y actualizar sus personajes favoritos de películas y programas.

Configurando Fauna

Dirígete a fauna.com y crea una nueva cuenta. Una vez que haya iniciado sesión, debería poder crear una nueva base de datos.

Dale un nombre a tu base de datos. Voy a nombrar el mio flutter_demo. A continuación, podemos seleccionar un grupo de regiones. Para esta demostración, elegiremos clásico. Fauna es una base de datos sin servidor distribuida globalmente. Es la única base de datos que admite acceso de lectura y escritura de baja latencia desde cualquier lugar. Piense en ello como CDN (Content Delivery Network) pero para su base de datos. Para obtener más información sobre los grupos regionales, siga esta guía.

Generando una clave de administrador

Una vez que se crea la base de datos, diríjase a la pestaña de seguridad. Haga clic en el botón de nueva clave y cree una nueva clave para su base de datos. Mantenga esta clave segura ya que la necesitamos para nuestras operaciones GraphQL.

Crearemos una clave de administrador para nuestra base de datos. Las claves con un rol de administrador se utilizan para administrar su base de datos asociada, incluidos los proveedores de acceso a la base de datos, las bases de datos secundarias, los documentos, las funciones, los índices, las claves, los tokens y los roles definidos por el usuario. Puede obtener más información sobre las diversas claves de seguridad y roles de acceso de Fauna en el siguiente enlace.

Redactar un esquema GraphQL

Crearemos una aplicación simple que permitirá a los usuarios agregar, actualizar y eliminar sus personajes de TV favoritos.

Creando un nuevo proyecto de Flutter

Creemos un nuevo proyecto de flutter ejecutando los siguientes comandos.

flutter create my_app

Dentro del directorio del proyecto, crearemos un nuevo archivo llamado graphql/schema.graphql.

En el archivo de esquema, definiremos la estructura de nuestra colección. Las colecciones de Fauna son similares a las tablas de SQL. Solo necesitamos una colección por ahora. Lo llamaremos Character.

### schema.graphql
type Character {
    name: String!
    description: String!
    picture: String
}
type Query {
    listAllCharacters: [Character]
}

Como puede ver arriba, definimos un tipo llamado Character con varias propiedades (es decir, name, description, picture, etc.). Piense en las propiedades como columnas de una base de datos SQL o un valor clave pagado de una base de datos NoSQL. También hemos definido una consulta. Esta consulta devolverá una lista de los caracteres.

Ahora volvamos al panel de Fauna. Haga clic en GraphQL y haga clic en importar esquema para cargar nuestro esquema en Fauna.

Una vez realizada la importación, veremos que Fauna ha generado las consultas y mutaciones GraphQL.

¿No te gusta GraphQL generado automáticamente? ¿Quiere tener más control sobre su lógica empresarial? En ese caso, Fauna le permite definir sus resolutores GraphQL personalizados. Para obtener más información, siga este enlace.

Configurar el cliente GraphQL en la aplicación Flutter

Abramos nuestro pubspec.yaml archivo y agregue las dependencias requeridas.

...
dependencies:
  graphql_flutter: ^4.0.0-beta
  hive: ^1.3.0
  flutter:
    sdk: flutter
...

Agregamos dos dependencias aquí. graphql_flutter es una biblioteca cliente GraphQL para flutter. Reúne todas las características modernas de los clientes GraphQL en un paquete fácil de usar. También agregamos el hive package como nuestra dependencia. Hive es una base de datos liviana de clave-valor escrita en Dart puro para almacenamiento local. Estamos usando hive para almacenar en caché nuestras consultas GraphQL.

A continuación, crearemos un nuevo archivo. lib/client_provider.dart. Crearemos una clase de proveedor en este archivo que contendrá nuestra configuración de Fauna.

Para conectarnos a la API GraphQL de Fauna, primero debemos crear un GraphQLClient. Un GraphQLClient requiere una caché y un enlace para inicializarse. Echemos un vistazo al código a continuación.

// lib/client_provider.dart
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:flutter/material.dart';

ValueNotifier<GraphQLClient> clientFor({
  @required String uri,
  String subscriptionUri,
}) {

  final HttpLink httpLink = HttpLink(
    uri,
  );
  final AuthLink authLink = AuthLink(
    getToken: () async => 'Bearer fnAEPAjy8QACRJssawcwuywad2DbB6ssrsgZ2-2',
  );
  Link link = authLink.concat(httpLink);
  return ValueNotifier<GraphQLClient>(
    GraphQLClient(
      cache: GraphQLCache(store: HiveStore()),
      link: link,
    ),
  );
} 

En el código anterior, creamos un ValueNotifier para envolver el GraphQLClient. Observe que configuramos AuthLink en las líneas 13 a 15 (resaltadas). En la línea 14, hemos agregado la clave de administrador de Fauna como parte del token. Aquí he codificado la clave de administración. Sin embargo, en una aplicación de producción, debemos evitar codificar las claves de seguridad de Fauna.

Hay varias formas de almacenar secretos en la aplicación Flutter. Por favor, eche un vistazo a esta publicación de blog como referencia.

Queremos poder llamar Query y Mutation desde cualquier widget de nuestra aplicación. Para hacerlo, necesitamos envolver nuestros widgets con GraphQLProvider widget.

// lib/client_provider.dart

....

/// Wraps the root application with the `graphql_flutter` client.
/// We use the cache for all state management.
class ClientProvider extends StatelessWidget {
  ClientProvider({
    @required this.child,
    @required String uri,
  }) : client = clientFor(
          uri: uri,
        );
  final Widget child;
  final ValueNotifier<GraphQLClient> client;
  @override
  Widget build(BuildContext context) {
    return GraphQLProvider(
      client: client,
      child: child,
    );
  }
}

A continuación, vamos a nuestro main.dart archivo y envuelva nuestro widget principal con el ClientProvider widget. Echemos un vistazo al código a continuación.

// lib/main.dart
...

void main() async {
  await initHiveForFlutter();
  runApp(MyApp());
}
final graphqlEndpoint="https://graphql.fauna.com/graphql";
class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return ClientProvider(
      uri: graphqlEndpoint,
      child: MaterialApp(
        title: 'My Character App',
        debugShowCheckedModeBanner: false,
        initialRoute: "https://css-tricks.com/",
        routes: {
          "https://css-tricks.com/": (_) => AllCharacters(),
          '/new': (_) => NewCharacter(),
        }
      ),
    );
  }
}

En este punto, todos nuestros widgets posteriores tendrán acceso para ejecutar Queries y Mutations funciones y puede interactuar con la API GraphQL.

Páginas de aplicación

Las aplicaciones de demostración deben ser sencillas y fáciles de seguir. Sigamos adelante y creemos un widget de lista simple que mostrará la lista de todos los personajes. Creemos un nuevo archivo lib/screens/character-list.dart. En este archivo, escribiremos un nuevo widget llamado AllCharacters.

// lib/screens/character-list.dart.dart

class AllCharacters extends StatelessWidget {
  const AllCharacters({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            pinned: true,
            snap: false,
            floating: true,
            expandedHeight: 160.0,
            title: Text(
              'Characters',
              style: TextStyle(
                fontWeight: FontWeight.w400, 
                fontSize: 36,
              ),
            ),
            actions: <Widget>[
              IconButton(
                padding: EdgeInsets.all(5),
                icon: const Icon(Icons.add_circle),
                tooltip: 'Add new entry',
                onPressed: () { 
                  Navigator.pushNamed(context, '/new');
                },
              ),
            ],
          ),
          SliverList(
            delegate: SliverChildListDelegate([
                Column(
                  children: [
                    for (var i = 0; i < 10; i++) 
                      CharacterTile()
                  ],
                )
            ])
          )
        ],
      ),
    );
  }
}

// Character-tile.dart
class CharacterTile extends StatefulWidget {
  CharacterTilee({Key key}) : super(key: key);
  @override
  _CharacterTileState createState() => _CharacterTileeState();
}
class _CharacterTileState extends State<CharacterTile> {
  @override
  Widget build(BuildContext context) {
    return Container(
       child: Text(&quot;Character Tile&quot;),
    );
  }
}

Como puede ver en el código anterior, [line 37] tenemos un bucle for para completar la lista con algunos datos falsos. Eventualmente, haremos una consulta GraphQL a nuestro backend de Fauna y buscaremos todos los caracteres de la base de datos. Antes de hacer eso, intentemos ejecutar nuestra aplicación tal como está. Podemos ejecutar nuestra aplicación con el siguiente comando

flutter run

En este punto deberíamos poder ver la siguiente pantalla.

Realización de consultas y mutaciones

Ahora que tenemos algunos widgets básicos, podemos continuar y conectar consultas GraphQL. En lugar de cadenas codificadas, nos gustaría obtener todos los caracteres de nuestra base de datos y verlos en AllCharacters widget.

Volvamos a la zona de juegos GraphQL de Fauna. Observe que podemos ejecutar la siguiente consulta para enumerar todos los caracteres.

query ListAllCharacters {
  listAllCharacters(_size: 100) {
    data {
      _id
      name
      description
      picture
    }
    after
  }
}

Para realizar esta consulta desde nuestro widget, necesitaremos realizar algunos cambios.

import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:todo_app/screens/Character-tile.dart';

String readCharacters = ";";";
query ListAllCharacters {
  listAllCharacters(_size: 100) {
    data {
      _id
      name
      description
      picture
    }
    after
  }
}
";";";;

class AllCharacters extends StatelessWidget {
  const AllCharacters({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            pinned: true,
            snap: false,
            floating: true,
            expandedHeight: 160.0,
            title: Text(
              'Characters',
              style: TextStyle(
                fontWeight: FontWeight.w400, 
                fontSize: 36,
              ),
            ),
            actions: <Widget>[
              IconButton(
                padding: EdgeInsets.all(5),
                icon: const Icon(Icons.add_circle),
                tooltip: 'Add new entry',
                onPressed: () { 
                  Navigator.pushNamed(context, '/new');
                },
              ),
            ],
          ),
          SliverList(
            delegate: SliverChildListDelegate([
              Query(options: QueryOptions(
                document: gql(readCharacters), // graphql query we want to perform
                pollInterval: Duration(seconds: 120), // refetch interval
              ), 
              builder: (QueryResult result, { VoidCallback refetch, FetchMore fetchMore }) {
                if (result.isLoading) {
                  return Text('Loading');
                }
                return Column(
                  children: [
                    for (var item in result.data['listAllCharacters']['data'])
                      CharacterTile(Character: item, refetch: refetch),
                  ],
                );
              })
            ])
          )
        ],
      ),
    );
  }
} 

En primer lugar, definimos la cadena de consulta para obtener todos los caracteres de la base de datos. [line 5 to 17]. Hemos envuelto nuestro widget de lista con un widget de consulta de flutter_graphql.

No dude en echar un vistazo a la documentación oficial de la biblioteca flutter_graphql.

En el argumento de opciones de consulta proporcionamos la propia cadena de consulta GraphQL. Podemos pasar cualquier número flotante para el argumento pollInterval. Poll Interval define la frecuencia con la que nos gustaría recuperar datos de nuestro backend. El widget también tiene una función de construcción estándar. Podemos usar una función de construcción para pasar el resultado de la consulta, recuperar la función de devolución de llamada y obtener más funciones de devolución de llamada en el árbol de widgets.

A continuación, voy a actualizar el CharacterTile widget para mostrar los datos del personaje en la pantalla.

// lib/screens/character-tile.dart
...
class CharacterTile extends StatelessWidget {
  final Character;
  final VoidCallback refetch;
  final VoidCallback updateParent;
  const CharacterTile({
    Key key, 
    @required this.Character, 
    @required this.refetch,
    this.updateParent,
  }) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: () {
      },
      child: Padding(
        padding: const EdgeInsets.all(10),
        child: Row(
          children: [
            Container(
              height: 90,
              width: 90,
              decoration: BoxDecoration(
                color: Colors.amber,
                borderRadius: BorderRadius.circular(15),
                image: DecorationImage(
                  fit: BoxFit.cover,
                  image: NetworkImage(Character['picture'])
                )
              ),
            ),
            SizedBox(width: 10),
            Expanded(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    Character['name'],
                    style: TextStyle(
                      color: Colors.black87,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  SizedBox(height: 5),
                  Text(
                    Character['description'],
                    style: TextStyle(
                      color: Colors.black87,
                    ),
                    maxLines: 2,
                  ),
                ],
              )
            )
          ],
        ),
      ),
    );
  }
}

Agregar nuevos datos

Podemos agregar nuevos caracteres a nuestra base de datos ejecutando la siguiente mutación.

mutation CreateNewCharacter($data: CharacterInput!) {
    createCharacter(data: $data) {
      _id
      name
      description
      picture
    }
}

Para ejecutar esta mutación desde nuestro widget podemos usar el Mutation widget de flutter_graphql Biblioteca. Creemos un nuevo widget con un formulario simple para que los usuarios interactúen e ingresen datos. Una vez enviado el formulario, createCharacter se llamará mutación.

// lib/screens/new.dart
...
String addCharacter = ";";";
  mutation CreateNewCharacter($data: CharacterInput!) {
    createCharacter(data: $data) {
      _id
      name
      description
      picture
    }
  }
";";";;
class NewCharacter extends StatelessWidget {
  const NewCharacter({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Add New Character'),
      ),
      body: AddCharacterForm()
    );
  }
}
class AddCharacterForm extends StatefulWidget {
  AddCharacterForm({Key key}) : super(key: key);
  @override
  _AddCharacterFormState createState() => _AddCharacterFormState();
}
class _AddCharacterFormState extends State<AddCharacterForm> {
  String name;
  String description;
  String imgUrl;
  @override
  Widget build(BuildContext context) {
    return Form(
      child: Padding(
        padding: EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            TextField(
              decoration: const InputDecoration(
                icon: Icon(Icons.person),
                labelText: 'Name *',
              ),
              onChanged: (text) {
                name = text;
              },
            ),
            TextField(
              decoration: const InputDecoration(
                icon: Icon(Icons.post_add),
                labelText: 'Description',
              ),
              minLines: 4,
              maxLines: 4,
              onChanged: (text) {
                description = text;
              },
            ),
            TextField(
              decoration: const InputDecoration(
                icon: Icon(Icons.image),
                labelText: 'Image Url',
              ),
              onChanged: (text) {
                imgUrl = text;
              },
            ),
            SizedBox(height: 20),
            Mutation(
              options: MutationOptions(
                document: gql(addCharacter),
                onCompleted: (dynamic resultData) {
                  print(resultData);
                  name="";
                  description = '';
                  imgUrl="";
                  Navigator.of(context).push(
                    MaterialPageRoute(builder: (context) => AllCharacters())
                  );
                },
              ), 
              builder: (
                RunMutation runMutation,
                QueryResult result,
              ) {
                return Center(
                  child: ElevatedButton(
                    child: const Text('Submit'),
                    onPressed: () {
                      runMutation({
                        'data': {
                          ";picture";: imgUrl,
                          ";name";: name,
                          ";description";: description,
                        }
                      });
                    },
                  ),
                );
              }
            )
          ],
        ),
      ),
    );
  }
}

Como puede ver en el código anterior, el widget de mutación funciona de manera muy similar al widget de consulta. Además, el widget Mutation nos proporciona una función onComplete. Esta función devuelve el resultado actualizado de la base de datos después de que se completa la mutación.

Eliminando datos

Para eliminar un personaje de nuestra base de datos podemos ejecutar el deleteCharacter mutación. Podemos agregar esta función de mutación a nuestro CharacterTile y disparar cuando se presiona un botón.

// lib/screens/character-tile.dart
...

String deleteCharacter = ";";";
  mutation DeleteCharacter($id: ID!) {
    deleteCharacter(id: $id) {
      _id
      name
    }
  }
";";";;

class CharacterTile extends StatelessWidget {
  final Character;
  final VoidCallback refetch;
  final VoidCallback updateParent;
  const CharacterTile({
    Key key, 
    @required this.Character, 
    @required this.refetch,
    this.updateParent,
  }) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: () {
        showModalBottomSheet(
          context: context,
          builder: (BuildContext context) {
            print(Character['picture']);
            return Mutation(
              options: MutationOptions(
                document: gql(deleteCharacter),
                onCompleted: (dynamic resultData) {
                  print(resultData);
                  this.refetch();
                },
              ), 
              builder: (
                RunMutation runMutation,
                QueryResult result,
              ) {
                return Container(
                  height: 400,
                  padding: EdgeInsets.all(30),
                  child: Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      mainAxisSize: MainAxisSize.min,
                      children: <Widget>[
                        Text(Character['description']),
                        ElevatedButton(
                          child: Text('Delete Character'),
                          onPressed: () {
                            runMutation({
                              'id': Character['_id'],
                            });
                            Navigator.pop(context);
                          },
                        ),
                      ],
                    ),
                  ),
                ); 
              }
            );
          }
        );
      },
      child: Padding(
        padding: const EdgeInsets.all(10),
        child: Row(
          children: [
            Container(
              height: 90,
              width: 90,
              decoration: BoxDecoration(
                color: Colors.amber,
                borderRadius: BorderRadius.circular(15),
                image: DecorationImage(
                  fit: BoxFit.cover,
                  image: NetworkImage(Character['picture'])
                )
              ),
            ),
            SizedBox(width: 10),
            Expanded(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    Character['name'],
                    style: TextStyle(
                      color: Colors.black87,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  SizedBox(height: 5),
                  Text(
                    Character['description'],
                    style: TextStyle(
                      color: Colors.black87,
                    ),
                    maxLines: 2,
                  ),
                ],
              )
            )
          ],
        ),
      ),
    );
  }
}

Editando datos

La edición de datos funciona igual que agregar y eliminar. Es solo otra mutación en la API GraphQL. Podemos crear un widget de forma de carácter de edición similar al nuevo widget de forma de personaje. La única diferencia es que el formulario de edición se ejecutará updateCharacter mutación. Para editar creé un nuevo widget lib/screens/edit.dart. Aquí está el código de este widget.

// lib/screens/edit.dart

String editCharacter = """
mutation EditCharacter($name: String!, $id: ID!, $description: String!, $picture: String!) {
  updateCharacter(data: 
  { 
    name: $name 
    description: $description
    picture: $picture
  }, id: $id) {
    _id
    name
    description
    picture
  }
}
""";
class EditCharacter extends StatelessWidget {
  final Character;
  const EditCharacter({Key key, this.Character}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Edit Character'),
      ),
      body: EditFormBody(Character: this.Character),
    );
  }
}
class EditFormBody extends StatefulWidget {
  final Character;
  EditFormBody({Key key, this.Character}) : super(key: key);
  @override
  _EditFormBodyState createState() => _EditFormBodyState();
}
class _EditFormBodyState extends State<EditFormBody> {
  String name;
  String description;
  String picture;
  @override
  Widget build(BuildContext context) {
    return Container(
       child: Padding(
         padding: const EdgeInsets.all(8.0),
         child: Column(
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
            TextFormField(
               initialValue: widget.Character['name'],
                decoration: const InputDecoration(
                  icon: Icon(Icons.person),
                  labelText: 'Name *',
                ),
                onChanged: (text) {
                  name = text;
                }
            ),
            TextFormField(
              initialValue: widget.Character['description'],
              decoration: const InputDecoration(
                icon: Icon(Icons.person),
                labelText: 'Description',
              ),
              minLines: 4,
              maxLines: 4,
              onChanged: (text) {
                description = text;
              }
            ),
            TextFormField(
              initialValue: widget.Character['picture'],
              decoration: const InputDecoration(
                icon: Icon(Icons.image),
                labelText: 'Image Url',
              ),
              onChanged: (text) {
                picture = text;
              },
            ),
            SizedBox(height: 20),
            Mutation(
              options: MutationOptions(
                document: gql(editCharacter),
                onCompleted: (dynamic resultData) {
                  print(resultData);
                  Navigator.of(context).push(
                    MaterialPageRoute(builder: (context) => AllCharacters())
                  );
                },
              ),
              builder: (
                RunMutation runMutation,
                QueryResult result,
              ) {
                print(result);
                return Center(
                  child: ElevatedButton(
                    child: const Text('Submit'),
                    onPressed: () {

                      runMutation({
                        'id': widget.Character['_id'],
                        'name': name != null ? name : widget.Character['name'],
                        'description': description != null ? description : widget.Character['description'],
                        'picture': picture != null ? picture : widget.Character['picture'],
                      });
                    },
                  ),
                );
              }
            ),
           ]
         )
       ),
    );
  }
}

Puede ver el código completo de este artículo a continuación.

¿Tienes preguntas sobre Fauna o Flutter? Puede comunicarse conmigo en Twitter @HaqueShadid

GitHub

A dónde ir desde aquí

La intención principal de este artículo es ponerte en marcha con Flutter and Fauna. Aquí solo hemos arañado la superficie. El ecosistema de fauna proporciona un backend completo, de escalabilidad automática y fácil de desarrollar como servicio para sus aplicaciones móviles. Si su objetivo es enviar una aplicación móvil multiplataforma lista para producción en un tiempo récord, brinde Fauna y Flutter atrás.

Recomiendo encarecidamente consultar el sitio de documentación oficial de Fauna. Si está interesado en obtener más información sobre los clientes GraphQL para Dart / Flutter, consulte el repositorio oficial de GitHub para graphql_flutter.

Feliz piratería y hasta la próxima.

(Visited 10 times, 1 visits today)