FlutterのWidgetをコードを動かしながら学ぶ: Wrap編

Flutter Widget of the Week を見るだけでも勉強になるのですが視聴によるインプットだけでなく手元でコードを動かしてそれをブログ記事にする所までやって後から思い出しやすいようにしてみるシリーズです。今回はWrapオブジェクトについて書きました。
2020.03.16

Flutter は動画コンテンツも充実していて週毎にウィジェットを紹介している Flutter Widget of the Week では短い動画でウィジェットの概要を掴むことができる動画を視聴できます。

Flutter Widget of the Week を見るだけでも勉強になるのですが、記憶力が良くないので、視聴によるインプットだけでなく手元でコードを動かしてそれをブログ記事にする所までやって後から思い出しやすいようにしてみるシリーズです。

これまでの記事は以下のリンクから参照できます。

今回取り上げるのは Wrap です。

Wrap とは

Flutter Widget of the Week で Wrap をとりあげた動画は以下です。Expanded のドキュメントを見ると、複数の行または列でその子のウィジェットを表示するためのウィジェットと記載されています。

Expanded を使ってみる

ドキュメントに記載されているサンプルコードを実行すると以下のような表示になります。

ラップは row や column のように小ウィジェットを direction に指定された軸で各々を隣接して配置します。spacing に指定された分スペースを空けます。direction に指定された方向に子ウィジェットをレイアウトしていきながら子を配置するための十分なスペースがない場合、新たに行・列を作成してレイアウトします。

コードを弄りつつ説明するためサンプルコードを用意します。margin に 10.0 をもつ Container を複数 children に持つ Row ウィジェットを用意しました。Row は x 軸方向に子ウィジェットを配置し、配置された子ウィジェットは画面内に収まっているとします。

コピペでそのまま動くサンプルは以下です。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: Scaffold(
          appBar: AppBar(
            title: Text('Wrapのサンプルコード'),
          ),
          body: body(),
        ));
  }
}

Widget body() {
  return Container(
    color: Colors.lightBlue,
    padding: EdgeInsets.all(20.0),
    child: Row(
      children: <Widget>[
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
      ],
    ),
  );
}

Container marginedColorContainer(
    {EdgeInsets margin = const EdgeInsets.all(10.0),
    Color color = Colors.yellow}) {
  return Container(
    margin: margin,
    width: 100.0,
    height: 100.0,
    color: color,
  );
}

実行結果は以下のようになります。

Rows の children にもう一つ Container を追加してみます。画面幅に収まらない想定をしています。

実行してみると収まらない部分に関する警告が表示されているのが確認できます。

A RenderFlex overflowed by 88 pixels on the right.と表示されています。

════════ Exception caught by rendering library ═════════════════════════════════════════════════════
The following assertion was thrown during layout:
A RenderFlex overflowed by 88 pixels on the right.

The relevant error-causing widget was:
  Row ../wrap_playgrounds/lib/main.dart:27:12
The overflowing RenderFlex has an orientation of Axis.horizontal.
The edge of the RenderFlex that is overflowing has been marked in the rendering with a yellow and black striped pattern. This is usually caused by the contents being too big for the RenderFlex.

Consider applying a flex factor (e.g. using an Expanded widget) to force the children of the RenderFlex to fit within the available space instead of being sized to their natural size.
This is considered an error condition because it indicates that there is content that cannot be seen. If the content is legitimately bigger than the available space, consider clipping it with a ClipRect widget before putting it in the flex, or using a scrollable container rather than a Flex, like a ListView.

The specific RenderFlex in question is: RenderFlex#9ff89 relayoutBoundary=up3 OVERFLOWING
...  parentData: offset=Offset(20.0, 20.0) (can use size)
...  constraints: BoxConstraints(0.0<=w<=392.0, 0.0<=h<=696.0)
...  size: Size(392.0, 120.0)
...  direction: horizontal
...  mainAxisAlignment: start
...  mainAxisSize: max
...  crossAxisAlignment: center
...  textDirection: ltr
...  verticalDirection: down

エラーメッセージを読むと Axis.horizontal な方向にオーバーフローしていて、その原因は RenderFlex に対して大きすぎるコンテンツが原因と記載されています。RenderFlex のエッジは黄色と黒の縞模様でマーキングされている、と記載されていますが先程の実行結果を見るとたしかにエッジが確認できます。これに内部のコンテンツが収まらなかったためにエラーを表示しているということでしょう。

そこで Wrap を使用します。オーバーフローしている方向は Axis.horizontal なので、Wrap のコンストラクタ引数 direction には Axis.horizontal を指定したいところですがソースを読むとデフォルト値が Axis.horizontal なので今回は指定する必要はないです。

Widget body() {
  return Container(
    color: Colors.lightBlue,
    padding: EdgeInsets.all(20.0),
    child: Wrap(
      direction: Axis.horizontal,
      children: <Widget>[
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(), // 追加した
      ],
    ),
  );
}

収まりきらない分は次の行・列に折り返してもう一つ行・列を作ってコンテンツを配置します。これにより先程の警告は表示されなくなりました。

Wrap のその他主要なプロパティ

ソースを見ずとも Android Studio で Quick Documentation を使うとコンストラクタとデフォルト値が確認できると知りました。

(new) Wrap Wrap({Key key, Axis direction = Axis.horizontal, WrapAlignment alignment = WrapAlignment.start, double spacing = 0.0, WrapAlignment runAlignment = WrapAlignment.start, double runSpacing = 0.0, WrapCrossAlignment crossAxisAlignment = WrapCrossAlignment.start, TextDirection textDirection, VerticalDirection verticalDirection = VerticalDirection.down, List<Widget> children = const <Widget>[]})

direction

direction の型は Axis です。Axis は列挙型として宣言されていて上下を表す vertical、左右を表す horizontal が用意されています。最初のサンプルではデフォルト値の Axis.horizontal になるので横方向ですが Axis.vertical を指定してみます。

Widget body() {
  return Container(
    color: Colors.lightBlue,
    padding: EdgeInsets.all(20.0),
    child: Wrap(
      direction: Axis.vertical, // 追加した。directionを指定
      children: <Widget>[
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
      ],
    ),
  );
}

実行結果は以下です。要素が縦方向に配置されています。

space

space の型は double 型ですが、現状のコードだと最初からマージンが設定されているのでコードを修正します。marginedColorContainer メソッドは Optional Default Parameters Function なのでデフォルト値を持つ引数によりコンストラクタ引数に値をセットする必要がありません。

今回は明示的に EdgeInsets.all(0)をセットしてマージンをリセットします。

Widget body() {
  return Container(
    color: Colors.lightBlue,
    padding: EdgeInsets.all(20.0),
    child: Wrap(
      direction: Axis.vertical,
      children: <Widget>[
        marginedColorContainer(margin: const EdgeInsets.all(0)),
        marginedColorContainer(margin: const EdgeInsets.all(0)),
        marginedColorContainer(margin: const EdgeInsets.all(0)),
        marginedColorContainer(margin: const EdgeInsets.all(0)),
        marginedColorContainer(margin: const EdgeInsets.all(0)),
        marginedColorContainer(margin: const EdgeInsets.all(0)),
        marginedColorContainer(margin: const EdgeInsets.all(0)),
        marginedColorContainer(margin: const EdgeInsets.all(0)),
        marginedColorContainer(margin: const EdgeInsets.all(0)),
        marginedColorContainer(margin: const EdgeInsets.all(0)),
      ],
    ),
  );
}

表示は以下のようになります。マージンゼロなのでつながっているように見えます。

上記のコードの Wrap に spacing を指定してみます。すると要素とその次の要素の間にスペースができるのが確認できます。

Widget body() {
  return Container(
    color: Colors.lightBlue,
    padding: EdgeInsets.all(20.0),
    child: Wrap(
      direction: Axis.vertical,
      spacing: 10.0, // 追加した
      children: <Widget>[
        marginedColorContainer(margin: const EdgeInsets.all(0)),
        marginedColorContainer(margin: const EdgeInsets.all(0)),
        marginedColorContainer(margin: const EdgeInsets.all(0)),
        marginedColorContainer(margin: const EdgeInsets.all(0)),
        marginedColorContainer(margin: const EdgeInsets.all(0)),
        marginedColorContainer(margin: const EdgeInsets.all(0)),
        marginedColorContainer(margin: const EdgeInsets.all(0)),
        marginedColorContainer(margin: const EdgeInsets.all(0)),
        marginedColorContainer(margin: const EdgeInsets.all(0)),
        marginedColorContainer(margin: const EdgeInsets.all(0)),
      ],
    ),
  );
}

runSpace

runSpace は Wrap の行・列ごとの間のスペースを指定するためのプロパティです。

runSpace を指定しない例として以下のコードを記述します。

Widget body() {
  return Container(
    color: Colors.lightBlue,
    padding: EdgeInsets.all(20.0),
    child: Wrap(
      children: <Widget>[
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
      ],
    ),
  );
}

実行結果は以下です

次は runSpace を指定してみます。

Widget body() {
  return Container(
    color: Colors.lightBlue,
    padding: EdgeInsets.all(20.0),
    child: Wrap(
      runSpacing: 30.0, // 追加した
      children: <Widget>[
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
      ],
    ),
  );
}

実行結果は以下になります。行と行の間に先程より広いスペースが与えられていることが確認できます。

alignment

メイン軸の要素をどのように配置するか指定します。型は WrapAlignment でデフォルト値は start です。

Widget body() {
  return Container(
    color: Colors.lightBlue,
    padding: EdgeInsets.all(20.0),
    child: Wrap(
      alignment: WrapAlignment.start, // ここを変更していく
      children: <Widget>[
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(),
        marginedColorContainer(), // 追加した
      ],
    ),
  );
}

start

end

center

spaceAround

spaceBetween

spaceEvenly

まとめ

Wrap頻繁に使うので改めて動画、ドキュメントをみつつコードを手元で動かしながら記事がかけてよかったです。実はWrapの挙動でまだ腑に落ちてないプロパティがあって、それは長くなりそうなので別途記事にしようと思います。記事内で何か誤りや説明不足な点があればご指摘いただけるとありがたいです。