40
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Flutter大学Advent Calendar 2021

Day 13

FlutterでGraphQLを使ってみよう

Last updated at Posted at 2021-12-12

はじめに

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

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

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

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

概要

ポケモンのリストを取得できるGraphQLサーバーを公開してくれているので、
それを使ってFlutter上でポケモンのリストを表示するサンプルコードを作りました
上記のコードを作る過程を見ることでFlutterでGraphQLを使う方法を紹介していきます。

![スクリーンショット 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)

どんな人が対象?

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

GraphQLを触ってみる

概要で紹介したポケモンのリストを取得できるGraphQLサーバーがあるのでまずはそこでどうやってポケモンのリストを取得するか確認していきましょう。
スクリーンショット 2021-12-13 0.31.32.png

ページを開くと大まかに

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

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

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

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

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

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

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

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

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

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
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というツールを使ってSDLをダウンロードします。
スクリーンショット 2021-12-13 0.56.04.png

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

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

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

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

  • ポケモンの情報のどのフィールドを使用するか宣言する部分(fragment)
lib/gql/fragment.graphql
fragment pokemonField on Pokemon {
  id
  name
  image
  types
}
  • クエリ部分
lib/gql/queries/search_pokemons.graphql
query searchPokemons {
  pokemons(
      first: 20
  ) {
    ...pokemonField
  }
}

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

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にコードが生成されます。

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

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

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

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サーバーにクエリを投げる
  2. レスポンスが返ってくるまではロード画面にする
  3. 返ってきたらそのデータを使ってListViewを作る

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

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

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

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

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

 

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

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

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型から変換する羽目になるので、自動生成をオススメします。

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

最後に

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

サンプルコードも用意しているので興味を持ったらぜひ動かしてみてください。

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

40
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
40
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?