RenderFlex children have non-zero flex but incoming height constraints are unboundedの原因と解決方法


この記事はARISE analytics Advent Calendar 2023の13日目の記事になります。

僕がFlutter開発をしていて表題のエラーで詰まった際、非常に参考になった[stackoverflowの回答](https://stackoverflow.com/a/66718208)があるので、図解を交えて日本語で説明し直してみました。


# 本題
Flutterの開発ではRowやColumnを多用すると思いますが、その際に次のようなエラーに遭遇することがあります：`RenderFlex children have non-zero flex but incoming height constraints are unbounded`

これは、RenderFlexオブジェクトに高さのconstraintが指定されていない状態でflexプロパティを持つ子ウィジェットを配置したことで発生するエラーになります（widthの場合もあります）。

具体的にどういう時に発生するかというと、Columnの子ウィジェットにColumnを、さらにその子ウィジェットにExpandedを配置した際に発生します。

この時に何が起こっているのかを図解したものが以下になります。

![constraint1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243690/836e3a2e-b4ac-68b6-bf14-5c9871883a8d.png)

[Constraints go down. Sizes go up. Parent sets position.](https://docs.flutter.dev/ui/layout/constraints)と公式で謳われているように、Flutterでは親ウィジェットから子ウィジェットへ制約が伝えられ、それを受け取った子ウィジェットがサイズを計算して親ウィジェットへ伝えるという仕組みになっています。

図のケースを順に追っていきます。
1. まず一番上の要素としてScreenがあります。Screenは自身のサイズと同じになるような制約を子ウィジェットに渡します
2. Screenの子ウィジェットであるColumn_0は、Screenと同じサイズになる制約を受け取るので、Screenと同じ高さに決まります。一方で、Columnウィジェットの性質として子ウィジェットに対しては高さ制約をなくしてしまいます（unboundedな制約を伝える）
3. Column_0の子ウィジェットであるText_Aは、unboundedな制約を受け取りますが、ウィジェット自身に高さが設定されているので制約がなくても高さが決まります
4. Column_0の子ウィジェットであるColumn_1は、unboundedな制約を受け取るため自身の高さが一意に定まりません。そのため、子ウィジェットの高さで決まります。子ウィジェットに対しては、Column_0と同様にunboundedな制約を伝えます
5. Column_1の子ウィジェットであるText_Bは、unboundedな制約を受け取りますが、ウィジェット自身に高さが設定されているので制約がなくても高さが決まります
6. Column_1の子ウィジェットであるExpandedは、unboundedな制約を受け取るため自身の高さが一意に定まりません。Expandedは、サイズが決まっているウィジェットを配置した後で残ったスペースを自身のサイズとして決定しようとします

この最後の手順で、エラーが発生します。
Expandedはflexパラメータを持つウィジェットであるため、親によって与えられた制約から他のウィジェットのサイズを差し引いた残りスペースを自身のサイズとして決定します。よって、以下の計算式でExpandedのサイズが決まります。

残りスペース = 親から与えられる制約 - サイズが確定しているウィジェットのサイズ

Expandedウィジェットのサイズは、親ウィジェットであるColumn_1から受け取った高さ制約(unbounded)から、高さ固定のウィジェットであるText_Bの高さを引いた値になります。よって、以下の計算式となり、残りスペースが計算できません。

残りスペース = unbounded - Text_Bの高さ

## エラーが発生する仕組み
これでエラーとなる仕組みはわかりました。しかし、一つ気になることがあります。Columnがネストしていない場合、このエラーは発生しません。なぜColumnがネストしている場合にこのような事象が発生するのでしょうか。

まず、[公式ドキュメント](https://api.flutter.dev/flutter/widgets/Column-class.html)にColumnウィジェットのレイアウトアルゴリズムの説明があります。Step1に次の記載があります。
`1. Layout each child with a null or zero flex factor (e.g., those that are not Expanded) with unbounded vertical constraints`

flex factor(flexフィールド)を持たない子ウィジェット（つまりExpandedではないようなウィジェット）を垂直方向にunboundedな制約を課して配置するという内容です。

次に、ネストしたColumn（Column_1）はColumn_0の子ウィジェットであり、またflex factorを持ちません。
ColumnはFlexを継承したクラスですが、Flexはflex factorを持ちません。Flexibleクラスとそれを継承するクラス(ExpandedとSpacer)だけがflex factorを持つウィジェットです。

したがって、Column_1はColumn_0の子ウィジェットでありかつflex factorを持たないため、unboundedな制約が課されます。親の制約とText_Bの高さをもとに、Expandedは残りスペースを計算しようと試みますが、計算できないためエラーが発生します。

## エラーを解決するには
ではどうやってエラーを解決するかというと、Column_0とColumn_1の間にExpandedをはさみます。すると以下のようになります。

![constraint2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243690/ad577594-fa18-7cee-8cf8-913f74c741ca.png)

まず、Expanded_Aの高さ計算です。Expanded_Aはflex factorを持つため、Screenサイズの制約を受け取ります。よってExpanded_Aの高さが計算でき、Screeの高さからText_Aの高さを差し引いた値で決定します。

次に、Expanded_Bの高さ計算です。Column_1はExpanded_Aの子ウィジェットなので、Expanded_Aサイズの制約を受け取ります。Expanded_Bはflex factorを持つため、Column_1からExpanded_Aサイズの制約を受け取ります。これでExpanded_Bの高さが計算でき、Expanded_Aの高さからText_Bの高さを差し引いた値で決定します。

これで、flex factorを持つ全てのウィジェットがサイズを算出でき、boundedな制約を獲得できました。

## 結論
この時はこれ！のような単純明快な解決方法ではありませんが、各ウィジェットの制約とサイズ計算を整理することで解決の緒を見つけられるので、落ち着いて整理してみるのが近道になりそうです。