LoginSignup
20
13

More than 3 years have passed since last update.

HasuraでGraphQL Server実装してFlutterのTODOアプリ作ってみる

Last updated at Posted at 2019-07-05

個人的に最近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を選択、この時アカウント作成も行われる
必要な情報を入力し進めるとデプロイが完了する
スクリーンショット 2019-07-03 15.43.02.png

Postgresのテーブルを作成、データを作る

下のViewボタンをクリックするとコンソールが開く
ヘッダーにDATAの項目を選択し、create tableからテーブルを作成する
スクリーンショット 2019-07-05 13.49.22.png

デーブルが作成できたらヘッダーのGRAPHQLの項目に戻り、左メニューに作成したテーブルのqueryやmutationなどが選択できる。

insert_todoでmutaitonを発行しテーブルにデータを登録できる。
スクリーンショット 2019-07-03 15.52.01.png

クライアント側のFlutterアプリ部分を作る

コンソール上でデータのCRUDが可能になり、GraphQL Serverの準備は完了。
実際にtodoアプリを作っていく。

必要設定

まずは必要パッケージをインストールする。
pubspec.yamlgraphql_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),
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
);

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

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ボタンを押すとデータが生成され、一覧に反映される
Simulator Screen Shot - iPhone Xʀ - 2019-07-05 at 13.40.34.png
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 Serverを実装してみたが、APIをコンソールで簡単に操作できるのは大きい。
PrismaはORM的な立ち位置のため環境構築に多少面倒臭さはあるがその分ガッツリカスタマイズできるので、しっかりサーバーサイドを実装したいのならPrismaの方がいいと思う。

FlutterもQuery,Mutationの定義さえしてしまえば他のところはそれほど難しいところはないので相性はそれなりにいいと感じた。

20
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
13