10
3

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.

FlutterAdvent Calendar 2023

Day 3

flutterでgraphQLを使用してみた(graphql_codegenとgraphql_flutter)

Last updated at Posted at 2023-12-02

株式会社Neverのkousoです。

株式会社Neverは「NEVER STOP CREATE 作りつづけること」をビジョンに掲げ、理想を実現するためにプロダクトを作り続ける組織です。モバイルアプリケーションの受託開発、技術支援、コンサルティングを行っております。アプリ開発のご依頼や開発面でのお困りの際はお気楽にお問合せください。
https://neverjp.com/contact/

概要

最近、graphQLを使用しているので、勉強のためにタイトル記載のパッケージを使用してみました。
色々なパッケージがある中、mutationやqueryをコード生成してくれるgraphql_codegen、キャッシュ機能があるgraphql_flutterが実際に活かせそうだと思ったので使用してみました。

GraphQLのAPI

今回はこちらを使用しました

デモアプリ

20件取得して、次のページでさらに20件取得してます

graphql_codegen

  • graphql_codegen
  • build_runner(コード生成に必要)

のパッケージをpubspec.yamlに追加します

schema.graphqlを追加

IDEのGraphiQLやApollo Studioもありますが、今回はPostmanを使用してスキーマを取得しました。

schema.graphqlファイルは、Dartのコード生成に必須です。
query、mutation、subscriptionなどの構造を定義して、そのスキーマに基づいて、queryやmutationを実装し、適切なデータ型でレスポンスできます。
つまり、このファイルに基づいて.graphqlで定義されたqueryなどを解釈してコード生成するので、ないとコード生成されないです。
私自身、少しはまりました。。。

lib/graphql/schema.graphql
type Query {
    characters(page: Int!, filter: FilterCharacter): Characters
    locations(page: Int!): Locations
    episodes(page: Int!): Episodes
}

type Character {
    id: ID!
    name: String
    image: String
}

type Location {
    id: ID!
    name: String
    type: String
}

type Episode {
    id: ID!
    name: String
    episode: String
}

type Characters {
    results: [Character]
}

type Locations {
    results: [Location]
}

type Episodes {
    results: [Episode]
}

input FilterCharacter {
    name: String
}

queries.graphqlを追加

データ取得に必要な情報を記載していきます

lib/graphql/queries.graphql
query GetCharacters($page: Int!, $name: String!) {
    characters(page: $page, filter: { name: $name }) {
        results {
            id
            name
            image
        }
    }
}

query GetLocations {
    locations(page: 1) {
        results {
            id
            name
            type
        }
    }
}

query GetEpisodes {
    episodes(page: 1) {
        results {
            id
            name
            episode
        }
    }
}

自動生成の設定

graphql_codegenでコードを自動生成するので、その設定をします

build.yamlに下記を追加

targets:
  $default:
    builders:
      graphql_codegen:
        options:
          scopes:
            - lib/graphql/**
          clients:
            - graphql
            - graphql_flutter

graphql_flutterを使用するので追加してます

lib/graphql/**
lib/graphql/    ←コード生成されず

コード生成されずにハマったので、**は必要です!!

build_runnerでコード生成

dart run build_runner build

上記コマンドでコード生成します

代替テキスト

上記のように、ファイルが生成されます

比較するとわかりやすいので、codegenの有無

codegenを使用しない場合

const String getCharacters = r'''
query GetCharacters($page: Int!, $name: String!) {
  characters(page: $page, filter: { name: $name }) {
    results {
      id
      name
      image
    }
  }
}
''';

const String getLocations = r'''
query {
  locations(page: 1) {
    results {
      id
      name
      type
    }
  }
}
''';

const String getEpisodes = r'''
query {
  episodes(page: 1) {
    results {
      id
      name
      episode
    }
  }
}
''';

このように、ハードコードで書いていくことになります
実際、タイプミスでエラーになって悩んだ経験もあるので、そういうリスクは減らしたいです。

codegenを使用する場合

query GetCharacters($page: Int!, $name: String!) {
    characters(page: $page, filter: { name: $name }) {
        results {
            id
            name
            image
        }
    }
}

query GetLocations {
    locations(page: 1) {
        results {
            id
            name
            type
        }
    }
}

query GetEpisodes {
    episodes(page: 1) {
        results {
            id
            name
            episode
        }
    }
}

このように記載できます
コード整形もできますし、コード生成の段階でエラーがでます。

graphql_flutter

  • graphql_codegen
  • flutter_hooks

のパッケージをpubspec.yamlに追加します
状態管理にflutter_hooksを使用します

 lib/main.dart
class MyApp extends HookWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    final HttpLink httpLink = HttpLink('https://rickandmortyapi.com/graphql');

    final client = useState<GraphQLClient>(
      GraphQLClient(
        link: httpLink,
        cache: GraphQLCache(),
      ),
    );

    return GraphQLProvider(
      client: client,
      child: const MaterialApp(
        home: CharacterList(),
      ),
    );
  }
}

キャッシュのところでは、Hiveを使用してローカルDBに保存するのもよさそうです

一覧表示

Query$GetCharacters$Widget(
  options: Options$Query$GetCharacters(
    variables: Variables$Query$GetCharacters(
      page: index.value,
      name: searchText.value,
    ),
  ),
  builder: (
    QueryResult result, {
    VoidCallback? refetch,
    FetchMore? fetchMore,
  }) {
    if (result.isLoading) {
      return const Center(child: CircularProgressIndicator());
    }

    if (result.hasException) {
      return Text(result.exception.toString());
    }
    final data = result.data;
    if (data == null) {
      return const Text('データがありません');
    }

    final charactersResponse =
        CharactersResponse.fromJson(data['characters']);
    final characters = charactersResponse.results;

    return ListView.builder(
      itemCount: characters.length,
      itemBuilder: (context, index) {
        final character = characters[index];

        return ListTile(
          title: Text(character.name ?? ''),
          leading: Image.network(character.image ?? ''),
        );
      },
    );
  },
),

codegenで生成したコードは上記のように記載できます。

Query$GetCharacters$Widget:codegenで生成されたウィジェットで、この場合はGetCharactersのqueryの結果を表示するWidgetです。

options: Options$Query$GetCharacters(
    variables: Variables$Query$GetCharacters(
      page: index.value,
      name: searchText.value,
    ),
),

queryを実行するオプションを設定します。
Variables$Query$GetCharacters :クエリに必要な変数を指定しています(この場合は、pageの数字と、キャラクターの名前です)。

mutationの場合は、mutaions.graphqlでmutationを生成して、QueryのところをMutationに変更します。

コード全文

lib/character_list.dart
class CharacterList extends HookWidget {
  const CharacterList({super.key});

  @override
  Widget build(BuildContext context) {
    final searchText = useState<String>('');
    final index = useState<int>(1);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Rick and Morty Characters'),
      ),
      body: Column(
        children: [
          Row(
            children: [
              Expanded(
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: TextFormField(
                    decoration: const InputDecoration(
                      labelText: 'Search',
                      border: OutlineInputBorder(),
                    ),
                    onChanged: (value) {
                      searchText.value = value;
                    },
                  ),
                ),
              ),
              IconButton(
                icon: const Icon(Icons.arrow_back),
                onPressed: () {
                  if (index.value > 1) {
                    index.value--;
                  }
                },
              ),
              IconButton(
                icon: const Icon(Icons.arrow_forward),
                onPressed: () => index.value++,
              ),
            ],
          ),
          Expanded(
            child: Query$GetCharacters$Widget(
              options: Options$Query$GetCharacters(
                variables: Variables$Query$GetCharacters(
                  page: index.value,
                  name: searchText.value,
                ),
              ),
              builder: (
                QueryResult result, {
                VoidCallback? refetch,
                FetchMore? fetchMore,
              }) {
                if (result.isLoading) {
                  return const Center(child: CircularProgressIndicator());
                }

                if (result.hasException) {
                  return Text(result.exception.toString());
                }
                final data = result.data;
                if (data == null) {
                  return const Text('データがありません');
                }

                final charactersResponse =
                    CharactersResponse.fromJson(data['characters']);
                final characters = charactersResponse.results;

                return ListView.builder(
                  itemCount: characters.length,
                  itemBuilder: (context, index) {
                    final character = characters[index];

                    return ListTile(
                      title: Text(character.name ?? ''),
                      leading: Image.network(character.image ?? ''),
                    );
                  },
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

終わりに

graphql_codegen、graphql_flutterともに、いいパッケージだと思いました。

query、mutationはgraphql_codegenで自動生成し、Widget内で状態が完結する場合は、hooksとgraphql_flutter、他の画面でも状態管理が必要な場合はRiverpodを使用するなど、自分の中でそれぞれの役割が明確になった気がします。
他にもgraphQLのパッケージがあるので、使用、比較してみたいと思いました。

ありがとうございました!

10
3
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
10
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?