パルカワ2

最近はFlutterをやっています

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),
),

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