Home > Web Front-end > CSS Tutorial > How to Build a Full-Stack Mobile Application With Flutter, Fauna, and GraphQL

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

Lisa Kudrow
Release: 2025-03-21 10:34:13
Original
313 people have browsed it

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

Flutter is Google's UI framework for creating flexible, expressive cross-platform mobile applications. It is one of the fastest growing frameworks for mobile application development. Fauna, on the other hand, is a transactional, developer-friendly serverless database that supports native GraphQL. Flutter Fauna is a perfect match made by heaven. If you want to build and release feature-rich full-stack applications in record time, Flutter and Fauna are the right tools. In this article, we will walk you through the construction of your first Flutter application using Fauna and GraphQL backends.

You can find the full code for this article on GitHub.

Learning Objectives

After reading this article, you should know how to:

  1. Set up the Fauna instance,
  2. Write GraphQL pattern for Fauna,
  3. Set up the GraphQL client in the Flutter application, and
  4. Perform queries and mutations on the Fauna GraphQL backend.

Fauna vs. AWS Amplify vs. Firebase : What problems does Fauna solve? How is it different from other serverless solutions? If you are not familiar with Fauna and want to learn more about Fauna's comparison with other solutions, I suggest you read this article.

What are we building?

We will build a simple mobile app that allows users to add, delete and update their favorite movie and TV series characters.

Set up Fauna

Go to fauna.com and create a new account. After logging in, you should be able to create a new database.

Name your database. I named mine flutter_demo. Next, we can select a region group. For this demo, we will select classic. Fauna is a globally distributed serverless database. It is the only database that supports low-latency read and write access from anywhere. Think of it as a CDN (Content Distribution Network), but it is for your database. To learn more about the Region Group, follow this guide.

Generate administrator key

Once the database is created, go to the Security tab. Click the New Key button and create a new key for your database. Please keep this key properly because we need it for GraphQL operations.

We will create an administrator key for our database. A key with an administrator role is used to manage its associated databases, including database access providers, subdatabases, documents, functions, indexes, keys, tokens, and user-defined roles. You can learn more about Fauna's various security keys and access roles in the links below.

Writing GraphQL pattern

We will build a simple app that allows users to add, update and delete their favorite TV characters.

Create a new Flutter project

Let's create a new Flutter project by running the following command.

 <code>flutter create my_app</code>
Copy after login

In the project directory, we will create a new file named graphql/schema.graphql.

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

 <code>### schema.graphql type Character { name: String! description: String! picture: String } type Query { listAllCharacters: [Character] }</code>
Copy after login

As shown above, we define a type called Character that has multiple properties (i.e., name, description, picture, etc.). A property can be treated as a column of a SQL database or a key-value pair of a NoSQL database. We also define a Query. This query returns the role list.

Now let's go back to the Fauna dashboard. Click GraphQL and click Import Mode to upload our mode to Fauna.

After the import is complete, we will see Fauna generates GraphQL queries and mutations.

Don't like automatic generation of GraphQL? Want to have better control over your business logic? In this case, Fauna allows you to define a custom GraphQL parser. To learn more, click this link.

Setting up GraphQL client in Flutter application

Let's open the pubspec.yaml file and add the required dependencies.

 <code>... dependencies: graphql_flutter: ^4.0.0-beta hive: ^1.3.0 flutter: sdk: flutter ...</code>
Copy after login

We have added two dependencies here. graphql_flutter is Flutter's GraphQL client library. It integrates all the modern features of the GraphQL client 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 use hive to cache our GraphQL queries.

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

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

 <code>// 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, ), ); }</graphqlclient></graphqlclient></code>
Copy after login

In the above code, we create a ValueNotifier to wrap the GraphQLClient. Note that we configured AuthLink on lines 13-15 (highlighted). On line 14, we added the administrator key from Fauna as part of the token. Here, I hardcoded the admin key. However, in production applications, we must avoid hard-coded any security keys of Fauna.

There are several ways to store keys in a Flutter application. Please check this blog for reference.

We want to be able to call Query and Mutation from any widget in the application. To do this, we need to wrap our Widget using the GraphQLProvider widget.

 <code>// lib/client_provider.dart .... /// 使用`graphql_flutter`客户端包装根应用程序。 /// 我们使用缓存进行所有状态管理。 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, ); } }</graphqlclient></code>
Copy after login

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

 <code>// 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(), } ), ); } }</code>
Copy after login

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

Application Page

The demo application should be simple and easy to understand. Let's go ahead and create a simple list widget that will display a list of all roles. Let's create a new file lib/screens/character-list.dart. In this file, we will write a new widget called AllCharacters.

 <code>// 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 _CharacterTileeState(); } class _CharacterTileState extends State<charactertile> { @override Widget build(BuildContext context) { return Container( child: Text("Character Tile"), ); } }</charactertile></widget></code>
Copy after login

As shown in the above code, [line 37] we have a for loop that fills the list with some fake data. Finally, we will do GraphQL queries on the Fauna backend and get all the roles from the database. Before we do this, let's try to run our application. We can run our application using the following command

 <code>flutter run</code>
Copy after login

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

Execute queries and mutations

Now we have some basic widgets that we can continue to connect to GraphQL queries. We want to get all the roles from the database instead of hardcoded strings and view them in the AllCharacters widget.

Let's go back to Fauna's GraphQL playground. Note that we can run the following query to list all roles.

 <code>query ListAllCharacters { listAllCharacters(_size: 100) { data { _id name description picture } after } }</code>
Copy after login

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

 <code>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查询pollInterval: Duration(seconds: 120), // 重新获取间隔), 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), ], ); }) ]) ) ], ), ); } }</widget></code>
Copy after login

First, we define the query string to get all roles from the database [lines 5 to 17]. We wrap the list widget using the Query widget in flutter_graphql.

Feel free to view the official documentation of the flutter_graphql library.

In the query options parameter, we provide the GraphQL query string itself. We can pass any floating point number for the pollInterval parameter. Poll Interval defines how long we want to refetch data from the backend. The widget also has a standard builder function. We can use the builder function to pass query results, re-get callback functions, and get more callback functions into the Widget tree.

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

 <code>// 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, ), ], ) ) ], ), ), ); } }</code>
Copy after login

Add new data

We can add new roles to our database by running the following mutation.

 <code>mutation CreateNewCharacter($data: CharacterInput!) { createCharacter(data: $data) { _id name description picture } }</code>
Copy after login

To run this mutation from our Widget, we need to use the Mutation widget from the flutter_graphql library. Let's create a new widget with a simple form for users to interact and enter data. After submitting the form, createCharacter mutation will be called.

 <code>// 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, } }); }, ), ); } ) ], ), ), ); } }</addcharacterform></code>
Copy after login

As can be seen from the above code, the Mutation widget works very similarly to the Query widget. In addition, the Mutation widget provides us with an onComplete function. This function returns the update result in the database after the mutation is completed.

Delete data

We can delete roles from the database by running the deleteCharacter mutation. We can add this mutant to our CharacterTile and trigger it when the button is pressed.

 <code>// 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, ), ], ) ) ], ), ), ); } }</widget></code>
Copy after login

Edit data

Editing data is the same as adding and deleting. It's just another mutation in the GraphQL API. We can create an edit role form widget similar to the new role form widget. The only difference is that editing the form will run the updateCharacter mutation. For editing, I created a new Widget lib/screens/edit.dart. Here is the code for this Widget.

 <code>// 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'], }); }, ), ); } ), ] ) ), ); } }</editformbody></code>
Copy after login

You can view the full code of this article as follows.

Have questions about Fauna or Flutter? You can contact me on Twitter @HaqueShadid

GitHub ### Next Steps

The main purpose of this article is to get you started with Flutter and Fauna. We only touch the surface here. The Fauna ecosystem provides your mobile application with a complete, auto-scaling, developer-friendly backend as a service. If your goal is to release a cross-platform mobile app that can be used for production in record time, try Fauna and Flutter .

I highly recommend you check out Fauna's official documentation website. If you are interested in learning more about Dart/Flutter's GraphQL client, check out the official GitHub repository of graphql_flutter.

I wish you a happy programming, see you next time.

The above is the detailed content of How to Build a Full-Stack Mobile Application With Flutter, Fauna, and GraphQL. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template