【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('とっても長い名前なのでオーバーフロー対策をしないといけない')
+ ),
],
)
この解決法は、マテリアルの 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