如何利用Flutter、Fauna和GraphQL构建全栈式移动应用程序

169 阅读12分钟

(这是一篇赞助文章)。)

Flutter是谷歌的UI框架,用于创建灵活、富有表现力的跨平台移动应用程序。它是增长最快的移动应用开发框架之一。另一方面,Fauna是一个交易性的、对开发者友好的无服务器数据库,支持本地GraphQL。Flutter+Fauna是天作之合。如果你想在创纪录的时间内构建并交付一个功能丰富的全栈应用程序,那么Flutter和Fauna就是最合适的工具。在这篇文章中,我们将指导您使用Fauna和GraphQL后端构建您的第一个Flutter应用程序。

你可以在GitHub上找到这篇文章的完整代码。

学习目标

在本文结束时,您应该知道如何。

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

Faunavs.AWS Amplifyvs.Firebase:Fauna能解决什么问题?它与其他无服务器解决方案有什么不同?如果你是Fauna的新手,想了解更多关于Fauna与其他解决方案的比较,我推荐你阅读这篇文章。

我们正在构建什么?

我们将构建一个简单的移动应用,允许用户添加、删除和更新他们喜欢的电影和节目中的人物。

设置Fauna

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

给你的数据库取个名字。我打算将我的数据库命名为flutter_demo 。接下来,我们可以选择一个区域组。对于这个演示,我们将选择经典。Fauna是一个全球分布的无服务器数据库。它是唯一支持从任何地方进行低延迟读写访问的数据库。可以把它看作是CDN(内容交付网络),但为你的数据库。要了解更多关于区域组的信息,请遵循本指南

生成一个管理密钥

一旦数据库创建完毕,请到安全标签。点击新钥匙按钮,为你的数据库创建一个新钥匙。保持这个密钥的安全性,因为我们需要它来进行 GraphQL 操作。

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

组成一个GraphQL模式

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

创建一个新的Flutter项目

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

flutter create my_app

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

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

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

正如你在上面看到的,我们定义了一个叫做Character 的类型,有几个属性(即:name,description,picture, 等等)。把属性想象成SQL数据库的列或NoSQL数据库的键值支付。我们还定义了一个查询。这个查询将返回一个字符的列表。

现在让我们回到Fauna仪表板。点击GraphQL并点击导入模式,将我们的模式上传到Fauna。

一旦导入完成,我们会看到Fauna已经生成了GraphQL查询和突变。

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

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

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

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

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

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

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

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

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

有几种方法可以在Flutter应用程序中存储秘密。请看一下这篇博文,作为参考。

我们希望能够从我们应用程序的任何小部件中调用QueryMutation 。要做到这一点,我们需要用GraphQLProvider 小组件来包装我们的小组件。

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

接下来,我们去我们的main.dart 文件,用ClientProvider 小组件包裹我们的主小组件。让我们看一下下面的代码。

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

在这一点上,我们所有的下游widget都可以访问运行QueriesMutations 函数,并可以与GraphQL API进行交互。

应用程序页面

演示应用程序应该是简单和容易操作的。让我们继续创建一个简单的列表小部件,它将显示所有字符的列表。让我们创建一个新的文件lib/screens/character-list.dart 。在这个文件中,我们将编写一个名为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;),
    );
  }
}

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

flutter run

在这一点上,我们应该能够看到以下屏幕。

执行查询和变异

现在我们有了一些基本的小工具,我们可以继续前进,挂上GraphQL查询。我们想从我们的数据库中获取所有的字符,并在AllCharacters widget中查看,而不是硬编码的字符串。

让我们回到Fauna的GraphQL操场。注意,我们可以运行下面的查询来列出所有的字符。

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

为了从我们的widget中执行这个查询,我们需要对它做一些修改。

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

首先,我们定义了从数据库中获取所有字符的查询字符串[第5至17行]。我们用一个来自flutter_graphql 的查询小部件来包装我们的列表小部件。

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

在查询选项参数中,我们提供GraphQL查询字符串本身。我们可以为pollInterval参数传入任何浮点数。轮询间隔定义了我们希望从后端重新获取数据的频率。该小组件也有一个标准的构建器函数。我们可以使用构建器函数来传递查询结果,重新获取回调函数和获取更多的回调函数,沿着widget树向下。

接下来,我将更新CharacterTile widget,在屏幕上显示字符数据。

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

添加新的数据

我们可以通过运行下面的突变来向我们的数据库添加新的字符。

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

为了从我们的小组件运行这个突变,我们可以使用flutter_graphql 库中的Mutation 小组件。让我们创建一个新的小组件,有一个简单的表单,供用户互动和输入数据。一旦表单被提交,createCharacter 突变将被调用。

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

正如你从上面的代码中可以看到,突变小组件的工作方式与查询小组件非常相似。此外,突变小组件为我们提供了一个onComplete函数。这个函数在突变完成后从数据库返回更新的结果。

删除数据

要从我们的数据库中删除一个字符,我们可以运行deleteCharacter mutation。我们可以将这个突变函数添加到我们的CharacterTile ,并在按下一个按钮时启动它。

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

编辑数据

编辑数据的工作原理与添加和删除相同。它只是GraphQL API中的另一个突变。我们可以创建一个编辑字符表单小组件,类似于新的字符表单小组件。唯一的区别是,编辑表格将运行updateCharacter 突变。为了编辑,我创建了一个新的小组件lib/screens/edit.dart 。下面是这个小组件的代码。

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

你可以看看下面这篇文章的完整代码

GitHub

今后的发展方向

这篇文章的主要目的是让你开始使用Flutter和Fauna。我们在这里只触及到了表面。Fauna 生态系统为您的移动应用程序提供了一个完整的、自动扩展的、对开发者友好的后台服务。如果你的目标是在创纪录的时间内交付一个生产就绪的跨平台移动应用,那么Fauna和Flutter就是你的选择。

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

祝你黑客攻击愉快,下次再见。


The postHow to Build a Full-Stack Mobile Application With Flutter, Fauna, and GraphQLappeared first onCSS-Tricks.你可以通过成为MVP支持者来支持CSS-Tricks。