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
を使う、というのがちゃんとした方法のようです。
こちらで詳しく解説されています。
以上、画面遷移の方法でした!
FlutterでbottomNavigationBarを実装する
ネイティブアプリでよく見る、画面下部にあるナビゲーションバーのことを、ボトムナビゲーションバーというそうです!
ナビゲーションバーだとアンドロイドで言うと「◀」「●」「■」が並ぶエリアと呼び方がかぶってしまうので、「ボトムナビゲーションバー」が呼び名なのだとか。
ScaffoldでbottomNavigationBar
を指定します。
class _MyHomePageState extends State<MyHomePage> { int _currentIndex = 0; // currentIndexにデフォルト値を与えないとコンパイルエラーになる @override Widget build(BuildContext context){ return Scaffold( appBar: AppBar( title: Text('Title'), ), body: Center( child: Text('bottomNavigationBar test') ), bottomNavigationBar: BottomNavigationBar( currentIndex: _currentIndex, items: [ new BottomNavigationBarItem( icon: new Icon(Icons.home), title: Text("Home") ), new BottomNavigationBarItem( icon: new Icon(Icons.settings), title: Text("Settings") ), ], onTap: (int index) { print(index); // デバッグ用にprintしてみる }, ), ); } }
こんな感じで、実装はシンプル。
特徴的なのは、onTapがBottonNavigationBarItem(アイコン)側ではなくて、BottomNavigationBar側で準備されていること。
onTapは引数としてintを持っているので、このintを元に画面遷移を実装するようです!
Navigatorクラスを使って実装していくようですが、続きはまた明日やっていきます!
Gitでユーザ名とかパスワードを求められた時
新しいパソコンでGitの設定を間違ったり忘れたりしていて、少し戸惑ったのでメモ。
- git pullでPermission denied
$ git pull git@github.com: Permission denied (publickey). fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists.
SSH keyの登録を忘れていました😇
GitHubでssh接続する手順~公開鍵・秘密鍵の生成から~ - Qiitaを参考に公開鍵・秘密鍵のセットを作って登録します。
- git pushでユーザ名とパスワードを聞かれる
remote urlでhttpsを使用していました😇
sshを使って接続します。
git remote -v origin https://github.com/makicamel/test.git (fetch) origin https://github.com/makicamel/test.git (push)
GitHubのリポジトリの「Clone or download」ボタンを押した時に「Clone with SSH」と表示されていたら「Use SSH」をクリック、表示されたgit:github.com
から始まるアドレスをコピー。
git remote set-url origin git@github.com:makicamel/test.git git remote -v origin git@github.com:makicamel/test.git (fetch) origin git@github.com:makicamel/test.git (push)
- git pullでパスフレーズを聞かれる
秘密鍵にパスフレーズを設定していました😇
ssh-agentに秘密鍵を登録します。
ssh-add ~/.ssh/id_rsa
- 参考リンク
ssh-agentを利用して、安全にSSH認証を行う - Qiita
それでは快適なGitライフを!
Androidの仮装端末からインターネットに接続できない時の対処法
今日は(も)小ネタ。
Androidアプリの開発中、たまによくある(らしい)Wifiがつながらない事案。
そんな時はapp/src/main/AndroidManifest.xml
に以下を記述します。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.hoge_app"> <uses-permission android:name="android.permission.INTERNET" /> <!-- Added -->
アプリのインターネットへの接続を行うことができるようになる設定と教えていただきました!
ただ今回の原因はアプリではなく仮装端末自体がWifiにつながっておらず、
理由は不明で仮装端末の再起動で直りました😅
Androidの実機の際はWifiは端末(実機)依存なのだそうですが、
仮装端末だと起こりがちな事象だというお話を伺いました!
flutter clean
& エミュレータの再起動を習慣にしようと改めて思った事案でした!
間違ってremoteにpushしたファイルを削除したい時
誤ってパスワードなどの秘密情報が含まれたファイルをremoteにpushしてしまった時。
普通にgit rm hoge
しただけだと当然ながら履歴が残ってしまうので、履歴ごと改変したい時のやり方です。
リモートの履歴を改変することになるので、チーム開発の際は混乱が起きない様ご注意ください。
前提条件
こんな履歴があったとします。
$ git log --oneline fae133a (HEAD -> master, origin/master) 3rd commit. 3ee314f 2nd commit. a3af1e4 Initial commit.
よく見ると、パスワードが書かれたsecret.file
をcommit(&push)しています!
$ git log -p -2 commit fae133a633e21e7feb94c3994c276ef86d41210c (HEAD -> master, origin/master) Author: makicamel Date: Tue Mar 19 19:00:30 2019 +0900 3rd commit. # (略) commit 3ee314fc2f06bb7118229dc25ece3b54af889fb4 Author: makicamel Date: Tue Mar 19 19:00:00 2019 +0900 2nd commit. diff --git a/secret.file b/secret.file index e69de29..9c3f6c8 100644 --- a/secret.file +++ b/secret.file @@ -0,0 +1 @@ +PASSWORD # <= これを消したい!
git rm secret.file
をするとファイルは消せますが、履歴は消せません。
履歴自体を消したいので、commitからやり直します。
commitを取り消し
まずはローカルで変更を修正。
git log
で戻りたい履歴のコミットのハッシュ値を確認、そこまで戻ります。
git reset
のオプションはこちらに詳しいですが--hard
、--mixed
、--soft
とあります。
$ git log --oneline fae133a (HEAD -> master, origin/master) 3rd commit. 3ee314f 2nd commit. # <= ここでsecret.fileを追加した a3af1e4 Initial commit. # <= ここに戻れればOK
今回はファイルの変更はそのまま、コミットだけ取り消したいので--soft
します。
$ git reset a3af --soft $ git log --oneline a3af1e4 (HEAD -> master) Initial commit.
ファイルをgit管理対象から外す
.gitignore
に外したいファイルを追記。
$ echo "secret.file" > .gitignore
ただしこのままpushしても、gitが管理してくれているままになるので、git rm
します。
git rm
だとファイルごと削除されるので、ファイルを残すよう--cached
オプションをつけて実行。
$ git rm --cached secret.file
remoteにpush
あとはremoteにpushするだけ!
リモートとローカルで不整合が起きるので、-f
オプションでpushします。
$ git push -f
無事履歴からファイルを削除することができました!