Flutter で仕事したい人のための Widget ツリー入門

ご存知の通り、 Flutter は Widget を入れ子の構造で記述することで UI を構築します。

```dart
Center(
  child: Column(
    children: const <Widget>[
      Text('Hello, Flutter!'),
      SizedBox(height: 16),
      Text('This is my first app.'),
    ],
  ),
),
```

Flutter の Widget 同士の関係は、このようなソースコードの形状から「入れ子」の構造をイメージしがちですが、 Flutter の仕組みを理解する上では、このソースコードから Widget の __ツリー構造__ がイメージできるようになっておくと何かと役に立ちます。

この記事では、Flutter の仕組みを説明する上でよく使われる __「Widget ツリー」__ について、初学者向けに基本から解説していきたいと思います。

# なぜ Widget ツリーを理解するのか

例えば「中央寄せしたければ `Center` で囲う」のように簡単な UI を構築するだけであれば Flutter が用意してくれている色々な Widget とそのプロパティの使い方を理解すれば作れてしまうのが Flutter の良いところです。

しかし一方で、 `Snackbar` を表示しようと思ったら `Scaffold.of() called with a context that does not contain a Scaffold` エラーが発生した、複数の画面でデータを共有する方法を調べたら `InheritedWidget` や `Provider` といった知らない仕組みが出てきて混乱した、など、 __少し込み入ったアプリを作ろうと思ったら「こう書けば、こう表示される」の対応表だけではやりたいことを実現できない__ 場合も少なくありません。

これは、Flutter が提供するほとんどの Widget が「Widget のツリー構造」を前提に設計されているためです。

逆に言うと、この __Widget のツリー構造がイメージできるようになれば、より効率的に Flutter に対する理解を深めることができ、「Flutter の考え方」に則って安全で無駄のないアプリが開発できるようになる__ はずです。

# Widget ツリーを組み立てる

では、先ほどのサンプルコードを使いながら Widget ツリーがどのように組み立てられるのかを見ていきましょう。

## 基本は `child` / `children` プロパティに指定した通り

`Text` や `Image` といった一部の Widget を除き、 Flutter が提供するほとんどの Widget には `child` または `children` プロパティが用意されていて、　Widget ツリーの構造もそれに従った形になります。

例えば、以下のようにサンプルコード（再掲）があったとき

```dart
Center(
  child: Column(
    children: const <Widget>[
      Text('Hello, Flutter!'),
      SizedBox(height: 16),
      Text('This is my first app.'),
    ],
  ),
),
```

以下の図のような Widget ツリーができあがります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134943/5ae9f92c-d52d-af24-1bb0-74b635f76434.png)

- ソースコードの先頭に `Center` があるためツリーの先頭は `Center` が配置され、
- `Center` の `child` プロパティに `Column` が渡されているため、ツリー上も `Center` の下（子 Widget）に `Column` が配置され、
- 以下同様に `Column` の子 Widget として `children` プロパティの `Text`, `SizedBox`, `Text` が配置され、

という具合です。

まずはこれが Widget ツリーのイメージの基本となります。ソースコードと見比べて Widget ツリーのイメージを脳内で変換できるようになると良いでしょう。

## StatelessWidget が入る場合

さて、以下のように `child`（もしくは `children`） に `StatelessWidget` が渡された場合はどうなるでしょうか。

```dart
Center(
  child: Column(
    children: <Widget>[
      Text('Hello, Flutter!'),
      SizedBox(height: 16),
      UserCard(),  // 自作の StatelessWidget
    ],
  ),
),

.. 省略..

// ユーザーのアイコンと名前を横並びにした Widget
class UserCard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Icon(Icons.people),
        Text('User name'),
      ],
    );
  }
}

```

このソースコードを Widget ツリーで表すと以下のようになります。（前の例と変わらない部分は半透明にしてあります）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134943/8ef8826f-9b74-e3ce-2c46-678a80a4c4f3.png)

見ての通り、自分で作った `UserIcon` もひとつの Widget としてツリーに組み込まれ、その子 Widget には `build()` メソッドで `return` した Widget（この例では `Row`）が続く形になっています。

Widget ツリーにおいて、 __子 Widget は必ずしも `child` / `children` で指定したものとは限らない__ という点は覚えておいてください。

## StatefulWidget をはさんだ場合

では、 `StatefulWidget` はどうでしょうか。以下のコードの場合を考えてみましょう。

```dart
Center(
  child: Column(
    children: <Widget>[
      Text('Hello, Flutter!'),
      SizedBox(height: 16),
      Counter(),  // 自作 StatefulWidget
    ],
  ),
),

.. 省略..

// シンプルなカウンター Widget
class Counter extends StatefulWidget {
  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  var _counter = 0;
  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Text('$_counter'),
        IconButton(
          icon: Icon(Icons.add),
          onPressed: () => setState(() => _counter++),
        ),
      ],
    );
  }
}

```

このコードを Widget ツリーで表すと以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134943/98bf5558-e8c8-687a-4952-bd07737caba8.png)

ということで、 StatelessWidget の場合と特に変わらないことが分かるかと思います。

一点だけ注意しなければならないのは、 StatefulWidget の場合は `build()` メソッドが　Widget 自身（この例では `Counter`）ではなく `State`（この例では `_CounterState`）のに定義されている、という点です。

`child` や `children` でも Widget の `build()` でもなく、StatefulWidget の場合は `State` の `build()` メソッドが `return` した Widget が自身の子 Widget となります。

# Widget ツリーを構築する Element （一歩踏み込んだ話）

というわけで、Flutter のソースコードからどのように Widget ツリーが構築されるかをざっと見てみました。

特に、ある Widget の子 Widget がどう決まるかは

- `child` / `children` プロパティに指定した Widget
- Widget の `build()` メソッドで返却した Widget （StatelessWidget の場合）
- State の `build()` メソッドで返却した Widget （StatefulWidget の場合）

などいくつかのパターンはあるものの、概ね Flutter で書いた Widget の入れ子がそのままツリー構造になっていることがイメージできたのではないかと思います。

さて、ここにリストアップした「この場合は、この Widget を子として配置する」の判断はどこでやっているのでしょうか。

その答えは、 Flutter においてとても重要な役割を持っている `Element` というクラスのソースコードに書かれています。

実はここまで説明してきた「Widget ツリー」というのはあくまで「イメージ」であり、必ずしも Widget 同士が親（もしくは子） Widget を参照しているわけではありません。

実際は、 Widget １つひとつが `Element` という別のクラスと紐づいており、 Element 同士が親子の参照を保持する「Element ツリー」を構築することで Flutter における「ツリー構造」が成り立っています。[^1] その様子を表したのが以下のイメージです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134943/4a3ac46a-fdde-c69c-d18d-bcfc3a4edc30.png)

この「Elementツリー」を理解することは Flutter のより深い理解につながりますが、一方で Flutter は __"Everything is a Widget"__ のスローガンの通り Widget さえ理解していればある程度のアプリが開発できるようにデザインされたフレームワークですので、今の段階で Element まで踏み込む必要はないでしょう。

もしここまでの内容が問題なく理解できて、さらに Element についても理解したいという方向は、Element ツリーについて詳しく解説した以下の記事（と、その参考記事）を読んでみてください。

[【Flutter】Navigator.of(context) から理解する 3つのツリー | Zenn](https://zenn.dev/chooyan/articles/77a2ba6b02dd4f)

# 実は Text は StatelessWidget 

さて、これで Widget ツリーの基本を一通り説明してきましたが、最後に1つ、 __Text は StatelessWidget のサブクラスである__ という点について説明します。

詳しくは別記事 [【Flutter】Text とは何か](https://zenn.dev/chooyan/articles/d48faae7352e32) を読んでいただければと思いますが、 Text も StatelessWidget のサブクラスですので、 [StatelessWidget をはさんだ場合](https://qiita.com/chooyan_eng/items/7c7298795f2db24b24a5#statelesswidget-%E3%81%8C%E5%85%A5%E3%82%8B%E5%A0%B4%E5%90%88) で説明した通り `build()` メソッドの中で他のいくつかの Widget を組み立てて `return` し、それが Widget ツリーに反映されています。

そのことを考慮して最初の Widget ツリーの図を正確に書くと以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134943/b0995878-31e8-42ed-2a72-6fdb1a8af285.png)

図の通り、 Text の下に `Semantics` と `ExcludeSemantics`、さらに `RichText` といった別の Widget がつながっていることが読み取れます。

また、 `Semantics` と `ExcludeSemantics` は Text の `semanticsLabel` プロパティに何も指定しなかった場合は省略するような記述も、 Text の `build()` メソッドの処理として書かれています。

このように、一見ひとつの Widget でも、実はその Widget が `build()` メソッドを持っていて（つまり StatelessWidget / StatefulWidget のサブクラスで）、私たちアプリ開発者が作る StatelessWidget / StatefulWidget と同様、他のいくつもの Widget を組み合わせてツリー上に配置する場合が少なからず存在します。例えば `MaterialApp` や `Scaffold` などは、大量の Widget を `build()` で返却する Widget の例です。

ただし、ここまで正確に Widget ツリーを把握しなければならない場面はそれほどありません。開発ツール等で実際の Widget ツリーを細かく確認するようになったときに、「Flutter 標準の Widget は他の Widget の組み合わせでできている場合がある」ことを思い出す程度で良いでしょう。

# まとめ

多くの初学者にとって Flutter で UI の構築方法を学ぶ際、まずやるのは「どの Widget をどのように書けばどう表示されるか」を体験することだと思います。

一方で、Flutter の仕組みを少し詳しく知ろうと思ったとき、公式ドキュメントを含めほとんどの記事は Widget がツリー構造であることを前提に説明されています。また、 `Provider` を使ったり `Navigator` で画面遷移する仕組みなどもこの Widget ツリーについてのイメージがあるかどうかで理解度が大きく変わります。

まずは自分の書いたプログラムがどのような Widget ツリーを構築するのかイメージできるようになることで、そのような「一歩踏み込んだ」説明を理解する土台を作ると、学習効率の観点からも良いと思います。

[^1]: 話が複雑になるため省略していますが、Element にもいくつかの種類があります。