我打算使用Hasura来实现GraphQL服务器并制作一个Flutter的TODO应用

个人对于最近的GraphQL很感兴趣,所以想尝试将其与Flutter结合起来做些事情。

我发现了这篇文章,所以在稍微修改的基础上创建了一个新的版本。
我非常参考了这篇文章,非常感谢!《Flutter + GraphQL with Hasura – The GeekyAnts Blog》。

以下是一个ToDo应用程序的代码库链接:https://github.com/shinbey221/ToDo-App-with-Flutter

Hasura和Prisma

两者在实现GraphQL时经常采用的是GraphQL Engine,但其架构是不同的。

Prisma是一个专为GraphQL(或REST)服务器设计的GraphQL ORM,无法直接从客户端调用。

由于Hasura可以直接从客户端调用,因此实现起来非常方便。但是它只适用于PostgreSQL的GraphQL服务器。适合那些想要快速实施的人。

Hasura实现了GraphQL服务器。

在Heroku上部署

首先打开以下URL:Instant realtime GraphQL on Postgres | Hasura GraphQL Engine。

选择Heroku免费套餐。

スクリーンショット 2019-07-03 15.43.02.png

原文:创建Postgres表格并输入数据。

翻译:创建Postgres表格并填入数据。

スクリーンショット 2019-07-05 13.49.22.png

当您创建数据表后,可以返回到GraphQL的头部菜单,然后在左侧菜单中选择您创建的数据表的查询、变更等操作。

スクリーンショット 2019-07-03 15.52.01.png

创建客户端的Flutter应用程序部分

在控制台上可以对数据进行CRUD操作,GraphQL服务器的准备已经完成。
现在我们可以开始创建一个实际的待办事项应用程序。

需要设定

首先需要安装必要的软件包。
在pubspec.yaml文件中添加graphql_flutter | Flutter Package。

dependencies:
  flutter:
    sdk: flutter
  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2
  graphql_flutter: 1.0.0+4

获取包以进行安装

将graphql_flutter导入到main.dart文件中。

import 'package:graphql_flutter/graphql_flutter.dart';

需要将整个应用程序用 GraphQlProvider 包装起来,因此需要修改 main 如下:

void main() => runApp(
  GraphQLProvider(
    child: CacheProvider(
      child: MyApp()
    ),
  )
);

在中国,你可以这样翻译:

客户端的GraphQL定义

创建一个用于定义GraphQL客户端的文件。
在lib目录下创建一个名为services的文件夹,并在其中创建一个名为graphQldata.dart的文件。

在 graphQldata.dart 文件中,需要导入必要的包并进行 class 定义。设置 GraphQLServer 的终端 URL,将终端 URL 和缓存设置为客户端的配置,并生成客户端实例。

graphQldata.dart 的意思是GraphQl数据.dart。

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

class GraphQlObject {
  static HttpLink httpLink = HttpLink(
    uri: 'https://flutter-todo-app-hasura.herokuapp.com/v1/graphql',
  );
  static Link link = httpLink as Link;
  ValueNotifier<GraphQLClient> client = ValueNotifier(
    GraphQLClient(
      cache: InMemoryCache(),
      link: link,
    ),
  );
}

GraphQlObject graphQlObject = new GraphQlObject();

为了对由GraphQLServer生成的表进行操作,设置mutation和query。

graphQldata.dart可以被重述为:

图形查询数据.dart

String updateCompletedMutation(result, index) {
  return (
      """mutation ToggleTask{
           update_todo(where: {
            id: {_eq: ${result.data["todo"][index]["id"]}}},
            _set: {isCompleted: ${!result.data["todo"][index]["isCompleted"]}}) {
               returning {isCompleted } 
           }
      }"""
  );
}

String deleteTaskMutation(result, index) {
  return (
      """mutation DeleteTask{       
            delete_todo(
               where: {id: {_eq: ${result.data["todo"][index]["id"]}}}
            ) { returning {id} }
      }"""
  );
}

String addTaskMutation(title, content) {
  print(title);
  print(content);
  return (
      """mutation AddTask{
            insert_todo(objects: {content: "$content", isCompleted: false, title: "$title"}) {
              returning {
                id
              }
            }
      }"""
  );
}

String fetchQuery() {
  return (
      """query TodoGet{
           todo {
              title
              content
              isCompleted
              id
           }
      } """
  );
}

GraphQLObject的配置

由於GraphQL的設定已完成,現在需要將GraphQL物件設置到main.dart的GraphQLProvider中。

import 'package:todo_app_graphql/services/graphQldata.dart';  // 追加

void main() => runApp(
  GraphQLProvider(
    client: graphQlObject.client,  // 追加
    child: CacheProvider(
      child: MyApp()
    ),
  )
);

在Mutations中,需要初始化GraphQLClient对象以更新数据库。

主要.dart

class _MyHomePageState extends State<MyHomePage> {
  GraphQLClient client;
  // This widget is the root of your application.
  initMethod(context) {
    client = GraphQLProvider.of(context).value;
  }

  @override
  Widget build(BuildContext context) {
    WidgetsBinding.instance.addPostFrameCallback((_) => initMethod(context));

执行查询

从这里开始执行实际的查询,并尝试以列表形式显示接收到的数据。
使用查询组件(Query Widget)来接收查询。

return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Query(
          options: QueryOptions(document: fetchQuery(), pollInterval: 1),
          builder: (QueryResult result, {VoidCallback refetch}) {

          }
        )
      )
)

执行查询并获取数据

使用option指定实现查询,并将builder接收到的数据作为结果返回。
使用这个结果来显示数据列表。


if (result.data == null) {
  return Center(
    child: CircularProgressIndicator(),
  );
}
return ListView.builder(
  itemCount: result.data["todo"].length,
  itemBuilder: (BuildContext context, int index) {
    return Card (
      child: Column(
        mainAxisAlignment: MainAxisAlignment.start,
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Row(
            children: <Widget>[
              Flexible(
                child: Container(
                  height: MediaQuery.of(context).size.height/14.0,
                  padding: EdgeInsets.only(left: 15.0),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text('Title',style: TextStyle(color: Colors.grey),),
                      Text(
                        result.data["todo"][index]["title"],
                        overflow: TextOverflow.ellipsis,
                        style: TextStyle(fontSize: 17.0),
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
          Row(
            children: <Widget>[
              Flexible(
                child: Container(
                  height: MediaQuery.of(context).size.height/14.0,
                  padding: EdgeInsets.only(left: 15.0),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text('Content',style: TextStyle(color: Colors.grey),),
                      Text(
                        result.data["todo"][index]["content"],
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                        style: TextStyle(fontSize: 15.0),
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
);

Simulator Screen Shot - iPhone Xʀ - 2019-07-05 at 13.38.58.png

使用Mutation来进行新数据的注册、更新和删除。

使用查询可以列出数据库中的数据。
接下来,将使用Mutation来生成、删除和更新新数据。
首先,实现生成新数据的功能。

根据输入的内容生成新数据的处理方式如下所示。

await client.mutate(
  MutationOptions(
    document: addTaskMutation(
        titleController.text, contentController.text),
  ),
);

当按下浮动按钮时,实现弹出对话框的功能。

floatingActionButton: FloatingActionButton(
  heroTag: "Tag",
  onPressed: () {
    showDialog(
        context: context,
        builder: (BuildContext context1) {
          return AlertDialog(
            shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0))),
            title: Text("Add task"),
            content: Container(
              width: 500.0,
              child: Form(
                  child: Column(
                      mainAxisSize: MainAxisSize.min,
                      children: <Widget>[
                        TextField(
                          controller: titleController,
                          decoration: InputDecoration(labelText: "Title"),
                        ),
                        TextFormField(
                          maxLines: 10,
                          controller: contentController,
                          decoration: InputDecoration(labelText: "Coentent"),
                        ),
                        Center(
                            child: Padding(
                                padding: const EdgeInsets.only(top: 10.0),
                                child: RaisedButton(
                                    elevation: 7,
                                    shape: RoundedRectangleBorder(
                                      borderRadius: BorderRadius.circular(12),
                                    ),
                                    color: Colors.black,
                                    onPressed: () async {
                                      await client.mutate(
                                        MutationOptions(
                                          document: addTaskMutation(
                                              titleController.text, contentController.text),
                                        ),
                                      );
                                      Navigator.pop(context);
                                      titleController.text = "";
                                      contentController.text = "";
                                    },
                                    child: Text(
                                      "Add",
                                      style: TextStyle(color: Colors.white),
                                    )
                                )
                            )
                        )
                      ]
                  )
              ),
            )
          );
        }
     );
  },
  child: Icon(Icons.add),
),
Simulator Screen Shot - iPhone Xʀ - 2019-07-05 at 13.40.38.png

在接下来的步骤中,我们将实施删除和更新操作。
我们将在卡片元素中增加复选框和删除图标。

final TextEditingController titleController = new TextEditingController();
final TextEditingController contentController = new TextEditingController();

return Card (
      child: Column(
        mainAxisAlignment: MainAxisAlignment.start,
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Row(
            children: <Widget>[
              Flexible(
                child: Container(
                  height: MediaQuery.of(context).size.height/14.0,
                  padding: EdgeInsets.only(left: 15.0),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text('Title',style: TextStyle(color: Colors.grey),),
                      Text(
                        result.data["todo"][index]["title"],
                        overflow: TextOverflow.ellipsis,
                        style: TextStyle(fontSize: 17.0),
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
          Row(
            children: <Widget>[
              Flexible(
                child: Container(
                  height: MediaQuery.of(context).size.height/14.0,
                  padding: EdgeInsets.only(left: 15.0),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text('Content',style: TextStyle(color: Colors.grey),),
                      Text(
                        result.data["todo"][index]["content"],
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                        style: TextStyle(fontSize: 15.0),
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
          // 追加
          Row(
            mainAxisAlignment: MainAxisAlignment.end,
            children: <Widget>[
              Checkbox(
                value: result.data["todo"][index]["isCompleted"],
                onChanged: (bool value) {

                 },
              ),
              IconButton(
                icon: Icon(Icons.delete),
                onPressed: () {

                },
              ),
            ],
          ),
        ],
      ),
    );

将削除按钮的onPressed改为以下方式

onPressed: () async{
  await client.mutate(
    MutationOptions(
      document: deleteTaskMutation(
          result, index),
    ),
  );
},

将复选框的onChanged属性设置为以下内容。

onChanged: () async {
  await client.mutate(
    MutationOptions(
      document: changeCompletedMutation(
          result, index),
    ),
  );
},

每个都发出Mutation来删除和更新数据。这样,ToDo应用程序就完成了。

总结

我在这次使用 Hasura 实现 GraphQL 服务器时尝试了一下,发现在控制台上轻松操作 API 真的很方便。
Prisma 作为一种类似 ORM 的工具,在环境搭建方面可能会有些麻烦,但是它能够提供更多的自定义选项,所以如果你想要认真实现服务器端的功能,我认为用 Prisma 会更好。

一旦定义好Flutter的Query和Mutation,其他方面并不是太难,所以我觉得它在相容性方面还是相当不错的。

bannerAds