13
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

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

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

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 エラーが発生した、複数の画面でデータを共有する方法を調べたら InheritedWidgetProvider といった知らない仕組みが出てきて混乱した、など、 少し込み入ったアプリを作ろうと思ったら「こう書けば、こう表示される」の対応表だけではやりたいことを実現できない 場合も少なくありません。

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

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

Widget ツリーを組み立てる

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

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

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

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

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

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

image.png

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

という具合です。

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

StatelessWidget が入る場合

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

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

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

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

StatefulWidget をはさんだ場合

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

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

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

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

childchildren でも Widget の build() でもなく、StatefulWidget の場合は Statebuild() メソッドが 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 1つひとつが Element という別のクラスと紐づいており、 Element 同士が親子の参照を保持する「Element ツリー」を構築することで Flutter における「ツリー構造」が成り立っています。1 その様子を表したのが以下のイメージです。

image.png

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

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

【Flutter】Navigator.of(context) から理解する 3つのツリー | Zenn

実は Text は StatelessWidget

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

詳しくは別記事 【Flutter】Text とは何か を読んでいただければと思いますが、 Text も StatelessWidget のサブクラスですので、 StatelessWidget をはさんだ場合 で説明した通り build() メソッドの中で他のいくつかの Widget を組み立てて return し、それが Widget ツリーに反映されています。

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

image.png

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

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

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

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

まとめ

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

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

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


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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
13
Help us understand the problem. What are the problem?