首页 > web前端 > css教程 > 如何使用Flutter,Fauna和GraphQL构建全堆栈移动应用程序

如何使用Flutter,Fauna和GraphQL构建全堆栈移动应用程序

Lisa Kudrow
发布: 2025-03-21 10:34:13
原创
317 人浏览过

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

Flutter是Google的UI框架,用于创建灵活、富有表现力的跨平台移动应用程序。它是增长最快的移动应用程序开发框架之一。另一方面,Fauna是一个事务性的、对开发者友好的无服务器数据库,支持原生GraphQL。Flutter Fauna是天作之合。如果您希望在创纪录的时间内构建和发布功能丰富的全栈应用程序,Flutter和Fauna是正确的工具。在本文中,我们将引导您使用Fauna和GraphQL后端构建您的第一个Flutter应用程序。

您可以在GitHub上找到本文的完整代码。

学习目标

在阅读完本文后,您应该知道如何:

  1. 设置Fauna实例,
  2. 为Fauna编写GraphQL模式,
  3. 在Flutter应用程序中设置GraphQL客户端,以及
  4. 对Fauna GraphQL后端执行查询和变异。

FaunaAWS AmplifyFirebase: Fauna解决了哪些问题?它与其他无服务器解决方案有何不同?如果您不熟悉Fauna,并且想了解更多关于Fauna与其他解决方案的比较信息,我建议您阅读这篇文章。

我们正在构建什么?

我们将构建一个简单的移动应用程序,允许用户添加、删除和更新他们最喜欢的电影和电视剧角色。

设置Fauna

前往fauna.com并创建一个新帐户。登录后,您应该能够创建一个新的数据库。

为您的数据库命名。我将我的命名为flutter_demo。接下来,我们可以选择一个区域组。对于此演示,我们将选择classic。Fauna是一个全球分布式的无服务器数据库。它是唯一一个支持从任何地方进行低延迟读写访问的数据库。可以把它想象成CDN(内容分发网络),但它是针对您的数据库的。要了解有关区域组的更多信息,请遵循本指南。

生成管理员密钥

数据库创建完成后,转到“安全”选项卡。单击“新建密钥”按钮并为您的数据库创建一个新密钥。请妥善保管此密钥,因为我们需要它来进行GraphQL操作。

我们将为我们的数据库创建一个管理员密钥。具有管理员角色的密钥用于管理其关联的数据库,包括数据库访问提供程序、子数据库、文档、函数、索引、密钥、令牌和用户定义的角色。您可以在以下链接中了解有关Fauna各种安全密钥和访问角色的更多信息。

编写GraphQL模式

我们将构建一个简单的应用程序,允许用户添加、更新和删除他们最喜欢的电视角色。

创建一个新的Flutter项目

让我们通过运行以下命令创建一个新的Flutter项目。

<code>flutter create my_app</code>
登录后复制

在项目目录中,我们将创建一个名为graphql/schema.graphql的新文件。

在模式文件中,我们将定义集合的结构。Fauna中的集合类似于SQL中的表。我们现在只需要一个集合。我们将它命名为Character。

<code>### schema.graphql
type Character {
    name: String!
    description: String!
    picture: String
}
type Query {
    listAllCharacters: [Character]
}</code>
登录后复制

如上所示,我们定义了一个名为Character的类型,它具有多个属性(即名称、描述、图片等)。可以将属性视为SQL数据库的列或NoSQL数据库的键值对。我们还定义了一个Query。此查询将返回角色列表。

现在让我们回到Fauna仪表板。单击GraphQL,然后单击导入模式以将我们的模式上传到Fauna。

导入完成后,我们将看到Fauna生成了GraphQL查询和变异。

不喜欢自动生成的GraphQL?想要更好地控制您的业务逻辑?在这种情况下,Fauna允许您定义自定义GraphQL解析器。要了解更多信息,请点击此链接。

在Flutter应用程序中设置GraphQL客户端

让我们打开pubspec.yaml文件并添加所需的依赖项。

<code>...
dependencies:
  graphql_flutter: ^4.0.0-beta
  hive: ^1.3.0
  flutter:
    sdk: flutter
...</code>
登录后复制

我们在这里添加了两个依赖项。graphql_flutter是Flutter的GraphQL客户端库。它将GraphQL客户端的所有现代功能集成到一个易于使用的包中。我们还添加了hive包作为我们的依赖项。Hive是用纯Dart编写的轻量级键值数据库,用于本地存储。我们使用hive来缓存我们的GraphQL查询。

接下来,我们将创建一个新的文件lib/client_provider.dart。我们将在该文件中创建一个提供程序类,其中将包含我们的Fauna配置。

要连接到Fauna的GraphQL API,我们首先需要创建一个GraphQLClient。GraphQLClient需要一个缓存和一个链接才能初始化。让我们看一下下面的代码。

<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>
登录后复制

在上面的代码中,我们创建了一个ValueNotifier来包装GraphQLClient。请注意,我们在第13-15行(突出显示)配置了AuthLink。在第14行,我们添加了来自Fauna的管理员密钥作为令牌的一部分。在这里,我硬编码了管理员密钥。但是,在生产应用程序中,我们必须避免硬编码Fauna的任何安全密钥。

有几种方法可以在Flutter应用程序中存储密钥。请查看此博文以供参考。

我们希望能够从应用程序的任何小部件调用Query和Mutation。为此,我们需要使用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>
登录后复制

接下来,我们转到main.dart文件,并使用ClientProvider小部件包装我们的主小部件。让我们看一下下面的代码。

<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>
登录后复制

此时,我们所有的下游小部件都将能够运行Queries和Mutations函数,并可以与GraphQL API交互。

应用程序页面

演示应用程序应该简单易懂。让我们继续创建一个简单的列表小部件,它将显示所有角色的列表。让我们创建一个新的文件lib/screens/character-list.dart。在这个文件中,我们将编写一个名为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>
登录后复制

如上面的代码所示,[第37行]我们有一个for循环,用一些假数据填充列表。最终,我们将对Fauna后端进行GraphQL查询,并从数据库中获取所有角色。在我们这样做之前,让我们尝试运行我们的应用程序。我们可以使用以下命令运行我们的应用程序

<code>flutter run</code>
登录后复制

此时,我们应该能够看到以下屏幕。

执行查询和变异

现在我们有一些基本的小部件,我们可以继续连接GraphQL查询。我们希望从数据库获取所有角色,而不是硬编码字符串,并在AllCharacters小部件中查看它们。

让我们回到Fauna的GraphQL playground。请注意,我们可以运行以下查询来列出所有角色。

<code>query ListAllCharacters {
  listAllCharacters(_size: 100) {
    data {
      _id
      name
      description
      picture
    }
    after
  }
}</code>
登录后复制

要从我们的Widget执行此查询,我们需要对其进行一些更改。

<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>
登录后复制

首先,我们定义了从数据库获取所有角色的查询字符串[第5到17行]。我们使用flutter_graphql中的Query小部件包装了列表小部件。

随意查看flutter_graphql库的官方文档。

在query options参数中,我们提供了GraphQL查询字符串本身。我们可以为pollInterval参数传递任何浮点数。Poll Interval定义了我们希望多长时间从后端重新获取一次数据。该小部件还有一个标准的builder函数。我们可以使用builder函数将查询结果、重新获取回调函数和获取更多回调函数传递到Widget树中。

接下来,我将更新CharacterTile小部件以在屏幕上显示角色数据。

<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>
登录后复制

添加新数据

我们可以通过运行以下变异来向我们的数据库添加新角色。

<code>mutation CreateNewCharacter($data: CharacterInput!) {
    createCharacter(data: $data) {
      _id
      name
      description
      picture
    }
}</code>
登录后复制

要从我们的Widget运行此变异,我们需要使用flutter_graphql库中的Mutation小部件。让我们创建一个带有简单表单的新小部件,供用户交互和输入数据。提交表单后,将调用createCharacter变异。

<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>
登录后复制

从上面的代码可以看出,Mutation小部件的工作方式与Query小部件非常相似。此外,Mutation小部件为我们提供了一个onComplete函数。此函数在变异完成后返回数据库中的更新结果。

删除数据

我们可以通过运行deleteCharacter变异来从数据库中删除角色。我们可以将此变异函数添加到我们的CharacterTile中,并在按下按钮时触发它。

<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>
登录后复制

编辑数据

编辑数据与添加和删除相同。它只是GraphQL API中的另一个变异。我们可以创建一个类似于新角色表单小部件的编辑角色表单小部件。唯一的区别是编辑表单将运行updateCharacter变异。为了编辑,我创建了一个新的Widget lib/screens/edit.dart。以下是此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>
登录后复制

您可以查看本文的完整代码如下。

对Fauna或Flutter有疑问?您可以在Twitter @HaqueShadid上联系我

GitHub ### 后续步骤

本文的主要目的是让您开始使用Flutter和Fauna。我们在这里只触及了表面。Fauna生态系统为您的移动应用程序提供了一个完整的、自动缩放的、对开发者友好的后端即服务。如果您的目标是在创纪录的时间内发布一个可用于生产的跨平台移动应用程序,请尝试使用Fauna和Flutter

我强烈建议您查看Fauna的官方文档网站。如果您有兴趣了解更多关于Dart/Flutter的GraphQL客户端的信息,请查看graphql_flutter的官方GitHub存储库。

祝您编程愉快,下次再见。

以上是如何使用Flutter,Fauna和GraphQL构建全堆栈移动应用程序的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板