パルカワ2

最近はFlutterをやっています

Cirrus CI使い始めた

cirrus-ci.org

最近OSSでFlutterでWebViewを表示するプラグインを書いていて、AndroidエミュレータiOSシミュレータを利用してそれぞれのプラットフォームでテスト(flutter drive)を動かしたいという欲求があった。 最初は、iOSの実装だけやっていたので使ったことのあるBitriseを利用して動かしていたのだけど、Androidのテストを動かそうとしたら無料では2種類のワークフローを動かすことはできないっぽいのと1ビルド10分制限が厳しいなという感じだった。

業務で利用しているCircleCIは、iOSをビルドするのにお金を払う必要があり、お金払うかなぁと思っていたところ、FlutterがCirrus CIを使っていたので、僕も使ってみることにした。

シンプルでほしい機能があり制限も緩いので、ほんまに無料でええんか?という感じ

tmux 3.0a にする

tmux 2.8からアップデートでもするか〜と思ってした。 どうせ動かなくなるだろと思って雑にupgradeしたら動かなくなった。ガッハッハ

-fg みたいなのが廃止されて、-styleになったらしい

/Users/hisaichi5518/.tmux.conf:11: invalid option: pane-active-border-fg   [0/0]
/Users/hisaichi5518/.tmux.conf:12: invalid option: pane-active-border-bg
/Users/hisaichi5518/.tmux.conf:14: invalid option: mode-bg
/Users/hisaichi5518/.tmux.conf:15: invalid option: mode-fg
/Users/hisaichi5518/.tmux.conf:16: invalid option: window-status-bg
/Users/hisaichi5518/.tmux.conf:17: invalid option: window-status-fg
/Users/hisaichi5518/.tmux.conf:18: invalid option: window-status-current-bg
/Users/hisaichi5518/.tmux.conf:19: invalid option: window-status-current-fg
/Users/hisaichi5518/.tmux.conf:20: invalid option: window-status-current-attr
/Users/hisaichi5518/.tmux.conf:24: invalid option: pane-border-fg
/Users/hisaichi5518/.tmux.conf:25: invalid option: pane-active-border-fg

以下を見ながらなおした http://man7.org/linux/man-pages/man1/tmux.1.html http://man7.org/linux/man-pages/man1/tmux.1.html#STYLES

Bitriseでflutter driveを実行する

プロジェクトを作ってシミュレーターを開いて、flutter driveすればよい

---
format_version: '8'
default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
project_type: flutter
trigger_map:
- push_branch: "*"
  workflow: primary
- pull_request_source_branch: "*"
  workflow: primary
workflows:
  primary:
    steps:
    - activate-ssh-key@4.0.5:
        run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'
    - git-clone@4.0.18: {}
    - flutter-installer@0.11.0:
        inputs:
        - is_update: 'false'
    - cache-pull@2.1.4: {}
    - flutter-analyze@0.1.2:
        inputs:
        - project_location: "$BITRISE_FLUTTER_PROJECT_LOCATION"
    - script@1.1.6:
        inputs:
        - content: |-
            #!/usr/bin/env bash
            # fail if any commands fails
            set -e
            # debug log
            set -x

            open -a Simulator
            make test
        title: Run flutter drive
    - deploy-to-bitrise-io@1.9.6: {}
    - cache-push@2.2.3: {}
app:
  envs:
  - opts:
      is_expand: false
    BITRISE_FLUTTER_PROJECT_LOCATION: example
  - opts:
      is_expand: false
    BITRISE_PROJECT_PATH: example/ios/Runner.xcworkspace
  - opts:
      is_expand: false
    BITRISE_SCHEME: Runner
  - opts:
      is_expand: false
    BITRISE_EXPORT_METHOD: ad-hoc

flutter_inappwebviewに関する知見

ほしい機能がなかったからwebview_flutterをやめて、flutter_inappwebviewを使うようにした。 flutter_inappwebviewは機能がモリモリでモリモリすぎるという印象だけど、自分でモリモリ作るよりはいいだろという感じ。ちなみにflutter_inappwebviewはflutter_inappbrowerから名前が変わっている。

pub.dev

いくつか困ったことがあったので書いておく。

AndroidでcallHandlerがないと言われる

[InAppWebview]: window.flutter_inappwebview.callHandler is not a function · Issue #218 · pichillilorenzo/flutter_inappwebview · GitHub

原因はしっかり調べていないけど、AndroidだとcallHandlerがないと言われる。

private APIは使えるので、しょうがないのでそっちを使うようにした。ちなみにiOSではprivate APIのほうがないのでprivate APIの実装だけだとエラーになる。

const message = "value";
if (window.flutter_inappwebview.callHandler) {
  window.flutter_inappwebview.callHandler('handlerName', message);
} else {
  window.flutter_inappwebview._callHandler('handlerName', setTimeout(function(){}), JSON.stringify([message]));
}

iOSで hot restartするとエラーになる

flutter_inappwebview 2.1.0+1 で再現する。 InAppWebViewがremoveFromSuperviewされたタイミングでsetMethodCallHandler(nil)しているのが原因

MissingPluginException: No implementation found for method loadUrl · Issue #209 · pichillilorenzo/flutter_inappwebview · GitHub

現在のmaster branch(9c7ac0d)では、removeFromSuperviewでのsetMethodCallHandler(nil)がなくなっているので再現しない。

iOSで 9c7ac0d を使うとPromiseが使えない

TypeError: Promise._immediateFn is not a function. · Issue #288 · pichillilorenzo/flutter_inappwebview · GitHub

原因はこれ JavaScript - Object definition available before code execution on Safari - Stack Overflow

まとめ

なおしていくぞ

DartLintで独自のRuleを作って使いたい

Dartでlintといえば dartanalyzer だけど、custom lint rule に対応していない。 (dartanalyzerは中でdart-lang/linterを利用している) github.com

なので、独自のlint ruleを動かす君を作る。 独自のコマンドゆえにIDE上のDart AnalyticsではLintエラーが表示されないので注意が必要。ただCIで動いてれば漏れてても気付けるのでそれでいいなって思う。

import 'package:linter/src/analyzer.dart';
import 'package:linter/src/cli.dart' as cli;

import 'rule/avoid_utc_datetime.dart';

Future main(List<String> args) async {
  Analyzer.facade.register(AvoidUtcDateTime());
  await cli.run(args);
}

以下のように動かす。今回はDateTime.utc()とかを使っているとエラーが起きるようなRuleを書いた。

pub run tool/linter/main.dart . --rules=avoid_utc_datetime --packages=.packages

Rule自体は、ここにあるRuleを見ながら書くとよい。

linter/lib/src/rules at master · dart-lang/linter · GitHub

Dartのhttp packageでは物足りない部分を補う

http packageを使っている。 pub.dev

まあ、便利なんだけどtimeoutとcookieが対応していない。またUserAgentは常に指定する必要がある。 なので、それらに対応するには別packageを使うか自分で頑張る他ない。

諸々対応しているHTTP Clientとしてdioがあるが、自前で頑張っている+中国語でドキュメントが書かれていたりするので、今回は自分で頑張ることにした。

import 'dart:io';

import 'package:cookie_jar/cookie_jar.dart';
import 'package:http/http.dart' as http;

class HttpClient extends http.BaseClient {
  static const userAgent =
      "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36";

  final http.Client _inner = http.Client();
  final CookieJar _cookieJar = CookieJar();
  final Duration _timeout = Duration(seconds: 10);

  @override
  Future<http.StreamedResponse> send(http.BaseRequest request) async {
    print("${request.method} ${request.url}");

    // set User-Agent
    request.headers[HttpHeaders.userAgentHeader] = userAgent;

    final cookies = _cookieJar.loadForRequest(request.url);
    _removeExpiredCookies(cookies);

    String cookie = _getCookies(cookies);
    if (cookie.isNotEmpty) {
      request.headers[HttpHeaders.cookieHeader] = cookie;
    }

    final response = await _inner.send(request).timeout(_timeout);

    if (response != null && response.headers != null) {
      final cookieHeader = response.headers[HttpHeaders.setCookieHeader];
      _saveCookies(response.request.url, cookieHeader);
    }

    return response;
  }

  void _removeExpiredCookies(List<Cookie> cookies) {
    cookies.removeWhere((cookie) {
      if (cookie.expires != null) {
        return cookie.expires.isBefore(DateTime.now());
      }
      return false;
    });
  }

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

  void _saveCookies(Uri uri, String cookieHeader) {
    if (cookieHeader == null || cookieHeader.isEmpty) {
      return;
    }
    // set-cookieが複数あった場合は、","でjoinして返ってくるので分割する必要がある
    final cookies = cookieHeader.split(",");
    if (cookies.isEmpty) {
      return;
    }
    _cookieJar.saveFromResponse(
      uri,
      cookies.map((cookie) => Cookie.fromSetCookieValue(cookie)).toList(),
    );
  }
}

という感じでやっている。 また何度もリクエストするようなコードを書いていると Connection closed before full header was received というエラーが出ることがあり、それは1秒ほどdelayすると起きない。

final response1 = await http.get(...);
...
await Future<void>.delayed(Duration(seconds: 1));
...
// delayをしないとエラーになるが、delayするとエラーが起きない
final response2 = await http.get(...);

Firestoreエミュレータで listDocuments + orderBy + pageTokenを使うと gRPC Error (2, null) が出る

Package google.firestore.v1  |  Google Cloud
FirestoreのRPC APIを使っていて、Firestoreエミュレータを使って開発している。

以下のようにListDocumentsRequestで、orderByとpageTokenがある場合に「gRPC Error (2, null)」というエラーが返ってくる。エラーメッセージがnull

// Dartです
final request = ListDocumentsRequest()
  ..parent = "..."
  ..collectionId = "notifications"
  ..orderBy = "createdAt desc"
  ..pageToken = "..."
  ..pageSize = 20;

 final response = await firestoreClient.listDocuments(request);

orderByを外すと値は返ってくるようになるのだけど、当然ソートされておらず値はバラバラなので使えない。
RunQueryを使うと想定通りに動くのでそうやるようにした。
Package google.firestore.v1  |  Google Cloud

実際のFirestoreでは試していない。