FlutterでCookieを利用する。

# はじめに
本記事では、以下の状況を想定しています。

- FlutterのアプリケーションでAPIを飛ばす時にCookieを利用したい。
- APIで保存したCookieをWebViewでも使いたい。

## 環境
- Flutter 1.20.3
- HTTPクライアント: [dio](https://pub.dev/packages/dio) 3.0.10
- Cookie管理: [CookieJar](https://pub.dev/packages/cookie_jar) 1.0.1

## APIを飛ばす時にCookieを利用したい
既にFlutter(Dart)では、Cookieの仕組みが提供されていますので、それを利用します。 `import 'dart:io';`

CookieJarを利用すると、URIをキーにCookieの値を管理してくれます。
`List<Cookie> loadForRequest(Uri uri)` で保存されたCookieを取得し、
`void saveFromResponse(Uri uri, List<Cookie> cookies)` で、UriをキーにCookieを保存します。

HTTPクライアントでの準備です。
重要な箇所はコメントにて説明します。

```dart
class RestClient {
  factory RestClient() {
    return _restClient ??= RestClient._internal();
  }
  RestClient._internal() {
    // interceptorsに、後述するIntercepterの実装クラスを渡すことで、
    // リクエスト送信時、レスポンス受信時に何か処理を行うことができる。
    _http.interceptors.add(CookieManager(_cookieJar));
  }
  static RestClient _restClient;
  final _http = Dio();
  final _cookieJar = CookieJar();
}

```

```dart
/// Interceptorを実装した独自クラスを定義する。
class CookieManager extends Interceptor {
  CookieManager(this.cookieJar);
  final CookieJar cookieJar;

  /// リクエスト送信時に実行したい処理を記載
  @override
  Future onRequest(RequestOptions options) async {
    final cookies = cookieJar.loadForRequest(options.uri)
      ..removeWhere((cookie) {
        if (cookie.expires != null) {
          // 期限切れなら削除する
          return cookie.expires.isBefore(DateTime.now());
        }
        return false;
      });
    // オブジェクトのcookieをリクエストヘッダ用に文字列に変換する
    final cookie = getCookies(cookies);
    if (cookie.isNotEmpty) {
      options.headers[HttpHeaders.cookieHeader] = cookie;
    }
  }

  /// レスポンス受信時に、Cookieを保存する処理を実行
  @override
  Future onResponse(Response response) async => _saveCookies(response);

  @override
  Future onError(DioError err) async => _saveCookies(err.response);

  void _saveCookies(Response response) {
    if (response == null || response.headers == null) {
      return;
    }
    // HttpHeadersに、レスポンスヘッダのCookieのKeyが定義されている。他にも様々なヘッダのKeyが記載されている。
    final cookies = response.headers[HttpHeaders.setCookieHeader];
    if (cookies == null) {
      return;
    }
    // cookieの保存処理を実行
    cookieJar.saveFromResponse(
      response.request.uri,
      cookies
          .map((str) {
            try {
              // 補足1参照
              final cookie = Cookie.fromSetCookieValue(str)
                ..domain = response.request.uri.host;
              return cookie;
            } on Exception catch (e) {
              logger.warning(e);
              return null;
            }
          })
          .where((element) => element != null)
          .toList(),
    );
  }

  static String getCookies(List<Cookie> cookies) {
    return cookies.map((cookie) => '${cookie.name}=${cookie.value}').join('; ');
  }
}
```

### 補足1
Dart標準に用意されている`Cookie.fromSetCookieValue`は、Cookieの文字列をいい感じにCookieオブジェクトに変換してくれる便利なものです。
しかしながら、Cookieの文字列に `[]`などの記号が含まれていると、`FormatException`をthrowするため、Cookieが保存されず、Cookieオブジェクトに変換されません。

対処方法は２つ考えられます。
1. 投げられる例外を握り潰す。（上記例で記載されている方法)

ひとまず正常なCookieだけは生成されます。

2. Cookieクラスを実装し直す。(参考: https://juejin.im/post/6844903934042046472)

例外が投げられる原因は、要するに、

Dartは [RFC2616](https://tools.ietf.org/html/rfc2616#section-2.2) に準拠した実装になってはいるが、必ずしもサーバ側はそうではないから。だそうです。

暫定対処として、`Cookie.fromSetCookieValue`の定義元を辿ると、ソースコードが見れますので、それをコピペして、必要な処理を書き換える方法になります。

```dart
class MyCookie implements Cookie {
  MyCookie(String name, String value)
      : _name = _validateName(name),
        _value = _validateValue(value),
        httpOnly = true;
  MyCookie.fromSetCookieValue(String value)
      : _name = '',
        _value = '' {
    // Parse the 'set-cookie' header value.
    _parseSetCookieValue(value);
  }

  // _parseSetCookieValueを実装する
}
```

改善のためのIssueは挙げられています。
https://github.com/dart-lang/sdk/issues/42902


## APIで保存したCookieをWebViewでも使いたい。
[webview_flutter](https://pub.dev/packages/webview_flutter) を使用する場合、合わせて以下のパッケージも使用する必要があります。
[webview_cookie_manager](https://pub.dev/packages/webview_cookie_manager)

※これがマージされれば、webview_cookie_managerは必要なくなるかもしれません。
https://github.com/flutter/plugins/pull/2999

```dart
// ...
final _cookieManager = WebviewCookieManager();
// ...
WebView(
  initialUrl: url,
  javascriptMode: JavascriptMode.unrestricted,
  onWebViewCreated: (controller) async {
    // cookies: List<Cookie>

    // ClearしないとCookieが反映されない。
    await _cookieManager.clearCookies();
    if (cookies != null) {
      await _cookieManager.setCookies(cookies);
    }
   // ...
  },
)
```

Cookieがうまく読み込まれない場合は、initialUrlを指定するのではなく、`controller.loadUrl`を使用するといいかもしれません。

```dart
// ...
final _cookieManager = WebviewCookieManager();
// ...
WebView(
  javascriptMode: JavascriptMode.unrestricted,
  onWebViewCreated: (controller) async {
    // cookies: List<Cookie>

    // ClearしないとCookieが反映されない。
    await _cookieManager.clearCookies();
    if (cookies != null) {
      await _cookieManager.setCookies(cookies);
    }
    // Cookieを設定した後に、URLを読み込む
    await controller.loadUrl(url);
   // ...
  },
)
```

[flutter_webview_plugin](https://pub.dev/packages/flutter_webview_plugin) で使用する場合は、文字列にしてheadersに指定するだけです。

```dart
// cookies: List<Cookie>
WebviewScaffold(
  withLocalStorage: true,
  withJavascript: true,
  headers: {if (cookies != null) 'Cookie': cookies.map((cookie) => '${cookie.name}=${cookie.value}').join('; ');},
  // ...
)
```