【Flutter】いつもText のオーバーフロー対策を忘れるあなたへ
あ!!!テキストはみ出た!!!対策忘れてた!!!

テストケースなどがしっかりしていれば防げますが、そうでない場合などは対応が漏れる場合が多いです(自分もよく忘れます)。
よく知られた対処法は、Expanded/Flexibleで囲むことです。
Card(
child: Column(
children: [
Row(
children: [
FlutterLogo(),
SizedBox(width: 8),
+ Expanded(
child: Text('とっても長い名前なのでオーバーフロー対策をしないといけない'),
+ ),
],
),
]
)
)
上記のようなシンプルな作りだと、抵抗なくExpanded/Flexibleを追加できます。
ですが、Column や Row が複雑に構成される箇所で Expanded/Flexible を使うと、祖先のウィジェットにも影響してしまったり、扱いが少し難しくなります。
たとえば共通ウィジェットして、UIの部品を提供するときに不便になります。
Row(
children: [
+ // 祖先の Row.children として使われていたら、
+ // そこに Expanded をつけなければいけない。
+ Expanded(
child: Column(
children: [
Row(
children: [
FlutterLogo(size: 24),
SizedBox(width: 8),
Expanded(
child: Text('とっても長い名前なのでオーバーフロー対策をしないといけない')
),
],
),
SizedBox(height: 200),
],
),
+ ),
],
),
ListView(
children: const [
// エラーで表示できない
Row(
children: [
FlutterLogo(size: 24),
SizedBox(width: 8),
Expanded(
child: Text('とっても長い名前なのでオーバーフロー対策をしないといけない')
),
],
),
],
)
なので、自分もそうですが、極力 Expanded/Flexible はつけずに書こうとしてしまい、結果あとで忘れてしまいます。
解決策
結論をまず提示すると、テキストが含まれるウィジェットには Flexible と MainAxisSize.min を組み合わせて使いましょう。
これで、祖先のウィジェットにも影響せず、同時にオーバフロー対策もできます。
Row(
+ mainAxisSize: MainAxisSize.min,
children: [
FlutterLogo(size: 24),
SizedBox(width: 8),
+ Flexible(
child: Text('とっても長い名前なのでオーバーフロー対策をしないといけない')
+ ),
],
)
この解決法は、material.dart の ElevatedButton.icon の実装を見て気づきました。
return Row(
mainAxisSize: MainAxisSize.min,
spacing: lerpDouble(8, 4, scale)!,
children:
effectiveIconAlignment == IconAlignment.start
? <Widget>[icon, Flexible(child: label)]
: <Widget>[Flexible(child: label), icon],
);
内部でも使われている方法なので安心ですね。
テキストと何かを並べる実装を、コンポーネントとして実装する時は、基本的に何も考えず Flexible と MainAxisSize.min の組み合わせで実装しても良さそうです。
(もちろん Expanded を使うのが正解の場面も多くあると思います)
そもそもなぜオーバーフローするのか
本題は終わり、ここからは興味がある人向けです。
Row、Column、Expanded、Flexible の振る舞いの話です。
そもそも、なんでこっちでテキストのオーバーフロー対策しなきゃいけないんだよ。勝手にやってくれよみたいなことを思うのではないでしょうか。
Row の children は 0 <= w <= double.infinity で計算される
Flutter のシステムでは、BoxConstraints という形でサイズの制約が親から渡されてきます。
普通に Scaffold で描画すれば、デバイスのサイズが最大値の制約が取得できます。
// BoxConstraints(0.0<=w<=587.0, h=1302.0)
Scaffold(
body: LayoutBuilder(
builder: (context, constraints) {
print(constraints);
return Container();
},
),
)
それを、色んなウィジェットがなんやかんやして、上手いこと描画するような機構が備わっています。
制約を強くしたり弱くしたり、あるいはあえて無視しながら。
そして、Row や Colum は、一部制約をあえて無視することでレイアウトをしています。
// ↓ double.infinity に変わっている
// BoxConstraints(0.0<=w<=double.infinity, h=1302.0)
Scaffold(
+ body: Row(
+ children: [
LayoutBuilder(
builder: (context, constraints) {
print(constraints);
return Container();
},
),
+ ],
+ ),
)
Row ウィジェット自体には制約がついたままですが、Row の children には、最大幅の制約がついていません。
この仕様よって、横に並べる機能は自由度が増し、スクロールさせるようなことも可能になります。

SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [Text('とっても長い名前なのでオーバーフロー対策をしないといけない' * 10)],
),
)
Expanded/Flexible の役割
デフォルトで自由度の高い実装になっている Column / Row ウィジェットですが、ちゃんと親の制約を踏まえたレイアウトにしたい場合ももちろん出てきます。
そのときに登場するのが Expanded/Flexible ウィジェットです。
さっきの Row のなかの LayoutBuilder を、Flexible を囲んでみましょう。
// ↓ デバイスの幅に変わっている
// BoxConstraints(0.0<=w<=587.0, h=1302.0)
Scaffold(
body: Row(
children: [
+ Flexible(
child: LayoutBuilder(
builder: (context, constraints) {
print(constraints);
return Container();
},
),
+ ),
],
),
)
制約が親と同一になりました。
ちなみに Expanded の場合は w=587.0 となり、最大幅を子に強制させます。
// ↓ デバイスの幅に変わっている
// BoxConstraints(w=587.0, h=1302.0)
Scaffold(
body: Row(
children: [
+ Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
print(constraints);
return Container();
},
),
+ ),
],
),
)
Expanded(拡げる) なんて名前をしていますが、その実態はむしろ制約を持たせるというものです(制約いっぱいに広げる、もしくは Flexible に対しての Expanded という意図でしょう)。
まとめ
テキストのオーバーフロー対策を毎回しないといけない理由の話に戻ります。
テキストに限らず、Row / Column がそもそもオーバーフロー上等で children をレイアウトしていることに起因します。
よって、次のような使い分けをするのが良いでしょう。
- オーバーフローしてスクロールさせたい場合は
SingleChildScrollViewを使う- 縦スクロールコンテンツ内での
Columnは、そもそも考える必要がなさそう
- 縦スクロールコンテンツ内での
- オーバーフローさせない前提でレイアウトする場合は、
Flexible/Expandedでそれぞれサイズの振る舞いを決定させる- 意図しないオーバーフローの危険が付きまとう
Textなどは、FlexibleとMainAxisSize.minの組み合わせで、親ウィジェットにできるだけ影響を与えないようにする
- 意図しないオーバーフローの危険が付きまとう
Column/Row や Flexible/Expanded 実装の助けになれば幸いです。
参考になる記事など
Discussion
実装しときながら「 これ、オーバーフローするのか?」って自分で思うこと多いので、毎度ドキドキしがちです。