個人的に最近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 Serverになる。
サクッと実装したい人向け。
HasuraにGraphQL Server実装
Herokuにデプロイ
まず以下のURLを開く
Instant realtime GraphQL on Postgres | Hasura GraphQL Engine
Heroku Free Tierを選択
Deploy to Heroku
を選択、この時アカウント作成も行われる
必要な情報を入力し進めるとデプロイが完了する
Postgresのテーブルを作成、データを作る
下のViewボタンをクリックするとコンソールが開く
ヘッダーにDATAの項目を選択し、create table
からテーブルを作成する
デーブルが作成できたらヘッダーのGRAPHQLの項目に戻り、左メニューに作成したテーブルのqueryやmutationなどが選択できる。
insert_todoでmutaitonを発行しテーブルにデータを登録できる。
クライアント側のFlutterアプリ部分を作る
コンソール上でデータのCRUDが可能になり、GraphQL Serverの準備は完了。
実際にtodoアプリを作っていく。
必要設定
まずは必要パッケージをインストールする。
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
Package Get
でインストール
main.dart
にgraphql_flutterをインポート
import 'package:graphql_flutter/graphql_flutter.dart';
graphal_flutter
パッケージはアプリ全体をGraphQlProviderでラップする必要があるので以下のようにmainを修正する
void main() => runApp(
GraphQLProvider(
child: CacheProvider(
child: MyApp()
),
)
);
クライアントGraphQLの定義
クライアント側のGraphQLの定義をするファイルを作成する。
lib配下にservicesディレクトリを作成、配下にgraphQldata.dart
を作成する。
graphQldata.dart
に必要パッケージのインポートとclass定義を行い、GraphQLServerのエンドポイントURLを設定、クライアント側の設定としてエンドポイントURLとキャッシュの設定を行い、clientのインスタンスを生成する
graphQldata.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
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の設定ができたので、main.dart
のGraphQLProviderにgraphQlObjectを設定する。
import 'package:todo_app_graphql/services/graphQldata.dart'; // 追加
void main() => runApp(
GraphQLProvider(
client: graphQlObject.client, // 追加
child: CacheProvider(
child: MyApp()
),
)
);
Mutationsでは、データベースを更新するためにGraphQLClientオブジェクトが必要なため、GraphQLClientオブジェクトを初期化する。
main.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の実行
ここから実際にQueryを実行して、受信したデータをリストで表示してみる。
Queryの受け取りは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}) {
}
)
)
)
Queryを実行してデータを取得
optionで実装するQueryを指定し、builderで受けとったデータがresultとして返ってくる。
このresultを使ってデータをリスト表示してみる。
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で新しいデータの登録、更新、削除を行う
Queryを用いてDBのデータを一覧表示することができた。
次は新しいデータの生成、削除、更新を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),
),
これで内容を入力し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 Serverを実装してみたが、APIをコンソールで簡単に操作できるのは大きい。
PrismaはORM的な立ち位置のため環境構築に多少面倒臭さはあるがその分ガッツリカスタマイズできるので、しっかりサーバーサイドを実装したいのならPrismaの方がいいと思う。
FlutterもQuery,Mutationの定義さえしてしまえば他のところはそれほど難しいところはないので相性はそれなりにいいと感じた。