How to Build a Full-Stack Mobile Application With Flutter, Fauna, and GraphQL

Publikováno: 5.8.2021

(This is a sponsored post.)

Flutter is Google’s UI framework used to create flexible, expressive cross-platform mobile applications. It is one of the fastest-growing frameworks for mobile app development. On the other hand, Fauna is a transactional, developer-friendly serverless …


The post How to Build a Full-Stack Mobile Application With Flutter, Fauna, and GraphQL appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

Celý článek

(This is a sponsored post.)

Flutter is Google’s UI framework used to create flexible, expressive cross-platform mobile applications. It is one of the fastest-growing frameworks for mobile app development. On the other hand, Fauna is a transactional, developer-friendly serverless database that supports native GraphQL. Flutter + Fauna is a match made in Heaven. If you are looking to build and ship a feature-rich full-stack application within record time, Flutter and Fauna is the right tool for the job. In this article, we will walk you through building your very first Flutter application with Fauna and GraphQL back-end.

You can find the complete code for this article, on GitHub.

Learning objective

By the end of this article, you should know how to:

  1. set up a Fauna instance,
  2. compose GraphQL schema for Fauna,
  3. set up GraphQL client in a Flutter app, and
  4. perform queries and mutations against Fauna GraphQL back-end.

Fauna vs. AWS Amplify vs. Firebase: What problems does Fauna solve? How is it different from other serverless solutions? If you are new to Fauna and would like to learn more about how Fauna compares to other solutions, I recommend reading this article.

What are we building?

We will be building a simple mobile application that will allow users to add, delete and update their favorite characters from movies and shows.

Setting up Fauna

Head over to fauna.com and create a new account. Once logged in, you should be able to create a new database.

Give a name to your database. I am going to name mine flutter_demo. Next, we can select a region group. For this demo, we will choose classic. Fauna is a globally distributed serverless database. It is the only database that supports low latency read and writes access from anywhere. Think of it as CDN (Content Delivery Network) but for your database. To learn more about region groups, follow this guide.

Generating an admin key

Once the database is created head, over to the security tab. Click on the new key button and create a new key for your database. Keep this key secure as we need this for our GraphQL operations.

We will be creating an admin key for our database. Keys with an admin role are used for managing their associated database, including the database access providers, child databases, documents, functions, indexes, keys, tokens, and user-defined roles. You can learn more about Fauna’s various security keys and access roles in the following link.

Compose a GraphQL schema

We will be building a simple app that will allow the users to add, update, and delete their favorite TV characters.

Creating a new Flutter project

Let’s create a new flutter project by running the following commands.

flutter create my_app

Inside the project directory, we will create a new file called graphql/schema.graphql.

In the schema file, we will define the structure of our collection. Collections in Fauna are similar to tables in SQL. We only need one collection for now. We will call it Character.

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

As you can see above, we defined a type called Character with several properties (i.e., name, description, picture, etc.). Think of properties as columns of SQL database or key-value paid of an NoSQL database. We have also defined a Query. This query will return a list of the characters.

Now let’s go back to Fauna dashboard. Click on GraphQL and click on import schema to upload our schema to Fauna.

Once the importing is done, we will see that Fauna has generated the GraphQL queries and mutations.

Don’t like auto-generated GraphQL? Want more control over your business logic? In that case, Fauna allows you to define your custom GraphQL resolvers. To learn more, follow this link.

Setup GraphQL client in Flutter app

Let’s open up our pubspec.yaml file and add the required dependencies.

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

We added two dependencies here. graphql_flutter is a GraphQL client library for flutter. It brings all the modern features of GraphQL clients into one easy-to-use package. We also added the hive package as our dependency. Hive is a lightweight key-value database written in pure Dart for local storage. We are using hive to cache our GraphQL queries.

Next, we will create a new file lib/client_provider.dart. We will create a provider class in this file that will contain our Fauna configuration.

To connect to Fauna’s GraphQL API, we first need to create a GraphQLClient. A GraphQLClient requires a cache and a link to be initialized. Let’s take a look at the code below.

// 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,
    ),
  );
} 

In the code above, we created a ValueNotifier to wrap the GraphQLClient. Notice that we configured the AuthLink in lines 13 – 15 (highlighted). On line 14, we have added the admin key from Fauna as a part of the token. Here I have hardcoded the admin key. However, in a production application, we must avoid hard-coding any security keys from Fauna.

There are several ways to store secrets in Flutter application. Please take a look at this blog post for reference.

We want to be able to call Query and Mutation from any widget of our application. To do so we need to wrap our widgets with 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,
    );
  }
}

Next, we go to our main.dart file and wrap our main widget with the ClientProvider widget. Let’s take a look at the code below.

// 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: '/',
        routes: {
          '/': (_) => AllCharacters(),
          '/new': (_) => NewCharacter(),
        }
      ),
    );
  }
}

At this point, all our downstream widgets will have access to run Queries and Mutations functions and can interact with the GraphQL API.

Application pages

Demo applications should be simple and easy to follow. Let’s go ahead and create a simple list widget that will show the list of all characters. Let’s create a new file lib/screens/character-list.dart. In this file, we will write a new widget called 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;),
    );
  }
}

As you can see in the code above, [line 37] we have a for loop to populate the list with some fake data. Eventually, we will be making a GraphQL query to our Fauna backend and fetch all the characters from the database. Before we do that, let’s try to run our application as it is. We can run our application with the following command

flutter run

At this point we should be able to see the following screen.

Performing queries and mutations

Now that we have some basic widgets, we can go ahead and hook up GraphQL queries. Instead of hardcoded strings, we would like to get all the characters from our database and view them in AllCharacters widget.

Let’s go back to the Fauna’s GraphQL playground. Notice we can run the following query to list all the characters.

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

To perform this query from our widget we will need to make some changes to it.

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),
                  ],
                );
              })
            ])
          )
        ],
      ),
    );
  }
} 

First of all, we defined the query string for getting all characters from the database [line 5 to 17]. We have wrapped our list widget with a Query widget from flutter_graphql.

Feel free to take a look at the official documentation for flutter_graphql library.

In the query options argument we provide the GraphQL query string itself. We can pass in any float number for the pollInterval argument. Poll Interval defines how often we would like to refetch data from our backend. The widget also has a standard builder function. We can use a builder function to pass the query result, refetch callback function and fetch more callback function down the widget tree.

Next, I am going to update the CharacterTile widget to display the character data on screen.

// 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,
                  ),
                ],
              )
            )
          ],
        ),
      ),
    );
  }
}

Adding new data

We can add new characters to our database by running the mutation below.

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

To run this mutation from our widget we can use the Mutation widget from flutter_graphql library. Let’s create a new widget with a simple form for the users to interact with and input data. Once the form is submitted the createCharacter mutation will be called.

// 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,
                        }
                      });
                    },
                  ),
                );
              }
            )
          ],
        ),
      ),
    );
  }
}

As you can see from the code above Mutation widget works very similar to the Query widget. Additionally, the Mutation widget provides us with a onComplete function. This function returns the updated result from the database after the mutation is completed.

Removing data

To remove a character from our database we can run the deleteCharacter mutation. We can add this mutation function to our CharacterTile and fire it when a button is pressed.

// 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,
                  ),
                ],
              )
            )
          ],
        ),
      ),
    );
  }
}

Editing data

Editing data works same as add and delete. It is just another mutation in the GraphQL API. We can create an edit character form widget similar to the new character form widget. The only difference is that the edit form will run updateCharacter mutation. For editing I created a new widget lib/screens/edit.dart. Here’s the code for this 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'],
                      });
                    },
                  ),
                );
              }
            ),
           ]
         )
       ),
    );
  }
}

You can take a look at the complete code for this article below.

Where to go from here

The main intention of this article is to get you up and running with Flutter and Fauna. We have only scratched the surface here. Fauna ecosystem provides a complete, auto-scaling, developer-friendly backend as a service for your mobile applications. If your goal is to ship a production-ready cross-platform mobile application in record time give Fauna and Flutter is the way to go.

I highly recommend checking out Fauna’s official documentation site. If you are interested in learning more about GraphQL clients for Dart/Flutter checkout the official GitHub repo for graphql_flutter.

Happy hacking and see you next time.


The post How to Build a Full-Stack Mobile Application With Flutter, Fauna, and GraphQL appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

Nahoru
Tento web používá k poskytování služeb a analýze návštěvnosti soubory cookie. Používáním tohoto webu s tímto souhlasíte. Další informace