久々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()
が呼ばれる、という処理になり、エラーが解消されます。
すごくちょっとしたことなのですが、ネイティブアプリ(フロント?)に慣れないとこの辺りが中々理解できなかったので、まとめました。
ネイティブアプリのレンダリング周りはとても奥が深くて面白そうです!