【Flutter】さよなら、 Renderflex overflowed - 恐らくもっとも遠回りなUIエラーの解説 -
この記事の目的
この記事の目的はレイアウトを組んだ際に発生するRenderflex overflowed、あのデバッグモードで表示される黄色と黒の縞々と金輪際おさらばする事です

その為に、Widgetがどの様に描画されるのか、どの様にサイズが決まり、どの様なルールに則っているのかを明確にしていきます
結果的に Renderflex overflowed含めたUIエラー全体についての解説となっています
理解しておく背景や情報量が多いので、目的に直接関係のないところは端折って行きますが、非常に参考になるリンクを貼っていくのでそちらをご覧頂ければと思います
TL;DR
恐らく長すぎて誰も最後まで読む事はないと思うので、要点だけまとめておきます
- サイズの計算処理は
RenderオブジェクトのperformLayoutメソッド に書かれている - サイズは親から渡される
BoxConstraintsとperformLayoutに記述された内容の組み合わせで決まる - UIエラーを引き起こす組み合わせは2つ
- 無制約 x 画面サイズ以上になろうとするWidget
- 無制約 x 無制限に大きくなろうとするWidget
-
無制約 x 画面サイズ以上になろうとするWidget の解決方法
- ScrollableなWidgetでラップしてスクロール可能にする
- Expanded, Flexibleでラップして画面サイズに合わせる
-
無制約 x 無制限に大きくなろうとするWidget の解決方法
- 子Widgetにサイズ制約を課す
今回の記事を書くにあたり参考にさせて頂いた情報を登壇や記事で発信してきた先人の方々に改めて感謝いたします
めちゃくちゃありがて〜〜〜と何度唸ったか分かりません
また今回解説するUIエラーとそれに対する解決法をコメントしたサンプルは全て下記レポジトリにまとめています
サイズ計算に至る描画の全体像
Flutterフレームワークの全体像
FlutterでUIを作るとはどういう事でしょうか?我々が見る画面に表示される絵はどうやって描かれるのでしょうか?それを理解する為には軽くFlutterフレームワークの構造について触れていきます
Flutterというフレームワークは実は我々が通常記述するWidget以外にも非常に多くのレイヤーで出来ています

我々が記述するのは氷山の一角であり、水面下では非常に多くのコードが動いている事が分かりますね

大きくはDartで書かれたFramework、CやC++で書かれたEngine、各Platformに応じた言語で書かれたEmbedderの三層で分かれています
この内で描画に関係するレイヤーを抜き出すと下記の様なレイヤーとなります

この中のMaterial、Cupertino、Widgetと書かれたレイヤーが普段よく使うクラスが存在するレイヤーです。描画をする際にはこの層を一層一層上から下に下っていく事になります
Widgetレイヤーのクラスがその下のRenderingレイヤーのAPIを呼び出し、RenderingレイヤーがEngine側にあるdart:uiレイヤーのAPIを呼び出し、この層が実際に画面に描画を行います
ここの層で利用されるのがC++で書かれたSkia(スキーア)というグラフィックスライブラリです
普段我々が実装するWidgetレイヤーは全体の設計図の役割をしており、それに応じてRenderingレイヤーではサイズと位置の計算が行われ、実際に描画を担当するdart:uiレイヤーに伝える事でディスプレイ上にUIを描画してもらいます
今回の主題である「Widgetのサイズ」はこのRenderingレイヤーで行われるわけです
3つのツリー構造
それではこのRenderingレイヤーはどのようなクラスで構成されているのでしょうか?
ここで登場するのが有名なFlutterを構築する3つのツリー構造です

FlutterはWidgetツリー、Elementツリー、Renderツリーの3つのツリー構造で出来ている為、非常に軽快かつ高パフォーマンスな描画処理が行われます
普段実装しているWidgetツリーは主に設計図の役割を担います。Elementツリーを構成するElementは状態などを管理し、Renderツリーを構成するRender Objectは描画を担います。
この描画を司るRender Objectこそが今回の主役であり、Renderingレイヤーを構成するクラスです。Renderツリー自体がRenderingレイヤーとも言えます
Constraints go down, Size go up
ではこのRender Objectで構成されたRenderツリーはどのようにサイズと位置を決定していくのでしょうか?
ここで登場するのが「constraints go down, size go up」という有名なフレーズです。Flutterを学習する過程で一度は聞いた事のあるフレーズかもしれません。でも意味はいまいち分かっていないという方も多いかもしれません、私は少なくとも全然分かっていませんでした。
このフレーズはRenderツリーがどの様にサイズと位置を決定するのかを端的に表した言葉です

まずWidgetはそれぞれに対応したRender Objectを生成し、Renderツリーが構成されます。
そのRenderツリーの各ノードとなるRender Objectはそれぞれ自身の子ノードに渡すサイズ制約constraintsを持っています。「自分自身」ではなく、「自分の子ノード」が取って良いサイズの制約です。
子ノードは親から渡されたそのサイズ制約を元に自分自身のサイズを決め、親クラスに自身が取るサイズを伝えます。
しかし親からconstraintsを渡されてすぐにサイズを決めるわけではありません
この「自身の子ノードにサイズ制約を渡す」という処理はRenderツリーに沿って、末端のノードまで繰り返し、子ノードを持たないノードに到達して初めてサイズ計算が行われます。
その後、末端のノードは自身のサイズを計算し、自身の親に対して自分のサイズを伝えます
親ノードは子から子のサイズを受け取り、それを元に子の位置を決め、今度は自分のサイズを計算、親ノードに自身のサイズを伝えます
こうやってルートノードに到達するまでサイズ計算とその伝播がRenderツリーを逆方向に行われていきます
フレーズの通り、このRenderツリーに沿ってサイズ制約constraintsが末端まで下っていき(Constraints go down)、今度は逆に末端から計算されたサイズがルートに到達するまでツリーを上って伝えられていく(Size go up)わけです
ここについては下記動画が非常に分かりやすく解説頂いています
3つのツリーが構築されるまでの流れ
さて既にだいぶ迷子になっている人がいると思いますが、更に迷子の人を増やしていきましょう
もう少し具体的にどの様にWidgetからElementが生成され、ElementによってRender Obejctが生成され、更にそのRender Objectがサイズ計算を行うのかメソッド単位で見ていきましょう
まずWidgetが生成されるとそのWidgetが継承しているSingleRenderObjectWidgetが持つcreateElementメソッドが呼ばれます
Elementを生成
abstract class SingleChildRenderObjectWidget extends RenderObjectWidget {
const SingleChildRenderObjectWidget({ super.key, this.child });
final Widget? child;
SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this);
}
これによりElementツリーに挿入されるElementが生成されます。この例ではSingleChildRenderObjectWidgetに対応したElementであるSingleChildRenderObjectElementが生成されます
今度はElementが生成されたタイミングでElementが継承しているRenderObjectElementのmountedメソッドが実行され、このメソッド内でElementの生成元であるWidgetが実装するcreateRenderObjectメソッドが実行されます
createRenderObjectの呼び出し
abstract class RenderObjectElement extends Element {
/// Creates an element that uses the given widget as its configuration.
RenderObjectElement(RenderObjectWidget super.widget);
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
assert(() {
_debugDoingBuild = true;
return true;
}());
_renderObject = (widget as RenderObjectWidget).createRenderObject(this);
assert(!_renderObject!.debugDisposed!);
assert(() {
_debugDoingBuild = false;
return true;
}());
assert(() {
_debugUpdateRenderObjectOwner();
return true;
}());
assert(_slot == newSlot);
attachRenderObject(newSlot);
_dirty = false;
}
}
Renderオブジェクトを生成
class ConstrainedBox extends SingleChildRenderObjectWidget {
ConstrainedBox({
super.key,
required this.constraints,
super.child,
}) : assert(constraints != null),
assert(constraints.debugAssertIsValid());
final BoxConstraints constraints;
RenderConstrainedBox createRenderObject(BuildContext context) {
return RenderConstrainedBox(additionalConstraints: constraints);
}
これによりRenderツリーに挿入されるRenderオブジェクトが生成されました。この例ではConstrainedBoxWidgetに対応したRenderConstrainedBoxクラスが生成されました。
これでWidgetからElementが生成され、ElementによってRender Objectが生成され、それぞれのツリーに挿入される事でWidgetツリー、Elementツリー、Renderツリーの3つのツリーがセットされました
3つのツリーが生成された後はいよいよ実際のレンダリング処理が走っていくわけですが、そちらの処理を頭から追い始めるととんでもない事になるので、すっ飛ばして実際のサイズ計算を行う処理だけピックアップしましょう
その処理はズバリRender Objectが持つperformLayoutというメソッドになります
performLayoutメソッド
class RenderConstrainedBox extends RenderProxyBox {
...
void performLayout() {
final BoxConstraints constraints = this.constraints;
if (child != null) {
child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
size = child!.size;
} else {
size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
}
}
...
}
レンダリングパイプラインについてはこちらの動画や記事が参考になるのでより深く知りたい方はご覧になってみてください
Widgetサイズを決める 3 x 5 の組み合わせ
BoxConstraintsとperformLayoutメソッド
さてここまでで親から子に渡されるconstraints、そしてperformLayoutメソッドによってWidgetサイズが決められているという事が分かりました
ではもう少し詳しくそれぞれについて見てみましょう
BoxConstraints
この各Render Objectが持つcostraintsは子ノードが取って良い高さ(height)と幅(width)の最大、最小値を持つ非常にシンプルなクラスです
class BoxConstraints extends Constraints {
/// Creates box constraints with the given constraints.
const BoxConstraints({
this.minWidth = 0.0,
this.maxWidth = double.infinity,
this.minHeight = 0.0,
this.maxHeight = double.infinity,
}) : assert(minWidth != null),
assert(maxWidth != null),
assert(minHeight != null),
assert(maxHeight != null);
}
子に渡されるBoxConstraintsには以下3つのパターンしかありません
1. tightなconstraints
特定のサイズを強要するconstraintsです。幅、もしくは高さのminとmaxが同じ値となっており、子はその特定のサイズしか取る事はできません。
2. looseなconstraints
ある一定の幅のあるconstraintsです。幅、もしくは高さのminとmaxに乖離があり、子はその中で自由にサイズを取る事ができます。
3. unboundedなconstraints もしくは null
幅、もしくは高さのmaxがdouble.infinityなconstraints、もしくはそもそもconstraintsを渡しません。この場合、子はその方向(幅、高さ)についてどんな値を取る事も可能です。SingleChildScrollView, CustomScrollView, Flex, OverflowBoxなどのWidgetがこの制約を子に渡しています
performLayoutメソッド
performLayoutメソッドは前述の通り、Renderオブジェクトが保持し、自身のサイズを決める処理を担います
親から受け取ったconstraintsやその他の引数を元に自身のサイズを計算したり、自分の子のperformLayoutメソッドを呼び出したりと中身はWidget(厳密にはそれぞれのWidgetに対応するRenderオブジェクト)によって異なります
その挙動は大まかには下記の5つのパターンに集約されます
-
制約内でなるべく大きくなろうとする (
Center,ListView) -
なるべく自分の子と同じサイズになろうとする (
Transform,Opacity) -
制約に関係なく特定のサイズになろうとする (
Image,Text) -
コンストラクタに渡された引数に応じて制約に対する反応を変える (
Container) -
与えられた制約に応じて反応を変える (
Row,Column)
4,5について補足すると、例えば Container はデフォルトでは1の様に制約内でなるべく大きくなろうとしますが、widthが渡されると3の様にその特定のサイズになろうとします。
また Row や Column は自身の伸びる向き(Rowならwidth、Columnならheight)について、デフォルトでは子が存在すれば2の様になるべく子のサイズになろうとしますが、その向きにconstraintsを与えられた場合、その制約内でなるべく大きくなろうとします。
UIが破綻する組み合わせ
以上からWidgetのサイズの決まり方は3パターンの BoxConstraints と5パターンの performLayout の組み合わせのどれかになるという事が分かります。
しかしここで実はUIが破綻する組み合わせがあります。それが今回の本題となる RenderFlex overflowed やその他のUI関連エラーの原因です
その組み合わせは下記の2つです
- 「unboundedなconstraints もしくは null」 x 「制約に関係なく特定のサイズになろうとする」のサイズが更に上位(ex. ディスプレイサイズ)の制約を超えてしまう場合
- 「unboundedなconstraints もしくは null」 x 「制約内でなるべく大きくなろうとする」
言葉だけだと分かりづらいので具体例を見ていきましょう
破綻するパターン

1. Column x サイズ指定したContainer
このパターンではunboundedなconstraintsを子に渡す Column の子に
画面サイズを超える100 x 100の高さのContainer群が配置されています。画面サイズを超えるWidget群が配置された事で RenderFlex overflowed のエラーが発生します
コード
/// 適合するパターン:unboundedな制約を子に渡す x 特定のサイズになろうとする
body: Center(
child: Column(
children: [
for (int i = 1; i < 100; i++)
Container(
margin: const EdgeInsets.all(5),
width: 100,
height: 100,
color: Colors.yellow,
),
],
),
),
2. Row x Text, Image
このパターンでは、unboundedなconstraintsを子に渡すColumnの子に、画面サイズを超えるTextやImageが渡されています
パターン1同様、TextやImageはconstraintsに関係なく自身のサイズになろうとする為、画面サイズを超えてしまいRenderFlex overflowedのエラーが発生します
コード
/// 適合するパターン:unboundedな制約を子に渡す x 特定のサイズになろうとする
/// Text
body: Center(
child: Row(
children: const [
Text("But even now, I can't shake the memory of that cat. "
"It's not that I think about it all the time. It's just that "
" every once in a while, the image of that cat surfaces in my "
"mind, like an old photograph that I've filed away in some box."
" And when it does, I find myself wondering where that cat is now, "
"and what it's doing. It's a strange feeling, like a half-forgotten "
"dream that comes back to you briefly in the morning."),
],
),
),
/// Image
body: Center(
child: Row(
children: [
Image.asset('assets/images/claw_machine.png'),
],
),
),
3. Column x ListView
このパターンでは、unboundedなconstraintsを子に渡すColumnの子に、画面サイズを超えるListViewが渡されています
こちらも画面サイズを超えてしまいRenderFlex overflowedのエラーが発生します
コード
/// 適合するパターン:unboundedな制約を子に渡す x 特定のサイズになろうとする
body: Center(
child: Column(
children: [
ListView.builder(
itemCount: 50,
shrinkWrap: true,
itemBuilder: (context, index) {
return ListTile(
title: Text(
index.toString(),
),
);
},
),
],
),
),
4. Column x TextFormField
このパターンは少し特殊ですが、よく遭遇するパターンでもあります
このパターンではColumnの子に、画面サイズを超えない高さのWidgetが配置されており、通常は問題ありません
しかしTextFormFieldがfocusされキーボードが画面に出現すると画面サイズが小さくなってしまい、子Widgetが画面サイズを超えてしまいRenderFlex overflowedのエラーが発生します
コード
/// 適合するパターン:unboundedな制約を子に渡す x 特定のサイズになろうとする
child: Center(
child: Column(
children: <Widget>[
SizedBox(
height: MediaQuery.of(context).size.height * 0.3,
),
Padding(
padding: const EdgeInsets.all(32),
child: TextFormField(
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
),
),
SizedBox(
height: MediaQuery.of(context).size.height * 0.3,
child: const Placeholder(),
),
],
),
),
5. SingleChildScrollView x Expanded (x Column)
スクロールUIを実現する際によく使うSingleChildScrollView x Columnの組み合わせでも注意が必要です
Columnの子をExpandedで囲う実装はよく見ると思いますが、そのColumnを更にSingleChildScrollViewでラップする事で、Columnの子に対しunboundedなconstraintsを渡す事になります
その結果、制約がない中、Columnの子が無限に大きくなろうとしてしまう為、RenderFlex children have non-zero flex but incoming height constraints are unbounded.というエラーを見る事になります
コード
/// 適合するパターン:unboundedな制約を子に渡す x なるべく大きくなろうとする
body: SingleChildScrollView(
child: Column(
children: [
const Expanded(child: Placeholder()),
Expanded(
child: Image.asset('assets/images/dash_game.jpeg'),
),
const Expanded(child: Placeholder()),
],
),
),
6. ListView x ListView
このパターンではunboundedな制約を子に渡すListViewの子に、なるべく大きくなろうとするListViewを配置しています
この組み合わせでは子であるListViewが無限に大きくなろうとする為、レイアウトが破綻してしまい、Vertical viewport was given unbounded height.のエラーが発生してしまいます
コード
/// 適合するパターン:unboundedな制約を子に渡す x なるべく大きくなろうとする
body: Center(
child: ListView(
children: [
ListView(
children: [
for (int i = 1; i < 3; i++)
Container(
margin: const EdgeInsets.all(10),
height: 100,
width: double.infinity,
color: Colors.yellow,
)
],
)
],
),
),
7. ListView x TextField
このパターンでは横軸方向にunboundedな制約を子に渡すListViewの子に、なるべく大きくなろうとするTextFieldを配置しています
結果、子であるTextFieldが無限に大きくなろうとする為、レイアウトが破綻してエラーとなります
この例ではTextField特有のエラーであるAn InputDecorator, which is typically created by a TextField, cannot have an unbounded width.という表示がされます
コード
/// 適合するパターン:unboundedな制約を子に渡す x なるべく大きくなろうとする
body: Center(
child: ListView(
scrollDirection: Axis.horizontal,
children: const [
TextField(),
],
),
),
気をつけるべきWidgetは Flex, CustomScrollView
以上、UIが破綻しエラーとなってしまう組み合わせを見てきました
具体例を見て気づいた方もいるかもしれませんが、このUIの破綻が起きるのは Row や Column、ListView という縦軸、横軸方向に複数のWidgetを持つWidgetを扱う時です
厳密には Row や Column は 「Flex」 を、ListView や GridView は 「CustomScrollView」 を継承しており、気を付けなければならないのはこれらを継承するクラスを使う際です
ちなみにタイトルにもなっている RederFlex overflowed の RenderFlex とはこのFlexクラスの Render オブジェクトの事です
つまり我々を苦しめ続けてきた RenderFlex overflowed とはこの Flex クラスを継承した Row や Column が誤った子Widgetと組み合わさった際に遭遇するエラーです
解決法
大体のUIエラーがFlexとCustomScrollViewを扱う際に発生すると分かった所で、その解決法を見ていきましょう
結論から言ってしまうと以下の通りとなります
-
「unboundedなconstraints もしくは null」 x 「制約に関係なく特定のサイズになろうとする」のサイズが更に上位の制約を超えてしまう場合
- 解決法①: Scrollable widgetでラップし、スクロール可能にする
- 解決法②: 子Widgetを
ExpandedもしくはFlexibleでラップする
-
「unboundedなconstraints もしくは null」 x 「制約内でなるべく大きくなろうとする」
- 解決法: 子Widgetに
SizedBoxやConstrainedBoxで制約を課す
- 解決法: 子Widgetに
組み合わせ 1 の解決法
1の組み合わせでは子Widgetが画面サイズを超えてしまう事が問題でした
その為、以下のどちらかのアプローチで解決する事ができます
- 画面サイズを超えた場合、スクロール可能にしてしまう
- 画面サイズに合うように子Widgetのサイズを変えてしまう
解決後の挙動をどのようにしたいかでどちらのアプローチを取るかが変わるかと思います
先ほどの具体例1を解決してみましょう
BEFORE
body: Center(
child: Column(
children: [
for (int i = 1; i < 100; i++)
Container(
margin: const EdgeInsets.all(5),
width: 100,
height: 100,
color: Colors.yellow,
),
],
),
),
AFTER 1: スクロール可能にする
body: Center(
child: SingleChildScrollView( // <---Scrollableなwidgetでラップする
child: Column(
children: [
for (int i = 1; i < 10; i++)
Container(
margin: const EdgeInsets.all(5),
width: 100,
height: 100,
color: Colors.yellow,
),
],
),
),
),
AFTER 2: 画面サイズに合わせてサイズ変更する
body: Center(
child: Column(
children: [
for (int i = 1; i < 10; i++)
Expanded( // <--- `Expanded`でラップする
child: Container(
margin: const EdgeInsets.all(5),
width: 100,
height: 100,
color: Colors.yellow,
),
),
],
),
),
組み合わせ 2 の解決法
2の組み合わせでは制約がないのに、子Widgetが無制限に大きくなろうとする事が問題でした
その為、シンプルに「子Widgetに制限を課す」事で解決する事ができます
先ほどの具体例6を解決してみましょう
BEFORE
body: Center(
child: ListView(
children: [
ListView(
shrinkWrap: true,
children: [
for (int i = 1; i < 5; i++)
Container(
margin: const EdgeInsets.all(10),
height: 100,
width: double.infinity,
color: Colors.yellow,
),
],
),
],
),
),
AFTER
body: Center(
child: ListView(
children: [
SizedBox( // <--- `SizedBox`で制約を課す
height: 300,
child: ListView(
shrinkWrap: true,
children: [
for (int i = 1; i < 5; i++)
Container(
margin: const EdgeInsets.all(10),
height: 100,
width: double.infinity,
color: Colors.yellow,
),
],
),
),
],
),
),
以上の通り、非常にシンプルに解決出来てしまいました。具体例7も同様のアプローチで解決する事ができます
ただ具体例5だけは少し解決方法が特殊なので注意してください
BEFORE
body: SingleChildScrollView(
child: Column(
children: [
const Expanded(child: Placeholder()),
Expanded(
child: Image.asset('assets/images/dash_game.jpeg'),
),
const Expanded(child: Placeholder()),
],
),
),
AFTER
body: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min, // <-- `MainAxisSize.min`に設定
children: [
// `Expanded`を`Flexible`に置き換える
const Flexible(child: Placeholder()),
Flexible(
child: Image.asset('assets/images/dash_game.jpeg'),
),
const Flexible(child: Placeholder()),
],
),
),
具体例5では Columnの子Widgetがサイズ制約を受ける様に2段階のサイズ制約を課しています
まず Column 自身が子Widgetのサイズになろうとする様に mainAxisSize: MainAxisSize.min を設定します
その上で、Column の子Widgetをなるべく大きくなろうとする Expanded ではなく、viewportの範囲内で可能な限り大きくなると言う特性を持った Flexible でラップする事でサイズ制約をかけています
この辺りのFlexとFlexibleの関係性やアルゴリズムについては下記の動画で詳しく解説してくれています
先に挙げた具体例の解決方法は下記のサンプルレポジトリにコメントしているので、気になる方は答え合わせしてみてください
以上
以上で RenderFlex overflowed の駆逐を目指す長い旅は終わりです
正直長すぎてここまで読んで頂いた方はいらっしゃらないと思いますが、これをキッカケにずっと気になっていたFlutterの低レイヤー世界を冒険する事ができ、読まれなかろうが自分にとって非常に大きな学びとなりました。
もしここまで読んでくれた方がいたら、数多くの誤字脱字や表記揺れ、間違いで目眩を催しているかと思いますので目に余る部分があれば、生暖かくご指摘いただければ幸いです。
また先人のエンジニアの方々が残してくれた貴重な資料なくしてはこれほど深掘りする事はできませんでした。改めて貴重な資料や情報を、登壇や執筆してくださった先人の方々に感謝したいと思います。自分もいつかそんな誰かの為になる情報を発信できるよう日々精進していきたいと思います。
それでは、皆様...
良いお年を!!!
参考
公式
- https://docs.flutter.dev/development/ui/layout/constraints
- https://docs.flutter.dev/development/ui/layout/box-constraints
- https://docs.flutter.dev/resources/architectural-overview#rendering-and-layout
- https://docs.flutter.dev/testing/common-errors
登壇・記事
- https://www.youtube.com/watch?v=gBfHYHvojvk&t=10722s
- https://developers.cyberagent.co.jp/blog/archives/36869/
- https://www.youtube.com/watch?v=UCuf1vXPH3A
- https://www.youtube.com/watch?v=UUfXWzp0-DU&t=706s
- https://www.youtube.com/watch?v=_jlXS8chb7g
- https://medium.com/flutter-jp/dive-into-flutter-4add38741d07
Discussion