🚧

【Flutter】いつもText のオーバフロー対策を忘れるあなたへ

に公開

あ!!!テキストはみ出た!!!対策忘れてた!!!

テストケースなどがしっかりしていれば防げますが、そうでない場合などは対応が漏れる場合が多いです(自分もよく忘れます)。

よく知られた対処法は、Expanded/Flexibleで囲むことです。

Card(
  child: Column(
    children: [
      Row(
        children: [
          FlutterLogo(),
          SizedBox(width: 8),
+         Expanded(
            child: Text('とっても長い名前なのでオーバーフロー対策をしないといけない'),
+         ),
        ],
      ),
    ]
  )
)

上記のようなシンプルな作りだと、抵抗なくExpanded/Flexibleを追加できます。

ですが、ColumnRow が複雑に構成される箇所で 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 はつけずに書こうとしてしまい、結果あとで忘れてしまいます。

解決策

結論をまず提示すると、テキストが含まれるウィジェットには FlexibleMainAxisSize.min を組み合わせて使いましょう
これで、祖先のウィジェットにも影響せず、同時にオーバフロー対策もできます。

Row(
+ mainAxisSize: MainAxisSize.min,
  children: [
    FlutterLogo(size: 24),
    SizedBox(width: 8),
+   Flexible(
      child: Text('とっても長い名前なのでオーバーフロー対策をしないといけない')
+   ),
  ],
)

この解決法は、マテリアルの ElevatedButton.icon の実装を見て気づきました。
https://github.com/flutter/flutter/blob/d9c63168c8d232e3232a3c1af3393b45db4c6cf8/packages/flutter/lib/src/material/elevated_button.dart#L548-L557

    return Row(
      mainAxisSize: MainAxisSize.min,
      spacing: lerpDouble(8, 4, scale)!,
      children:
          effectiveIconAlignment == IconAlignment.start
              ? <Widget>[icon, Flexible(child: label)]
              : <Widget>[Flexible(child: label), icon],
    );

内部でも使われている方法なので安心ですね。
テキストと何かを並べる実装を、コンポーネントとして実装する時は、基本的に何も考えず FlexibleMainAxisSize.min の組み合わせで実装しても良さそうです。
(もちろん Expanded を使うのが正解の場面も多くあると思います)

そもそもなぜオーバーフローするのか

本題は終わり、ここからは興味がある人向けです。
RowColumnExpandedFlexible の振る舞いの話です。

そもそも、なんでこっちでテキストのオーバーフロー対策しなきゃいけないんだよ。勝手にやってくれよみたいなことを思うのではないでしょうか。

Rowchildren は 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();
    },
  ),
)

それを、色んなウィジェットがなんやかんやして、上手いこと描画するような機構が備わっています。
制約を強くしたり弱くしたり、あるいはあえて無視しながら。

そして、RowColum は、一部制約をあえて無視することでレイアウトをしています。

//                        ↓ 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 などは、FlexibleMainAxisSize.min の組み合わせで、親ウィジェットにできるだけ影響を与えないようにする

Column/RowFlexible/Expanded 法海の助けになれば幸いです。

参考になる記事など

https://itome.team/blog/2019/12/flutter-advent-calendar-day9/

Discussion