はじめに
GraphQL、使っていますか?
私は個人でサービスを作る時にGraphQLを使っています。
GraphQLを使うことで以下2点の恩恵が得られるところが特に気に入っています。
- レスポンスのデータ型がはっきりと分かる
- 呼び出し側でデータ構造のどこまで取得するか決められる
TypeScript等でGraphQLを使うサンプルコードはよく見るものの、
DartでGraphQLを使用しているコードはあまり見かけなかったのでGraphQLの良さを伝えつつサンプルコードを紹介できればと思います。
良さそうだと思ったら記事にてサンプルコードを用意しているのでぜひ動かしてみてください。
概要
ポケモンのリストを取得できるGraphQLサーバーを公開してくれているので、
それを使ってFlutter上でポケモンのリストを表示するサンプルコードを作りました。
上記のコードを作る過程を見ることでFlutterでGraphQLを使う方法を紹介していきます。
どんな人が対象?
- GraphQLはよく聞くけどまだ使ったことはない人
- Flutter以外ではGraphQLを使ったことがあるけどFlutterでの使い方がわからない人
- FlutterでGraphQLを使ってみたが、型がはっきりしているなどの長所をうまく活かせなかった人
GraphQLを触ってみる
概要で紹介したポケモンのリストを取得できるGraphQLサーバーがあるのでまずはそこでどうやってポケモンのリストを取得するか確認していきましょう。
ページを開くと大まかに
- クエリを書くところ
- 実行結果
- 各データの型等の説明
が表示されます。
GraphQLサーバーはだいたいこういったプレイグラウンド環境を用意してくれるのが嬉しいですよね。
説明でパラメータfirst(一回あたりのほしいデータ数)
を渡してくれと言われているので以下のようにクエリを書いたらしっかり10件のポケモンの情報が取得できましたね。
このクエリを元にFlutterから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では動きました。)
# 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
# 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をダウンロードします。
- URLを入力するところに対象のGraphQLサーバーのURLを入力
- Docsを押す
- 三点リーダーを押す
- Export SDL
でダウンロードできました。そのファイルをプロジェクトのルートに配置します。
2. 叩きたいクエリを記述したファイルの作成
今回は以下の2つにファイルを分けることでコードを自動生成した時に扱いやすくします。
- ポケモンの情報のどのフィールドを使用するか宣言する部分(fragment)
fragment pokemonField on Pokemon {
id
name
image
types
}
- クエリ部分
query searchPokemons {
pokemons(
first: 20
) {
...pokemonField
}
}
3. 自動生成に関する設定ファイルの作成
プロジェクトのルートに以下のファイルを設置します。
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を吐き出したかったので少し小難しいコードを書いていますが、その部分を省略して書くと以下のようなコードになります。
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;
}
}
大まかな流れは以下のようになっています。
- FutureBuilderでポケモンGraphQLサーバーにクエリを投げる
- レスポンスが返ってくるまではロード画面にする
- 返ってきたらそのデータを使って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?>?
であることが分かっていますね。
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型から変換する羽目になるので、自動生成をオススメします。
mixin PokemonFieldMixin {
late String id;
String? name;
String? image;
List<String?>? types;
}
最後に
いかがだったでしょうか。
思ったよりGraphQLをFlutterで使うのって簡単だし便利なんだなって思っていただければ幸いです。
サンプルコードも用意しているので興味を持ったらぜひ動かしてみてください。
それでは良きFlutterライフを!