パルカワ2

PerlとRubyとイチャラブ

Flutter + Android Studioで Logcatタブが出ない

会社のPCでは出るけど、家のPCでは出ないのなんでって思ってたけど、設定してなかったみたい。

02:24    Frameworks Detected
            Android framework is detected.
            Configure

Event Logにこういうのが出ているので、Configureを押したら設定できる。

このあたりも確認した。

get_it を使っていたけどやめた

get_it | Dart Package を使っていたがやめた。

// 登録さえすば
GetIt.instance.registerSingleton<Logger>(Logger());

// どのクラスでもLoggerを参照できる
class HogeBloc {
  final Logger logger = GetIt.instance.get<Logger>();
}

便利なんだけど次の問題があると思った。

  • クラスがなんの型に依存しているのか外からわからない
  • GetIt の instance に依存することになり、テストでも依存する
    • BuildContext に依存したくないからモデルの中でProviderを利用していないのだけど、代わりにGetItのインスタンスに依存することになる
    • FlutterなどのPlatformには依存しないので考えすぎかも
  • provider と共存させる場合、どっちになにを置くか両方に必要なのか考える必要がある

今回の場合、providerも使っているので、providerで値を受け取って愚直にDIをすると解決する。Loggerを使う場所で全部自分でinjectしていくのは正直めんどいがそれは別の方法で解決するといいかなと思う。[どうやって?] providerも使っていると外から何に依存しているのかがわからないとは思うのだけど、それ以外のメリットが大きいので採用している。

Dart で簡単に data class を作りたい

Kotlinではdata classはこのように書ける。

data class User(val name: String, val age: Int)

これは、 ただのクラスではなくて equals() / hashCode() / toString() / copy() が実装されるクラスになる。 なので次のコードはtrueが返る。

User(name: "hisaichi5518", age: 1) == User(name: "hisaichi5518", age: 1) //=> true

Dartでこういうクラスを作ってみて、上記のような比較するとfalseが返ってしまう。

class Hoge {
  final String name;
  final Int age;
  Hoge({this.name, this.age})
}

これは equals が実装されていないからそうなる。つまりKotlinのようなdata classを作るには自前でequalsなどを実装する必要がある。 Dartリポジトリにもイシューとしてあがっている Add data classes · Issue #314 · dart-lang/language · GitHub

この問題を解決するライブラリは次のようなのがある。

これらのライブラリは, Dartのextensionを使わない方法で実装されている。 Dart 2.6から入ったextensionを使ったらもっとスマートな実装になるのでは?と思ったので調べてみたが、結論からいうと無理だった。

@immutable
class Hoge {
  final String name;
  final int age;
  Hoge({this.name, this.age});
}

// このextensionを自動生成してimportしてあげれば動くんじゃないかと考えた
extension HogeDataClass on Hoge {
  bool operator ==(Hoge other) {
    return other is Hoge && name == other.name && age == other.age;
  }

  ...
}

@immutableがついたクラスにextensionで ==toString() を実装すればいいと思ったのだけど、以下のような制限があると知った。

Extensions can’t declare members with the same name as a member declared by ‘Object’. Diagnostic messages | Dart

悲しい!

provider 3.2.0 にアップデートした

APIが4.0.0で変わるようで、builder を create/update に変えるだけで今の所良かった。直感的になって良い変更だなとおもう。

RFC Renaming the parameter ‘builder’ of providers · Issue #259 · rrousselGit/provider · GitHub

ついでに4.0.0-devにするかと思ってやってみたけど、MultiProvider で StreamProvider を使おうとするとエラーになるのでやめた。たぶんこれの影響 Simplify the integration of custom widgets with MultiProvider · Issue #237 · rrousselGit/provider · GitHub

より良いFlutterの画面遷移を考える

Flutterの画面遷移について考えたのでメモ。 現状このようにして遷移している。

Navigator.of(context).push(MaterialPageRoute(
  builder: (_) => HogeScreen(keyword: "..."),
));

このやり方だと settings.name が設定されていないので、FirebaseAnalyticsにログを送るときになんの画面なのか判別できないためログが送られない

settings.name を設定する

FirebaseAnalyticsObserverを利用して画面遷移のログを送信したい場合、settings.nameがnullだとログが送られない。 FirebaseAnalyticsObserver class - observer library - Dart API

ログを送るためには以下のようにsettings.name を設定するか、pushNamed を使う.

Navigator.of(context).push(MaterialPageRoute(
  settings: RouteSettings(name: "HogeScreen"),
  builder: (_) => HogeScreen(keyword: "..."),
))

pushNamed を使う

pushNamed は以下のように使い、画面遷移のコードを何度も書く必要がないというメリットもある。 pushNamed method - Navigator class - widgets library - Dart API

Navigator.of(context).pushNamed("HogeScreen", arguments: "...")

しかし、pushNamedには以下のような問題もあると思った。

  • route name を String で渡しそれを利用してRoute(またはWidget)を作るので、うっかりミスが起きやすい
  • settings.arguments 経由で値を渡すことになるので、型がObjectになり利用するときにcastする必要がありうっかりミスが起きやすい
  • route nameだけでは arguments が必要かどうかわからないので、うっかりミスが起きやすい

人間のうっかりミスが起きやすく、コンパイルエラーではなく実行時エラーになるため最悪気づかないことがある。

route メソッドをつくる

pushNamedは画面遷移のコードを何度も書く必要がないという点では便利だけど、うっかりミスも起きやすいと感じた。 それを防ぐためにはこのようにRouteを生成するメソッドを書いてあげればよいのではと考えた。

extension HogeScreenRoutes on HogeScreen {
  static Route<dynamic> route({@required this.keyword})) {
    return MaterialPageRoute(
      settings: RouteSettings(name: "HogeScreen"),
      builder: (_) => HogeScreen(keyword: "..."),
    )
  }
}
Navigator.of(context).push(HogeScreen.route());

こうすればうっかりタイポなどをしても実行したタイミングでエラーになるのではなくコンパイルのタイミングでわかるので、最高。 ただし、これにもうっかりミスが起きる場所がある。HogeScreenに渡す値が増えたときだ。

class HogeScreen {
  final String keyword;
  final String fuga; // 増えた
  HogeScreen({Key key, @required this.keyword, @reuqired this.fuga});
}

extension HogeScreenRoutes on HogeScreen {
  static Route<dynamic> route({@required String keyword})) {
    return MaterialPageRoute(
      settings: RouteSettings(name: "HogeScreen"),
      builder: (_) => HogeScreen(keyword: keyword), // fugaが渡されていない
    )
  }
}

@required をしていてもコンパイルエラーにならない。lintを動かせばエラーにすることはできる。 複数箇所変更するのが嫌ならコンストラクタからrouteメソッドを自動生成すればいいかなと思ったけどそこまでやらなくていいかなと思ってここで考えるのをやめた。 もっといい方法があるなら知りたい。

追記: こうやれば一応一箇所の変更ですむねって話を同僚としたけど、やりすぎ感あるのでやめておいた。

class HogeScreenArguments {
  final String keyword;
  final String fuga;
  HogeScreenArguments({Key key, @required this.keyword, @required this.fuga});
}

class HogeScreen {
  final HogeScreenArguments arguments;
  HogeScreen({Key key, @required this.arguments});
}

extension HogeScreenRoutes on HogeScreen {
  static Route<dynamic> route({@required HogeScreenArguments arguments})) {
    return MaterialPageRoute(
      settings: RouteSettings(name: "HogeScreen"),
      builder: (_) => HogeScreen(arguments: arguments),
    )
  }
}

カリギュラを見た

妹が死んでキレた皇帝の話です。
顔の良い男たちが出ているからかお客さんのほとんどが女性だった…すごい…
もっとめちゃくちゃな暴れん坊か?と思ってたけど、お笑い要素もあってちょっと意外だったんだけど、インタビューでも「みんなが思ってるカリギュラではない」って言ってた。

www.youtube.com

FlutterでWebViewをがんばる

Flutter + WebView

最近FlutterのWebViewを触るようなことをやっている。 WebViewのプラグインにはいくつか実装があるが、大きく分けるとするとPlatformViewを使わないか使うかによって分けれる。

PlatformView を使わないか使うか

PlatformViewを使わないとネイティブのViewをFlutterWidgetの上に被せて表示する実装になるので、FlutterのWidget(SnackBarなど)をWebViewの上にかぶせるなどができない。また大きさの変更もContainerなどのWidgetを利用して変更できない。launchするときにrectを指定すれば大きさの変更はできる。さらに1画面に1個のWebViewしか利用できない。このように制限はあるが、PlatformViewを経由しないため、それなりに動く。(キーボードが出てきてもViewの大きさが変わらない問題(仕様?)はある)

PlatformViewを使うとFlutterのWidgetとして扱えるので、WebViewの上にSnackBarを出したりWebViewの大きさをContainerに囲んで変更したり複数のWebViewを表示したり制限なく使える。しかし、後述するがAndroidのキーボードまわりでバグにぶちあたる。

PlatformView を使う

今回の要件的には上記のような制限を受けたくないので、PlatformViewを利用する選択をした。 PlatformViewを利用するプラグインは探せばたぶん無限にあるけど、この2択っぽい。

webview_flutterに関しては、公式のプラグインで安心感がある。利用者も多く大体の問題はイシューになっている。ただあまり実装は進んでおらず、onPageStartedなどのコールバックもほとんど実装されていないようだった。(プルリクはあるが… #1389, #1788

flutter_inappbrowser のほうはCookieManagerやコールバックもある。ただ、リリースはされてないが2.0.0でAPIが大きく変わりそう。また、大きな実装だなという印象で、InAppWebView.javaの実装を見るとJSがめっちゃ書かれていて、それって我々に必要なのか?みたいな気持ちになる。

今回は、公式のwebview_flutterで確認することにする。

webview_flutterを使ってぶちあたった壁

webview_flutterを利用する選択をしたはいいが、触っていたところAndroidのキーボードまわりで結構バグっていることに気づいた。全部イシューになっているけど、WebView起因というよりはPlatformViewが原因なのが多い気がしていて、シュッとは直せなさそうだと思った。 自分が遭遇しただけでも以下がある。

webview_flutter Keyboard persists after tapping outside text field · Issue #36478 · flutter/flutter · GitHub

TextFieldをタップしてキーボードを表示したあとTextField以外をタップしてもキーボードが出っぱなし。

WebView’s text selection dialog is not responding to touch events · Issue #24585 · flutter/flutter · GitHub

TextFieldをタップしてもコピーなどの項目が表示されない。 Flutter 1.9以降で動かないようになったらしく、なおしてるところとのこと。 https://github.com/flutter/flutter/issues/24585#issuecomment-513950478

WebView’s text selection handles are not showing on Android · Issue #24584 · flutter/flutter · GitHub

TextFieldだけではなくウェブページのテキストをコピペしようと長押ししても動かない。 そもそもLongTapが無効化されている。 https://github.com/flutter/plugins/blob/f31d16a6ca0c4bd6849cff925a00b6823973696b/packages/webview_flutter/lib/src/webview_android.dart#L31

webview_flutter Keyboard suggestions can be lost · Issue #37989 · flutter/flutter · GitHub

日本語を打つと強制的に確定されてしまい、サジェストが出ずキーボードが英字に変わってしまう。 Chromiumが原因かも?と言っている。ネイティブアプリでWebViewを使ってみてもならないので違うような気もするけどよくわからず。

サイズ変更が原因らしいので、サイズを変更しなければ起きない。 FlutterがつくるAndroidアプリは、キーボードを出すとViewの大きさを変える設定になっているので、それをやめればよい。AndroidManifest.xmlandroid:windowSoftInputMode="adjustSize"android:windowSoftInputMode="adjustPan"に変える。

ただ、この設定をするとFlutterで作った画面すべてに影響し(つまりWebViewを利用していない画面でキーボードが出てもViewのサイズは変更されない)、Viewの大きさが変わらないとキーボードを出した状態で一番下までスクロールできなくなるため、あまり現実的な手段ではないと思う。

この問題は、flutter_inappbrowserだと実装が違いそうなので、動かして確認したところそもそも文字がなにも入力できなかったので確認できなかった………。

webview_flutter Soft keyboard not appearing on Q · Issue #38375 · flutter/flutter · GitHub

Android Qでキーボードが出なかった。beta channelだとなおっているのを確認した。

Text input on Q platform views results in log spam · Issue #40716 · flutter/flutter · GitHub

#38375をなおした影響でAndroid Qでログが出まくる。 Q未満は出ないので、Q未満でデバッグすることにした。

autofillが効かない?

動かないか確認できていないけど、ログが出てる。

W/cr_AwAutofillManager( 2953): WebView autofill is disabled because WebView isn't created with activity context.

ただPlatformViewは、VirtualDisplayを使うことで実現していてそのためには android.app.Presentation を使う必要があるっぽくActivityのContextを利用するのは無理なのではと自己完結した。

まとめ

FlutterでWebViewを使いたいが、Androidのキーボードまわりで困っている。数カ月後にはなおってるかもしれないし、なおってないかもしれない。 せっかくFlutterを利用するのであれば、ネイティブでAndroid/iOSを書きたくないという思いがあるが、ネイティブで書くのがよいという決断をするかもしれない。

ちなみにiOSでは結構まともに動いていて「AndroidとFlutterは同じ会社が作ってるのでは…?」と思うなどした(大変そう)