我打算使用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免费套餐。

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

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

创建客户端的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),
),
],
),
),
),
],
),
],
),
);
}
);

使用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),
),

在接下来的步骤中,我们将实施删除和更新操作。
我们将在卡片元素中增加复选框和删除图标。
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,其他方面并不是太难,所以我觉得它在相容性方面还是相当不错的。