【Flutter】graphql_flutterのクライアントにTimeoutを設定する

graphql_flutterはgrahQLの有名なライブラリーであるApolloにインスパイアされたflutterのgraphql clientライブラリです。 FlutterでgraphQLを使用する場合はgraphql_flutterを使用するかgraphqlを使用することになるかと思います。

ネットワーク通信を行う場合は、通信環境の悪い場合を考慮してTimeout設定を行います。 しかしながらgraphql_flutterにはデフォルトのタイムアウト設定用のAPIが存在しません。 本稿ではgraphql_flutterにおけるTimeout設定に関して記載します。

TL;DR

  • Timeout用のAPIが存在しないのでFutureのタイムアウトを使用する
  • Timeout設定が一箇所に集約されるようにGraphQLClient用のクラスを作成する

開発環境

下記環境を使用しています。

[] Flutter (Channel stable, 1.22.2, on Mac OS X 10.15.6 19G2021, locale ja-JP)
 
[] Android toolchain - develop for Android devices (Android SDK version 30.0.0)
[] Xcode - develop for iOS and macOS (Xcode 11.5)
[] Android Studio (version 4.0)
[] VS Code (version 1.49.1)

graphql_flutter: "3.1.0"

Timeoutの実装

graqhql_flutterにはtimeout用のapiがありません。 しかしながら、graphql_flutterのquerymutateメソッドの返却値はFutureであり、dartのFutureにtimeout設定を行うためのAPIがあります。 そのため、Futureのtimeoutを設定することでgraphql_flutterの各メソッドにtimeoutを設定することができます。

// Futureのtimeoutメソッド
Future<T> timeout(Duration timeLimit, {FutureOr<T> onTimeout()?});

// 実装サンプル
future // Future<T>
  .timeout(const Duration(milliseconds: 3000), onTimeout: () {
      throw TimeoutException('通信に失敗しました。');
  });

timeoutはtimeoutまでの時間とtimeout時の処理を引数としてとります。 上記の実装により、3秒でtimeoutし、timeout時は通信失敗のメッセージを含んだTimeoutExceptionを発生させるようにしています。

上記により、timeout設定を行うことができました。 しかし、このままではクライアントのメソッド呼び出しのたびにtimeout設定を行う必要があるため冗長であり、また記述漏れや記述ミスによる不具合につながります。 以下で、そのような問題に対処する方法を記載します。

実装の改善

GraphQLClientをそのまま使用すると各メソッド処理でtimeoutの記述をする必要があるため、GraphQLClientをラップしたクラスをアプリ用に作成します。 先にコードの全体像を記載します。

import 'dart:async';

import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sample/core/configs/config.dart';

// hooksを使用してGraphQLClient用のproviderを作成しています。
// graphql_flutterのREADME通りvalue notifierを使用して、GraphQLProviderを使用した場合は別途検討が必要かと思います。
// getitなどを使用してdiしている場合は同様の方法で対応できるかと思われます。
final gqlClientProvider = Provider<AppGraphQLClient>((_) => AppGraphQLClient());

class AppGraphQLClient {
  AppGraphQLClient() {
    _gqlClient = GraphQLClient(link: _baseLink, cache: InMemoryCache());
  }

  GraphQLClient _gqlClient;
  final _baseLink = HttpLink(uri: Config.apiBaseUrl());

  // access tokenが一定時間で変わる場合にaccess tokenを入れ替える用のメソッドです。
  // 方法はまだ検討中です。
  Future<void> setAuth({String token}) {
    final authLink = AuthLink(getToken: () async => 'Bearer $token');
    _gqlClient =
        GraphQLClient(link: authLink.concat(_baseLink), cache: InMemoryCache());
  }

  // 本稿のメインです。
  // GraphQLClientのメソッドに直接アクセスするのではなく、
  // 本クラスのメソッド経由でアクセスすることでtimeoutやエラーハンドリングなどの処理を共通化しています。
  Future<QueryResult> query(QueryOptions queryOptions) async {
    final result = await _gqlClient
        .query(queryOptions)
        .timeout(const Duration(milliseconds: 3000), onTimeout: () {
      throw TimeoutException('通信に失敗しました。');
    });

    if (result.hasException) {
      // TODO: Error Handling
    }

    return result;
  }

  // 同上です。
  Future<QueryResult> mutate(MutationOptions mutationOptions) async {
    final result = await _gqlClient
        .mutate(mutationOptions)
        .timeout(const Duration(milliseconds: 3000), onTimeout: () {
      throw TimeoutException('通信に失敗しました。');
    });

    if (result.hasException) {
      // TODO: error handling
    }

    return result;
  }
}

GraphQLClientをラップしたクラスを作成します。 queryやmutateのメソッドを作成し、各メソッド処理からGraphQLClientのquery, mutateにアクセスします。 その際にtimeoutを設定することで、timeout時のエラーハンドリング を共通化できます。

また、graphQLはRESTとは異なりServer側での意図したエラー(401や403など)は200で返却した上で errors に含まれます。 その処理も可能な限り共通化した方が良いかと思いますので、同様に各メソッドの処理に含めています。 TODOの記載に済ませていますが、具体的にはerrorsのstatus codeからアプリで定義した各エラーにキャストする、 extensionsに含まれた内容からアプリで定義したAPI Server用の型にパースした上でThrowするなどがあげられるかと思います。

まとめ

本稿ではgraphql_flutterでのtimeout設定方法に関して記載しました。 また、timeout処理の共通化と合わせてエラーハンドリング部分の共通化に関しても言及しました。 私はgraphQL, Flutterともに学習期間がまだまだ短く、知らないことも多いためもっと良い方法をご存知の方がいましたらコメントいただけると嬉しいです。 本稿の情報がどなたかのお役に立てたら幸いです。

参考