LoginSignup
11
14

More than 1 year has passed since last update.

Flutterのレイアウトを理解して極める

Last updated at Posted at 2022-08-20

はじめに

Flutterを使った開発で、よく遭遇するであろうエラーに
Vertical viewport was given unbounded height.
A RenderFlex overflowed by ~ pixels on the bottom.
などがあります。今回はこれに関わるものとして、
①レイアウトの簡単な仕組みと具体例
②レイアウトの詳細
③実際のサンプル問題を通して理解を深める
の3つを書いていきたいと思います。

対象読者

  • Flutter初学者
  • 上のエラー文で検索してきた人
  • ③のサンプル問題が解けなかった人

参考

①レイアウトの簡単な仕組みと具体例

Flutterのレイアウトは上の参考記事にある通り、Constraints go down. Sizes go up.が全てです。 (positionは特に触れないため今回の記事から省きました)
上を図にすると、

まず、親が子にconstraints(制約)を伝える。

それを踏まえて、子は親に自分のsizesを伝える。

のようになっています。

具体的な数字を入れてみるとこんな感じです。
まず、親が子にconstraints(制約)を伝える。

それを踏まえて、子は親に自分のsizesを伝える。

エラーは、sizesがconstraintsを何かしらの理由で超えてしまった場合予期せぬconstraintsを親から受け取った場合などに生じます。
また、Flutterはツリー構造なので、孫widgetも登場します

この場合は、constraintsは孫Widgetにまで渡ってから、次は逆にsizesを親Widgetまで伝えます。

さらに、子Widgetが複数存在することもあります。

こうなってくると、子は親のconstraintsに収まるのが難しくなりそうですよね...

具体例

では実際に具体例を見てみましょう。今はwidthだけを考えていきます。なお、ウィンドウ幅は500であるとしてください。

return Container(
  width: 100,
  color: Colors.red,
  child: SizedBox(
    width: 150,
  ),
);

これは親WidgetがContainer、子WidgetがSizedBoxという関係になっています。
読み進める前に、上のソースコードを描画するとどのような結果になるのか予想してみてください。

Constraintsについて

まず、constraintsから考えていきます。
Containerは子にどんな制約を持つでしょうか?
Containerはconstraintsに何も指定しない場合、BoxConstraints.tightForが設定されます。
すると、これは「子を100widthにしてくれ」という制約になります。

続いて、SizedBoxが子に持つ制約を考えます。
SizedBoxも同じく、BoxConstraints.tightForのconstraintsが使われています。よって、「子を150widthにしてくれ」という制約があるのですが、SizedBoxContainer自分のconstraintsを親のconstraintsの範囲に収めるという特徴があります。すると、ここでも「子を100widthにしてくれ」という制約になります。

実はもう一つ考えるべきconstraintsがあります。 それはRenderViewの存在です。
FlutterはRenderViewをルートに持つので、このconstraintsも考えておく必要があります。ソースコードによると、ここでは、BoxConstraints.tightが使われているようです。デバイスの幅が500とすると、これは「子を500widthにしてくれ」という制約になります。
すると、ContainerSizedBoxの制約も変わってきますね。

ここで、constraintsに関してまとめておきます。

constraints

RenderView Container SizedBox
子を500width 子を500width 子を500width

結局全て同じconstraintsになりました。
また、今回は簡単のためheightを考えませんでしたが同じことです。

sizesについて

次にようやく、sizesを考えることができます。
sizesを考える場合は、子から親方向へ考えていきます。
ではまずSizedBoxからです。SizedBoxは子を持つ場合と持たない場合で、サイズの指定が変わります。子を持たない場合、ソースコードによるとconstraintsに収まる範囲で、0に近くなるようにしてサイズが決まるようです。よって、今回は500widthピッタリです。

Containerは子がある場合は、子のサイズがそのまま自分のサイズになるようです。よって同じく500widthです。

ここでsizesについてまとめます。

sizes

Container SizedBox
500width 500width

結局これも500となりました。

描画結果

ソースコードの描画結果を下に示します。
Screen Shot 2022-08-20 at 12.24.00 AM.png

全画面が真っ赤になっていますね。
これは全てのconstraintsが結局ウィンドウサイズとなり、全てのsizesもウィンドウサイズとなっているからです。
また、子のwidthが親のwidthよりも大きいにも関わらず、エラーが出ていないことは意外ではないでしょうか?これは本当のwidthはconstraintsとsizesの関係で決まることを意味しています。(設定した150がそのまま使われる訳ではない)

Scaffoldを使う

ここで、上のソースコードを少し修正してみます。

return Scaffold( //**** 変更箇所 ****//
  body: Container(
    width: 100,
    color: Colors.red,
    child: SizedBox(
      width: 150,
    ),
  ),
);

ScaffoldContainerの親に置いてみました。
これによって、どのようにconstraintsが変わるのか確認します。
ソースコードによると、Scaffold親のconstraintsを子のconstraintsへ伝える方法SizedBoxContainerとは異なります。widthのconstraintsに関しては、minWidth:0, maxWidth:親のconstraintsの最大幅のようになります。高さのconstraintsに関しても、親のconstraintsの最大高さからappBarなどのコンテンツの高さを引いて求めています。つまり、必ずしも親のconstraintsの中に収めるのではなくて、親のconstraintsの最大値以下の範囲に収めています。(最小値は親のconstraintsを下回ることが可能)

最終的に全体のconstraintsは次のようになります。
今回はウィンドウ高さは1000としてください。

RenderView Scaffold Container (width 100) SizedBox (width 150)
minWidth:500 maxWidth:500 minHeight:1000 maxHeight:1000 minWidth:0 maxWidth:500 minHeight:0 maxHeight:1000 minWidth:100 maxWidth:100 minHeight:0 maxHeight:1000 minWidth:100 maxWidth:100 minHeight:0 maxHeight:1000

ContainerSizedBoxのmin-maxWidthが500に固定されなくなりました!
(今回は特にappBarなど指定していないのでウィンドウ高さの1000がScaffoldでそのまま利用される形となってます)

今度はsizesを考えます。
Scaffoldのサイズはソースコードによると、constraintsの最大値でchildのサイズに寄らないのが特徴です。
他も1つずつ考えていくと、以下の表のようになります。

Scaffold Container SizedBox
width:500 height:1000 width:100 height:0 width:100 height:0

描画結果は以下のようになります。
実際はContainerなどは描画されているんですが、高さが0になっているので見えなくなっています。
(SizedBoxもしくはContainerheightを設定してあげると、赤の矩形が見えるようになります。)

Screen Shot 2022-08-20 at 1.17.56 AM.png

ここまでのまとめ

だいぶややこしくなってきました。一旦ここまでの話をまとめたいと思います。
親widgetをA、子widgetをB、孫widgetをCとします。

  • constraintsはAからBそしてCへ伝わる
  • AからBに伝わったconstraintsはBからCにそのまま伝わる訳ではない。
  • constraintsは基本minWidth、maxWidth、minHeight、maxHeightの4つで構成される
  • sizesは基本的にCからBそしてAへ伝わる
    • ただし、Scaffoldのように子のsizesは関係なしに、自身のsizesを決めるものがある

②レイアウトの詳細

①で見たように、constraintsや、sizesの伝わり方、決め方に一定のルールはあるものの、widgetごとに微妙に挙動が違うことがあります。

constraintsについて

constraintsの種類

まずはconstraintsの基本である、BoxConstraintsにどんなコンストラクタがあるか見てみます。これを見ることで、min-maxWidth、min-maxHeightにどんな値が入るのか理解できるようになります。
コンストラクタの種類は以下です。

コンストラクタ名 説明
デフォルト 引数がなければ0~∞。あればその値。
tight 1つの同じ値をmin-maxに指定する。よって範囲はない。
tightFor 引数がnullのときは0~∞。それ以外の引数ならtightと同じ。
tightForFinite 引数が∞のときは0~∞。それ以外の引数ならtightと同じ。
loose minは0で固定されており、maxを指定する。
expand 引数がnullの時は∞。それ以外の引数ならtightと同じ。

tightForContainerSizedBoxで使われていましたね。
こうしてみてみると、デフォルトを除いて、looseだけが少し仲間はずれなパターンです。他は引数が条件を満たせば、tightと全く同じ挙動です。
例外として、_BodyBoxConstraintsというBoxConstraintsを継承したクラスがあります。これはScaffoldのbody専用で、おおよそlooseと同じ挙動です。

BoxConstraintsのメソッド

①で最後まとめたように、親から子に伝わったconstraintsはそのまま孫に伝えていくことは少なく、少し変更されてから伝わると書きました
どんなマージ方法があるのか、今度はメソッドを見てみましょう。

メソッド名 説明
enforce 引数のconstraintsの範囲に収まりかつ、元々のconstraintsとなるべく近くなる形で、新たなBoxConstraintを返す

constraintsを引数にとるメソッドは1つだけでした。
ContainerSizedBoxはこのメソッドを使っているので、①で書いたように親のconstraintsに収まるようにconstraintsが変更されていました。

Containerの場合、多くの記事で

子を持たない場合、可能な限り大きく表示される

のように書かれていることがあります。ただこれは、あまり厳密ではなく片手落ちです。
Containerのドキュメントにもある通り、厳密にはこうです。

Containers with no children try to be as big as possible unless the incoming constraints are unbounded, in which case they try to be as small as possible.

子を持たないコンテナーは、入ってくる制約が無制限でない限り、できるだけ大きくしようとします。制限がない場合は、できる限り小さくしようとします。

例えば、今回のように親から∞以外のconstraintsが渡されている場合は確かに「できるだけ大きく」しようとしますが、ListViewのようなunboundedが渡されてきた場合、「できる限り小さく」しようとします。
それぞれ具体例を示します。

親から∞以外のconstraintsが渡されている場合

SizedBox(親)から100✖️100のconstraintsがContainerに渡されている場合

    return Scaffold(
      body: SizedBox(
        height: 100,
        width: 100,
        child: Container(
          color: Colors.red,
        ),
      ),
    );

Screen Shot 2022-08-20 at 5.34.00 PM.png

確かに100×100の矩形ができ、渡されたconstraintsの範囲内で可能な限り大きくなっています。
この理由は、Containerの内部でConstrainedBox(constraints: const BoxConstraints.expand())がchildとして使われているためです。
ConstrainedBoxは自分に子がいない場合、自分のconstraintsを親から渡されたconstraintsとBoxConstraint.enforceします。
自分のconstraintsにはBoxConstraints.expand()が使われているので、enforceすると、親のconstraintsの最大値をtightしてできたBoxConstraintsが生成されます。
そのconstraintsの最小値(=最大値)に自分のsizesを一致させます。

親から∞のconstraintsが渡されている場合

この場合は、ソースコード内ではunconstraintsや、unboundedというキーワードで登場します。
スクロール系のwidgetではBoxConstraintsではなくSliverConstraintsが使われており、これは基本的に最大値が無限大のBoxConstraintsに変換されます。ソースコード
他にもColumnRowFlexといった並べる系のwidgetは子に渡すconstraintsはunboundedになります。ソースコード

ListView(親)から(window width)✖️(0~∞)のconstraintsがContainerに渡されている場合、

    return Scaffold(
      body: ListView(
        children: [
          Container(
            color: Colors.red,
          ),
        ],
      ),
    );

Screen Shot 2022-08-20 at 7.11.05 PM.png

もし可能な限り大きくなるのであれば、Containerの高さは∞となって、画面全体を赤で埋めているはずです。しかし、そうはなっていません。
この理由は、Containerの内部でLimitedBoxが使われているためです。
これは、もし親から最大値∞のconstraintsが渡された場合は、その最大値を引数のmaxWidth、maxHeightに変更するというものです。
これによって、unconstraintsが最大値ありのconstraintsとなり、「可能な限り大きくなる」ということが防がれています。

sizesについて

sizesは子のsizeを自身のsizeの計算に含めるパターンか、子のsizesは気にせず計算するパターン、そしてその2つを場合に応じて使い分けるパターンの3つがあります。しかし、どのパターンも親から渡ってきたconstraintsをsizesの計算に含めるのは共通です。
例えば1つめはContainerSizedBoxで、2つめは、Scaffold、3つめはAlignCenterPositionなどがあります。
肌感覚ですが、子の配置には関わるが、子が大きくなっても小さくなっても自分には関係ないというwidgetは「子のsizesは気にせず自身のsizesを計算するwidget」に当てはまります。
そういう意味で「Alignfactorを指定しない場合は、子のsizesは気にしなくていいので2のパターンになりますが、factorを指定した途端、子のsizeを考慮する必要が出てしまうので、1のパターンになる」と考えることができます。

ここまでのまとめ

  • constraintsは親から子へ伝わるが、そのときenforceといったメソッドを通じて変更される
  • sizesは「子のsizesを考える必要のあるwidget」と「考える必要のないwidget」で導出方法が変わる
  • 最大限広がろうとするwidgetでも、親から渡ってきたconstraintsがunboundedの場合はなるべく小さくなろうとする (子なしのContainerFlex系でMainAxisSize.maxのものなど)

③実際のサンプルを通して理解を深める

ここまでを踏まえ、いくつかのサンプルコードでエラーが出るか出ないかを予想してみましょう!

1問目

    return Scaffold(
      body: Column(
        children: [
          ListView()
        ],
      ),
    );
結果と解説

結果

Vertical viewport was given unbounded height.

解説

Columnは親からのconstrainsにかかわらず、高さ方向のconstraintsを0~∞(unbounded)にします。
ListViewTabBarViewなど内部でViewPortを持つものは、スクロール方向などにかかわらず、親がunboundedのconstraintsを渡してくるだけでerrorをthrowします。

2問目

    return Scaffold(
      body: SizedBox(
        width: 500,
        child: Row(
          children: [
            Container(width: 500, color: Colors.black),
            Container(width: 500, color: Colors.red),
          ],
        ),
      ),
    );
結果と解説

結果

A RenderFlex overflowed by 500 pixels on the right.

解説

最終的にRowは横方向に1000のsizeとなりますが、SizedBoxはconstraintsで500としているのでoverflowエラーとなります。

3問目

    return Scaffold(
      body: Row(
        children: [
          ConstrainedBox(
            constraints: BoxConstraints.expand(),
          )
        ],
      ),
    );
結果と解説

結果

RenderConstrainedBox object was given an infinite size during layout.

解説

Rowは横方向に∞のconstraintsを作ります。
結果、その子のConstrainedBoxは∞の幅のsizesとなりエラーが出ます。
∞のconstraintsは直ちにエラーになるものではないですが、∞のsizesはエラーが出ます。
(※大丈夫だと思いますが、ここで言うsizesはSizedBoxなどに指定するwidthやheightのことではなく、最終的にconstraintsなどから決定されるwidthのsizesのことを指しています。)

4問目

    return Scaffold(
      body: Row(
        children: [
          ConstrainedBox(
            constraints: BoxConstraints.tightFor(),
          )
        ],
      ),
    );
結果と解説

結果

エラーなし!

解説

3問目と似ていますが、expandtightForになっています。tightForは引数がnullなら0~∞のconstraintsを作ります。対してexpandは∞のconstraintsです。
よって実際のsizesは0となり、特にエラーは起きません。

5問目

    return Scaffold(
      body: DefaultTabController(
        length: 2,
        child: Column(
          children: [
            TabBar(
              tabs: [
                Tab(text: 'tab1'),
                Tab(text: 'tab2'),
              ],
            ),
            TabBarView(children: [
              Container(
                width: 100,
                height: 100,
              ),
              Container(
                width: 100,
                height: 100,
              )
            ]),
          ],
        ),
      ),
    );
結果と解説

結果

Horizontal viewport was given unbounded height.

解説

1問目のTabBarView版です。

6問目

    return Scaffold(
      body: DefaultTabController(
        length: 2,
        child: Column(
          children: [
            TabBar(
              tabs: [
                Tab(text: 'tab1'),
                Tab(text: 'tab2'),
              ],
            ),
            Expanded(
              child: TabBarView(
                children: [
                  Container(
                    width: 100,
                    height: 100,
                  ),
                  Container(
                    width: 100,
                    height: 100,
                  )
                ],
              ),
            ),
          ],
        ),
      ),
    );
結果と解説

結果

エラーなし!

解説

Expandedを挟むことでその子(ViewPort)に渡すconstraintsが∞ではなく、その親(Column)に対してScaffoldから渡されてくるconstraintsの最大値が使われるようになります。これによって1や5問目のようなエラーを防ぐことができます。

7問目

    return Scaffold(
      body: Column(
        children: [
          Column(
            children: [
              Expanded(
                child: SizedBox(),
              ),
            ],
          ),
        ],
      ),
    );
結果と解説

結果

RenderFlex children have non-zero flex but incoming height constraints are unbounded.

解説

6で言ったようにExpandedを使うとその子に渡すconstraintsは親に渡されてくるconstraintsの最大値が使われるようになります。しかしその最大値がunboundedである場合は上のようなエラーが出ます。

8問目

    return Scaffold(
      body: Column(
        children: [
          Expanded(
            child: Column(
              children: [
                Expanded(
                  child: SizedBox(),
                ),
              ],
            ),
          ),
        ],
      ),
    );
結果と解説

結果

エラーなし!

解説

少し変に思うかもしれませんが、Expandedを使うことでconstraintsの最大値をunboundedにすることなく、うまくScaffoldから渡ってくるconstraintsを伝搬できるようになったのでエラーが出ません。

9問目

    return Scaffold(
      body: CustomScrollView(
        slivers: [
          Container(
            color: Colors.red,
          )
        ],
      ),
    );
結果と解説

結果

RenderSliver does not understand the RenderBox layout protocol.

解説

Sliver系のwidgetはconstraintsとして、SliverConstraintsを使います。これは、BoxConstraintsを継承している訳ではないので互換性がありません。よってこのエラーが出ます。ちなみに、SliverToBoxAdapterを使うとうまくSliverConstraintsからBoxConstraintsへ変換してくれます。

10問目

    return Scaffold(
      body: SliverList(
        delegate: SliverChildListDelegate([
          Container(
            color: Colors.red,
          )
        ]),
      ),
    );
結果と解説

結果

A RenderCustomMultiChildLayoutBox expected a child of type RenderBox but received a child of type
RenderSliverList.

解説

9問目の逆バージョンです。きちんと期待しているconstraintsの型を渡せるようにchildを設定しましょう。

まとめ

気を付けることは、

  • constraintsがどのように渡っていくのか
  • unboundedのconstraintsに対してエラーが出る場合は、Expandedなどで囲む
  • sizesがoverflowしないようにする
  • 親や子が期待しているconstraintsの型にする

あたりかなと思います。
今後エラーが出ても対応できることを願っています!

11
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
14