FlutterでGraphQLを使ってみよう

## はじめに
GraphQL、使っていますか？
私は個人でサービスを作る時にGraphQLを使っています。
GraphQLを使うことで以下２点の恩恵が得られるところが特に気に入っています。

- レスポンスのデータ型がはっきりと分かる
- 呼び出し側でデータ構造のどこまで取得するか決められる

TypeScript等でGraphQLを使うサンプルコードはよく見るものの、
DartでGraphQLを使用しているコードはあまり見かけなかったのでGraphQLの良さを伝えつつサンプルコードを紹介できればと思います。

良さそうだと思ったら記事にてサンプルコードを用意しているのでぜひ動かしてみてください。

## 概要
ポケモンのリストを取得できる[GraphQLサーバーを公開してくれている](https://graphql-pokemon2.vercel.app/)ので、
それを使ってFlutter上で[ポケモンのリストを表示するサンプルコードを作りました](https://github.com/yam-yanu/flutter-graphql-artemis)。
上記のコードを作る過程を見ることでFlutterでGraphQLを使う方法を紹介していきます。
<center>
![スクリーンショット 2021-12-13 0.14.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52249/1a7ca80a-6831-7074-2b49-72a28727d8f6.png)
</center>

## どんな人が対象？
- GraphQLはよく聞くけどまだ使ったことはない人
- Flutter以外ではGraphQLを使ったことがあるけどFlutterでの使い方がわからない人
- FlutterでGraphQLを使ってみたが、型がはっきりしているなどの長所をうまく活かせなかった人

## GraphQLを触ってみる
概要で紹介した[ポケモンのリストを取得できるGraphQLサーバー](https://graphql-pokemon2.vercel.app/)があるのでまずはそこでどうやってポケモンのリストを取得するか確認していきましょう。
![スクリーンショット 2021-12-13 0.31.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52249/9e80786b-a892-4e55-16f9-0090be7d4793.png)

ページを開くと大まかに

- クエリを書くところ
- 実行結果
- 各データの型等の説明

が表示されます。
GraphQLサーバーはだいたいこういったプレイグラウンド環境を用意してくれるのが嬉しいですよね。


説明でパラメータ`first(一回あたりのほしいデータ数)`を渡してくれと言われているので以下のようにクエリを書いたらしっかり10件のポケモンの情報が取得できましたね。
このクエリを元にFlutterからGraphQLを叩く準備をしていきます。

```graphql:search_pokemons.graphql
{
  pokemons(
    first: 10
  ){
    name
    image
    types
  }
}
```

## FlutterでGraphQLライブラリを入れる

今回は触ったことがあるという理由で[artemisというパッケージ](https://pub.dev/packages/artemis)を利用してサンプルコードを作っていきます。
このパッケージだとキャッシュが効かない等デメリットがあるので、自分のプロジェクトで採用する際は[ferry](https://pub.dev/packages/ferry)等別のパッケージも検討することをおすすめします。

artemisは大まかに以下の2つで構成されています。

- 使用するGraphQLサーバーのSDL(型定義などが書かれたファイル)と自分が叩きたいクエリを読み込んでいい感じのDartコードを自動生成してくれる機能
- 上記で自動生成したコードを使用してGraphQLサーバーを叩いてくれるClient機能

そのため、自動生成に関するパッケージとClientに関するパッケージを入れる必要があります。
これらを`pubspec.yaml`に追加して`flutter pub get`します。
(2021/12/12、flutter version 2.5.3では動きました。)

```yaml:pubspec.yaml
# dependenciesにはClient機能に関するパッケージを入れてます
dependencies:
  artemis: ^7.2.6-beta
  equatable: ^2.0.3
  gql: ^0.13.1-alpha
  # 2021/12/12にまっさらな状態でpub getするとこのパッケージの別バージョンでエラーが出たので一旦このバージョンで固定
  gql_exec: ^0.3.1-alpha+1635149947799
  json_annotation: ^4.3.0
```

```yaml:pubspec.yaml
# dev_dependenciesにはコード自動生成機能に関するパッケージを入れてます
dev_dependencies:
  build_runner: ^2.1.4
  json_serializable: ^6.0.1
```

## FlutterでGraphQLを使ってみる
まずはGraphQLサーバーのSDLと叩きたいクエリからDartのコードを自動生成します。
その次にFlutterでそのコードを使用して画面で情報を表示する部分のコードを書いていきます。

### コードの自動生成

**1. GraphQLサーバーのSDLの取得**
いまいち一番簡単なSDLの取得方法が分からなかったので今回は[Altair](https://altair.sirmuel.design/)というツールを使ってSDLをダウンロードします。
![スクリーンショット 2021-12-13 0.56.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52249/79de9525-1cc6-5045-4783-4bb48dd50315.png)

1. URLを入力するところに対象のGraphQLサーバーのURLを入力
1. Docsを押す
1. 三点リーダーを押す
1. Export SDL

でダウンロードできました。そのファイルをプロジェクトのルートに配置します。


**2. 叩きたいクエリを記述したファイルの作成**

今回は以下の2つにファイルを分けることでコードを自動生成した時に扱いやすくします。

- ポケモンの情報のどのフィールドを使用するか宣言する部分(fragment)

```graphql:lib/gql/fragment.graphql
fragment pokemonField on Pokemon {
  id
  name
  image
  types
}
```

- クエリ部分

```graphql:lib/gql/queries/search_pokemons.graphql
query searchPokemons {
  pokemons(
      first: 20
  ) {
    ...pokemonField
  }
}
```

**3. 自動生成に関する設定ファイルの作成**
プロジェクトのルートに以下のファイルを設置します。

```yaml:build.yaml
targets:
  $default:
    sources:
      - lib/**
      - schema.graphql
    builders:
      artemis:
        options:
          fragments_glob: lib/gql/fragment.graphql
          schema_mapping:
            - schema: schema.graphql
              queries_glob: lib/gql/queries/*.graphql
              output: lib/gql/generated/generated.dart
```

`fragments_glob`で使用するフィールドの定義(fragment)のファイルがある場所を指定し、
`queries_glob`でクエリのファイルがある場所を指定し、
`output`で自動生成したコードをどこに配置するかを指定しています。

**4. コマンドを叩いてコードを自動生成**
以下のコマンドを叩けば上記の設定ファイル通り`lib/gql/generated/generated.dart`にコードが生成されます。

```bash
# fvmを使用している方は最初にfvmをつける
flutter pub run build_runner build --delete-conflicting-outputs
```

### Flutterで生成されたコードを使う

サンプルコードだとGraphQLサーバーを叩いた時にエラーが出た時にExceptionを吐き出したかったので少し小難しいコードを書いていますが、その部分を省略して書くと以下のようなコードになります。

```dart:lib/main.dart
import 'package:artemis/artemis.dart';
import 'package:flutter/material.dart';
import 'package:flutter_graphql_artemis/gql/generated/generated.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const PokemonListView(),
    );
  }
}

class PokemonListView extends StatelessWidget {
  const PokemonListView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Pokemon List'),
      ),
      body: FutureBuilder<List<PokemonFieldMixin?>?>(
        future: searchPokemons(),
        builder: (context, snapshot) {
          if (snapshot.connectionState != ConnectionState.done) {
            return const Center(child: CircularProgressIndicator());
          }
          final pokemons = snapshot.data;
          if (pokemons == null) {
            return const Center(child: Text('データを取得できませんでした.'));
          }
          return ListView.separated(
            itemCount: pokemons.length,
            separatorBuilder: (context, index) {
              return const Divider(height: 0.5);
            },
            itemBuilder: (context, index) {
              final pokemon = pokemons[index];
              if (pokemon == null) {
                return const SizedBox();
              }
              return ListTile(
                leading: CircleAvatar(
                  backgroundImage: NetworkImage(pokemon.image!),
                  backgroundColor: Colors.grey,
                ),
                // GraphQLを使用することによって補完が効く+データ型がString?であることも分かる
                title: Text(pokemon.name!),
                subtitle: Text('Type: ${pokemon.types!.join(',')}'),
              );
            },
          );
        },
      ),
    );
  }

  // fragmentで分けておいたのでPokemonFieldMixinと宣言できる
  Future<List<PokemonFieldMixin?>?> searchPokemons() async {
    final _httpLink =
        Uri.parse('https://graphql-pokemon2.vercel.app').toString();
    final client = ArtemisClient(_httpLink);
    // SearchPokemonsQuery()がクエリから自動生成した部分
    final response = await client.execute(SearchPokemonsQuery());
    return response.data!.pokemons;
  }
}
```

大まかな流れは以下のようになっています。

1. FutureBuilderでポケモンGraphQLサーバーにクエリを投げる
1. レスポンスが返ってくるまではロード画面にする
1. 返ってきたらそのデータを使ってListViewを作る

ここで注目したいポイントを書いていきます。

**クエリからコードを自動生成するのでDart内で頑張ってクエリの文字列を作る必要がない**

artemisを使用していない時はDartのStringで頑張ってクエリを書いていましたが、この作業はとても辛いのでオススメしません。クエリに必要なパラメータが増えれば触れるほどクエリを文字列で管理することが辛くなります。
最初の設定が面倒くさくても自動生成をオススメします。

昔、以下のように文字列に変数を入れ込むような実装をしていましたがとても大変でした。

```dart
  final String _defaultProperty = '''
    id
    name
    members {
      ${UserRepository.defaultProperty}
    }
  ''';
  final String _defaultGetQuery = r'''
    query{
      teams(
        first: 10
        ${queries}
      ){
        data{
          ${defaultProperty}
        }
      }
    }
  ''';

```
　

**自動生成されたコードによってレスポンスのデータ構造やデータ型がはっきりと分かる**

以下の部分だと`SearchPokemonsQuery()`を叩いて`data`の下についているのが
`List<PokemonFieldMixin?>?`であることが分かっていますね。

```dart:lib/main.dart
  Future<List<PokemonFieldMixin?>?> searchPokemons() async {
    final _httpLink =
        Uri.parse('https://graphql-pokemon2.vercel.app').toString();
    final client = ArtemisClient(_httpLink);
    // SearchPokemonsQuery()がクエリから自動生成した部分
    final response = await client.execute(SearchPokemonsQuery());
    return response.data!.pokemons;
  }
```

さらに自動生成したコードを見ると、`PokemonFieldMixin`のプロパティはしっかりと型が宣言されているため、安心して利用できますね。これも自動生成なしだと全てのプロパティをdynamic型から変換する羽目になるので、自動生成をオススメします。

```dart:lib/gql/generated/generated.graphql.dart
mixin PokemonFieldMixin {
  late String id;
  String? name;
  String? image;
  List<String?>? types;
}
```


## 最後に
いかがだったでしょうか。
思ったよりGraphQLをFlutterで使うのって簡単だし便利なんだなって思っていただければ幸いです。

[サンプルコードも用意している](https://github.com/yam-yanu/flutter-graphql-artemis)ので興味を持ったらぜひ動かしてみてください。

それでは良きFlutterライフを!