パルカワ2

最近はFlutterをやっています

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では試していない。

ラム肉を焼いた

この記事はつくりおきAdvent Calendar 2019の21日目の記事です。

昨日は id:kazuhi_ra さんの 風邪おき(昼 / 夜) - #つくりおき でした。お大事に…。

クリスマスだしニワトリをまるごと一羽焼こうって話をしていたんですが、ここならなんでもあるでしょと高を括って訪れた肉屋に「今日はないですね」と言われてしまい買えなかったので、代わりにラム肉を買ってきました。次はちゃんと予約します。

f:id:hisaichi5518:20191216204208j:plain

こいつを焼きます。

f:id:hisaichi5518:20191216204330j:plain

塩とブラックペッパーをまぶしました。

f:id:hisaichi5518:20191216204345j:plain

ローズマリーとトマトを適当にのせます。適当に乗せただけなのにすでにうまそう

f:id:hisaichi5518:20191216204622j:plain

にんにくも乗せます。
今回は音楽を聴いて熟成したにんにくを使います。セレブが好きそう(ド偏見)

f:id:hisaichi5518:20191216205119j:plain

一粒がでかい。一玉500円。シンプルに高い。

f:id:hisaichi5518:20191216205532j:plain

オリーブオイルを適当にかけてオーブンにぶち込みます。
オリーブオイルソムリエに肉と相性のいいオリーブオイルをくださいと言ったら出てきたオリーブオイルを使いました。
初めてオリーブオイルを食べ比べたんですが、全然違って面白かった。速水もこみちがオリーブオイルを好きな理由もわかる。

f:id:hisaichi5518:20191216205616j:plain

焼けました。オーブンを開ける前からいい香りがしてテンションぶちあがる。

f:id:hisaichi5518:20191216205840j:plain

皿に乗せます。クレソンのナムルを製造して、パンも焼いて一緒に食べます。
食べた感想ですが、かなりうまい。ラム肉は柔らかくさっぱりしていて食べ終えると次の肉へとドンドン手が伸びる。骨から肉がペリペリと剥がれる感覚は心地良く病みつきになる。オリーブオイルとローズマリーによっていい香りが放たれており、クリスマスを感じることもできる。完全な成功。ありがとうございます。

とはいえ、すべて食べてしまい #つくりおき とは???という感じですが、つくりおきは人それぞれ、おかないのもまた #つくりおき なのだと思うことにしました。

明日は、 id:minemuracoffee さんです。「どうにかなる」いい言葉ですね。
お粗末さまでした。

material/dialog.dart を読む

flutter/dialog.dart at master · flutter/flutter · GitHub

結局角丸+paddingの設定+footerを設定できるようにしたかったので、 RoundedDialog というWidgetを自分で作った。

AlertDialogのボタン下のスペースが空いてしまう!

FlatButtonを使ってAlertDialogのアクションを表現したら、微妙にアクションの下スペースが広い。 debug printを表示するとわかりやすい。

f:id:hisaichi5518:20191210160658p:plainf:id:hisaichi5518:20191210160338p:plain

AlertDialog(
  ...,
  actions: <Widget>[
    FlatButton(
      child: const Text("OK"),
      onPressed: () => ok(context),
    ),
  ],
);

FlatButtonにスペースが含まれているからだった。 次のようにしたら思った通りの表示になった。

f:id:hisaichi5518:20191210160748p:plain f:id:hisaichi5518:20191210160439p:plain

FlatButton(
  materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
  child: const Text("OK"),
  onPressed: () => ok(context),
),