analysis_options.yamlをカスタマイズする(2)
昨日の続きで、analysis_options.yaml
の項目たちの説明です。
昨日の復習も含めて書いてゆきます。
analyzer: strong-mode: implicit-casts: false implicit-dynamic: false
動的型付け・型のキャストを許可する項目です。どちらもデフォルトはtrue
。
動的型付け・型のキャストを禁止したい時のみfalse
で指定します。
implicit-casts: false
暗黙的な型変換を許可しない設定。
Object o = 'hoge'; String s = o;
Object
クラスのインスタンスo
をString
クラスの変数s
に代入しようとすると文字列をStringに入れるのでコンパイルは通るのですが、implicit-casts: false
に反して暗黙的な型変換が行われるのでアラートが発生するようになります。
アラートを避けるには、型変換が行われないよう変数の型を揃える必要があります。
String o = 'hoge'; String s = o;
implicit-dynamic: false
動的型付けを許可しない設定。
casts
は型変換禁止なので、Object
→String
となると静的型付だけどアラートが出るし、dynamic
は動的型付け禁止なので、型をつけていないとアラートが出るという感じですね。
抜けがちなのは配列やオブジェクトの時かなと思いました。それぞれ要素の型を指定する必要があります。
// 1. hogeはListだけど、Listの中身を指定していないのでアラートが出る final List hoge = ['hoge']; // 1. Listの中身をジェネリクスで指定するとアラートが解消される final List<String> hoge = ['hoge']; // 2. MapのListだと指定しているけど、Mapの中身を指定していないので // Missing type arguments for map literalアラートが出る final List<Map> hoge = [{'fuga': 'piyo'}]; // 2. Mapの要素の型を再帰的に指定するとアラートが解消される final List<Map<String, String>> hoge = [{'fuga': 'piyo'}]; // 3. 明示的にvarを指定するとアラートは出ない var hoge = ['hoge']; // ちなみにそもそもvarと型は共存できない(analysis_options.yamlによらず) // これはコンパイルエラー // "Variables can't be declared using both 'var' and a type name." // var List hoge = ['hoge'];
関数呼び出しの際も、返り値のクラスを指定する必要があるようです。
たとえば、showDialog
メソッドで返ってくるのはAwesomeDialog
のインスタンスなので指定します。
// アラートが出る showDialog( context: context, builder: (_) { return AwesomeDialog(); } ); // 返り値のクラスを指定するとアラートが出なくなる showDialog<AwesomeDialog>( context: context, builder: (_) { return AwesomeDialog(); } );
以上、動的型付け禁止の項目でした!
Dart DevToolsの使い方
ウィジェットを並べていくFlutterは、ウィジェットのネストが深くなりがち。
どのウィジェットのHeight指定がうまくいってないの…?🤔を確認したくなった時とか、ウィジェットの構造を確認したくなった時に便利なのがDart DevTools
です。
DartDevTools - Getting Started
Android StudioとVS Codeと、それぞれ説明されていますが、わたしはVSCodeをメインに使っているのでVSCodeのやり方を書いていきます。
DevToolsのインストール、立ち上げ
VSCodeからエミュレータを起動(F5)するとこんなメッセージが出てきます(DevToolsがインストールされていない初回のみ)。
Dart DevTools need to be installed with 'pub global activate devtools' to use this feature.
DevToolsを使うにはpub global activate devtools
でインストールしてね、と言われるので、「Activate Dart DevTools」をクリックするとflutter packages get
が走ってDevToolsをActivateしてくれます。
しばらく待つとhttp://127.0.0.1:58696/?hide=debugger&port=57931
のようなアドレスでブラウザが立ち上がり、ウィジェットツリーをブラウザから確認することができます。
DevToolsが立ち上がらなかった時
なんらかの原因でDevToolsが立ち上がらなかった時は、VSCodeから直接コマンドで立ち上げることができます。
$ flutter packages pub global activate devtools
ポート番号がわからなくなった時
URLはhttp://127.0.0.1
で固定ですが、ポート番号は動的に振られてセッションが変わる度(アプリを再起動する度)に切り替わります。
ポート番号がわからなくなった時はVSCodeのステータスバーにある「Dart DevTools」の文字をクリックすると、ブラウザでDevToolsが立ち上がります。
DevToolsの使い方
直感的に使えるUIですが、よく使う機能を少しだけ。
Debug Paint
オンにするとウィジェットの枠線を引いたり、mainAxisAlignmentの向きを表示したりしてくれます。
モック作りではおそらく一番使うところ。Select Widget Mode
オンにすると、ブラウザでウィジェットを選択することでエミュレータのウィジェットも選択することができるようになります。
Chromeのデベロッパーツールで要素を選択できるのに似ているイメージ。Refresh Tree
エミュレータはホットリロードが効きますが、DevToolsはホットリロードが効かないので手動でリロードするためのボタン。
Navigator.push
なども反映されないので画面遷移した時も同様にリフレッシュする必要があります。
注意点としては、stackされているScaffoldが全てツリー表示されるので、違うウィジェットツリーを見ながら「あれ?ない??🤔」となりがちなこと。
ありがちなミスだと思うのでお気をつけて、快適なFlutter開発をお楽しみください!🎉
analysis_option.yamlをカスタマイズする
VSCodeやAndroidStudioを使っていると、Dart Analysis Serverが構文エラーをチェックしてくれます。非常に便利な機能ですが、これをカスタマイズできるのがanalysis_option.yaml
。
Customize Static Analysis - Dart
pubspec.yaml
と同じディレクトリに置いておくだけでAnalyserをカスタマイズすることができます。
ちなみに、analysis_option.yaml
の冒頭でinclude
することもできるそうで、ベースとなるルールをincludeして、それをプロジェクトごとにカスタマイズするという使い方も便利そうです。
include: package:pedantic/analysis_options.yaml
上述のDartのサイトに詳しく解説があるのですが、理解の為に少しずつ訳していこうと思います。
今日の項目はtype checks
。
Dartは動的型付言語ですが、より厳しくチェックをしたい場合、静的型付でないとエラーを出すことができます。
analyzer: strong-mode: implicit-casts: false implicit-dynamic: false
implicit
は「暗黙的な」という意味だそう。
つまり、ひとことでいうと、暗黙的な型変換を許可しないよ、という設定。
implicit-casts
とimplicit-dynamic
は別々に設定することができます。
どちらもデフォルトはtrue
。
implicit-casts
値がfalseの場合、暗黙的に特定の型にキャストしません。
公式で例示されているのは下記のコード。
Object o = 'hoge'; String s = o; // Implicit downcast
Object o
に入っているのはhoge
という文字列なので、String
の変数であるs
にo
を入れてもDartのコンパイラは許してくれますが、implicit-casts
をfalseにしていると暗黙的な型変換を行うと、エラーが表示されるようになります(エラーは表示のみで、コンパイラは通ります)。
implicit-dynamic
値がfalseの場合、静的型を決定できないときに動的型を選択しません。
これはちょっと具体例が理解できていないので、続きはまた明日書きます!
FlutterでDialogの表示を変更できなかったお話
今日はFlutterのダイアログのお話。
ダイアログの中にテキストフィールドを準備して、バリデーション違反があったらエラーメッセージを追加したいということがありました。
ダイアログの表示自体はSimpleDialog
やAlertDialog
を使えばできるのですが、表示の変更ができずにハマりました。
以下がコード例。
簡単に、AppBar
にあるアイコンをタップするとダイアログが表示されて、ダイアログ内のボタンをタップすると表示をトグルするようなコードにしています。
class _AwesomePageState extends State<AwesomePage> { bool _isAwesome; String _awesomeText; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Everyday Awesome'), actions: <Widget>[ PopupMenuButton( onSelected: (String value) { showDialog( context: context, builder: (BuildContext context) => SimpleDialog( title: const Text('It says'), children: <Widget>[ const Text('Flutter is awesome.'), RaisedButton( child: const Text('u a Awesome!'), onPressed: () { setState(() { _isAwesome = !_isAwesome; _awesomeText = _isAwesome ? 'u a Awesome!' : 'please tap Awesome..'; }); }, ), ], ), ); }, itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[ const PopupMenuItem(child: const Text('A'), value: 'a'), ], ) ], ), body: Center( child: const Text('Awesome'), ), ); } }
ボタンをタップする度にonPressed
の中でsetState()
して_awesomeText
が変更されて「u a Awesome!」となるはずですが、何度タップしても変更されません。
ボタンだけでなく、タイトルやテキストを変更しようとしても同じ。
おかしいな、と思ってSimpleDialog
のコードを読んでみると、理由がわかりました。
class SimpleDialog extends StatelessWidget { // ...(略) }
StatelessWidget!!
setState()
しても意味ないはずでした!
念の為AlertDialog
やDialog
クラスを見てみても、どちらも同じくStatelessWidget
。
ガイドラインをそう読み込んだわけではないのですが、Material Designガイドラインを読むに、ダイアログはあくまでも警告・エラーを表示したり、選択可能なリストを表示したりといったすごくシンプルなUIを目的としているみたい。
じゃあどうすれば…というと、FlutterによるFull-Screen Dialogの実装例があるのを先輩に教えて頂きました。
ちょっと長いですが、要は、StatefulWidget
を作って、SimpleDialog
の代わりに呼び出す、ということのよう。
実装するとこんな感じになりました。
class _AwesomePageState extends State<AwesomePage> { bool _isAwesome; String _awesomeText; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Everyday Awesome'), actions: <Widget>[ PopupMenuButton( onSelected: (String value) { showDialog( context: context, builder: (_) { return AwesomeDialog(); }, ); }, itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[ const PopupMenuItem(child: const Text('A'), value: 'あ'), ], ) ], ), body: Center( child: const Text('Awesome'), ), ); } } class AwesomeDialog extends StatefulWidget { @override _AwesomeDialogState createState() => _AwesomeDialogState(); } class _AwesomeDialogState extends State<AwesomeDialog> { bool _isAwesome; String _awesomeText; @override void initState() { super.initState(); _isAwesome = false; _awesomeText = 'please tap Awesome..'; } Widget _buildAwesomeButton() { return RaisedButton( child: Text(_awesomeText), onPressed: () { setState(() { _isAwesome = !_isAwesome; _awesomeText = _isAwesome ? 'u a Awesome!' : 'please tap Awesome..'; }); }, ); } @override Widget build(BuildContext context) { return SimpleDialog( title: const Text('It says'), children: <Widget>[ const Text('Flutter is awesome.'), _buildAwesomeButton(), ], ); } }
showDialog
関数のbuilder
にSimpleDialog
ではなく、AwesomeDialog
を渡しています。
AwesomeDialog
も返すウィジェットはSimpleDialog
ですが、stateを持ったRaisedButton
を外だしにして渡すことで、stateの変更が反映されるようになりました。
では、最初の例でもRaisedButton
を外だししたり、stateにしてしまえばよいのでは?🤔と思ったけれど、これはうまくいかない。
この辺りはまたいずれ調べてみようと思います!
FlutterでEXCEPTION CAUGHT BY RENDERING LIBRARYが出た時の対応方法
久々Flutter。
今日のお題はEXCEPTION CAUGHT BY RENDERING LIBRARY
エラー。
読んで字のごとく、レンダリングに関するエラーです。
例として、こんな画面を考えてみます。
- スクロールを検知して
_currentIndex
を設定 floatingActionButton
タップで_awesomeList
から_currentIndex
の要素を消す
(画面からも_awesomeList
のウィジェットを削除)
class _AwesomeState extends State<Awesome> { int _currentIndex = 0; int _awesomeWidgetHeight = 500; List<Widget> _awesomeList = [ // なんかすごくいいリスト ]; Widget somethingAwesomeSingleScrollView() { // なんかすごくいいリストを使ってすごくいいスクロールビューを返す } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('タイトル'), ), body: NotificationListener<ScrollNotification>( child: somethingAwesomeSingleScrollView(), onNotification: (ScrollNotification scrollInfo) { setState(() { _currentIndex = (scrollInfo.metrics.pixels / _awesomeWidgetHeight).toInt(); }); }, ), floatingActionButton: FloatingActionButton( child: Text('Awesome!'), onPressed: () { setState(() { _awesomeList.removeAt(_currentIndex); }); }, ), ); } }
※ スクロールの度にonNotification
が発火するのでめちゃ重です。
おおむねうまく動作するのですが、_awesomeList
の最後の要素を消そうとすると表題のエラーが出てしまい、うまくいきません。
I/flutter (21487): ══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════════════════════════════════════════════════ I/flutter (10000): The following assertion was thrown during performLayout(): I/flutter (10000): Build scheduled during frame. I/flutter (10000): While the widget tree was being built, laid out, and painted, a new frame was scheduled to rebuild I/flutter (10000): the widget tree. This might be because setState() was called from a layout or paint callback. If a I/flutter (10000): change is needed to the widget tree, it should be applied as the tree is being built. Scheduling a I/flutter (10000): change for the subsequent frame instead results in an interface that lags behind by one frame. If I/flutter (10000): this was done to make your build dependent on a size measured at layout time, consider using a I/flutter (10000): LayoutBuilder, CustomSingleChildLayout, or CustomMultiChildLayout. If, on the other hand, the one I/flutter (10000): frame delay is the desired effect, for example because this is an animation, consider scheduling the I/flutter (10000): frame in a post-frame callback using SchedulerBinding.addPostFrameCallback or using an I/flutter (10000): AnimationController to trigger the animation.
大事なのはここ。
While the widget tree was being built, laid out, and painted, a new frame was scheduled to rebuild the widget tree.
This might be because setState() was called from a layout or paint callback.
直訳すると、こんな感じ。
ウィジェットツリーが構築・レイアウト・描画されている間に、ウィジェットツリーがリビルドされる新しいフレーム(?)がスケジュールされました。
これはsetState()
がレイアウトまたは描画コールバックから呼ばれたせいかもしれません。
※ Flutterのウィジェットツリーをまだ理解していないのですが、こちらに詳しいです。
今回おそらくこんな流れでエラーが発生したのだろうと考えています。
- (前提:
_awesomeList
を消したことでウィジェットツリーの再描画が行われる) _awesomeList
の最後の要素を消したことで画面サイズが変更になる- 画面サイズが変更されたことでスクロールが起きて
onNotification
内のsetState()
が走る _awesomeList
要素削除によるウィジェットツリーの再描画中に新しく再描画処理が走る
※setState()
内の処理ではなく、メッセージ通りsetState()
が走ること自体が原因
なので、onNotification
をこんな風に変更します。
class _AwesomeState extends State<Awesome> { int _currentIndex = 0; int _awesomeWidgetHeight = 500; List _awesomeList = []; Widget somethingAwesomeSingleScrollView() { // なんかすごくいいスクロールビューを返す } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('タイトル'), ), body: NotificationListener<ScrollNotification>( child: somethingAwesomeSingleScrollView(), onNotification: (ScrollNotification scrollInfo) { // calculateCurrentIndexは現在の要素を示すIndex int calculateCurrentIndex = (scrollInfo.metrics.pixels / _awesomeWidgetHeight).toInt(); // _currentIndexが最後の要素でない場合のみ_currentIndexを更新する // _calculateIndexは0始まりなので、_awesomeList.length - 1の必要はない if (_awesomeList.length != calculateIndex) { setState(() { _currentIndex = calculateIndex; }); } }, ), floatingActionButton: FloatingActionButton( child: Text('Awesome!'), onPressed: () { setState(() { _awesomeList.removeAt(_currentIndex); }); }, ), ); } }
ここで注意するのは、if (_awesomeList.length != calculateIndex)
が必要な理由。_awesomeList.removeAt(_currentIndex)
が走った瞬間はまだ画面幅は再描画されていないということ。
_awesomeList.removeAt(_currentIndex);
が走り、_awesomeList.length
が1マイナスになっても、calculateIndex
はあくまでスクロール量(画面高さ)から計算しているので、_awesomeList
の要素が消された瞬間はまだ_awesomeList
があった時のIndexを示しています。
それからスクロール(画面高さを縮小)して、calculateIndex
が_awesomeList
の最後の要素を示すようになった時(=スクロールが完了した時)に初めてsetState()
が呼ばれる、という処理になり、エラーが解消されます。
すごくちょっとしたことなのですが、ネイティブアプリ(フロント?)に慣れないとこの辺りが中々理解できなかったので、まとめました。
ネイティブアプリのレンダリング周りはとても奥が深くて面白そうです!
Dartのカスケード記法(..)はRubyでいうtap
時々見かけるDartの謎な記法、カスケード記法。
どういうものかというと、..shuffle()
みたいに、.
をふたつ重ねる記法です。
void main(){ var hoge = [1,2,3]; print((hoge..shuffle()).toString()); }
Rubyを始めたばかりの頃も思ったのですが、こういうのって読み方がわからなくて調べるのに苦労しますよね…。カスケード記法というそうです。
cascadeとは 小滝、階段状に連続する滝、滝状に垂れたレース(など)、(菊などの)懸崖(けんがい)作り、縦つなぎ - Weblio辞書
つなぐ、という意味っぽい。
メソッドチェーン的なものかな?と思いましたが、なんかちょっと違う。
例にあげたshuffleの定義を覗いてみます。
void shuffle([Random random]);
shuffle
はvoid
で、呼び出し元のオブジェクト(レシーバ)を破壊的に変更して、戻り値は返しません。
なので、カスケード記法を使わないとこうなります。
void main(){ var hoge = [1,2,3]; // print(void)はできないのでコンパイルエラー // print(hoge.shuffle()); // hoge自体を破壊的に変更してからprintすると期待通りの挙動になる hoge.shuffle(); print(hoge); }
ただ、何度も呼び出そうと思うと行数も増えてしまうので、できればメソッドチェーンの形で書きたい。
そんな時のカスケード記法。
void main(){ var hoge = [1,2,3]; print(hoge..shufle()); // 破壊的に変更した後のhogeを返してくれる }
Rubyでいうと、tapでしょうか。
hoge = [1,2,3] p hoge.tap{|item| item.shuffle!} # shuffle!はレシーバ自体を返すので、tapを使う必要は本来ないです
Flutterで画面遷移を実装する
昨日の続き。BottomNavigationBar
の画面遷移を実装します。
といいつついきなりBottomNavigationBar
はさておいて、画面遷移の方法はどうやら大きくわけて3つあるようです。
Navigator
クラスを使ってpush/pop- ウィジェットの切り替えを行う
PageController
を実装してがんばる
みっつめの方法はあまりメジャーじゃないっぽい?(お作法がまだよくわからない)ので、ひとつめとふたつめの方法を見ていきます。
Navigatorクラスを使う
ひとつめ、Navigator
クラスを使うパターン。
main.dart
のmain()
で呼び出しているStatelessWidget
で、いままではこんな感じでホームとなるStatefulWidget
を呼び出していました。
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'MyApp', home: MyHomePage(), ); } }
このhome
を消して、ここでroute
を設定します。
routeをウィジェットの中に直書きするのは少し違和感。
ウィジェットをスタックしていくから仕方ないのでしょうか🤔
ともあれこんな風に書き換えます。
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'MyApp', initialRoute: '/', routes: { '/': (context) => MyHomePage(), '/awesomePage': (context) => AwesomePage(), }, ); } }
routes
プロパティの中に呼び出し名(route)と呼び出すStatefulWidget
名をセットで記載します。
initialRoute
がアプリ起動時に呼び出されるroute。通常はスプラッシュ画面を呼び出すところですね。
今回はMyHomePage
を起動時に呼び出したかったので、initialRoute
である'/'
にMyHomePage
を紐づけました。
RaisedButton
をタップした時なんかに、ここでつけた名前をつかって画面遷移ができます。
RaisedButton( onPressed: () => {Navigator.pushNamed(context, '/awesomePage')}, ),
ウィジェットの切り替えを行う
昨日書いたBottomNavigationBar
に画面遷移を実装したものがこれ。
class _MyHomePageState extends State<MyHomePage> { int _currentIndex = 0; @override Widget build(BuildContext context){ return Scaffold( appBar: AppBar( title: Text('MyHomePage'), ), body: _currentIndex == 0 ? Text('Home') : Text('Settings'), bottomNavigationBar: BottomNavigationBar( currentIndex: 0, onTap: (int index) { setState((){ this._currentIndex = index; }); }, items: [ new BottomNavigationBarItem( icon: new Icon(Icons.home), title: new Text("Home") ), new BottomNavigationBarItem( icon: new Icon(Icons.settings), title: new Text("Setting") ), ], ), ); } }
BottomNavigationBar
をtapするとMyHomePageState
のcurrentIndex
を変更、currentIndex
の変更でbody
を書き換えています。
ここでは単純にText
ウィジェットを表示しているだけですが、Container
やCustomScrollView
などを返します。
とても単純なのですが、これだと画面から非表示になったウィジェットは解放されるのでStatefulWidgetを使う場合には(Stateが解放されて)意図した動きにならない。
ので、Stack
ウィジェットを使って画面を持ちつつ、Offstage
で表示切り替えをして、MaterialApp
やNavigator
を使う、というのがちゃんとした方法のようです。
こちらで詳しく解説されています。
以上、画面遷移の方法でした!