Chapter 12

  Everything is a Widget

heyhey1028
heyhey1028
2023.02.19に更新

さてシミュレーターで立ち上げたアプリを参考に Flutter の UI を構成する基本的な概念について理解していきましょう。

main.dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

Widget とそのツリー構造

実際の UI を作っていく上で、最も重要なのが Widget です。Flutter では、UI を構成する各要素を Widget と呼びます。画面上に表示される UI コンポーネントのみならず、その配置や効果を与えるものなど UI に関わる様々な要素の Widget が存在します。

それらの Widget は下記のようなツリー構造をとる事で UI を構成していきます。Widget は、他の Widget を子要素として持つことができます。その子要素が更に子要素を持ち、またその子要素が...というように入れ子構造に Widget が繋がっていき ツリー構造を形成します。

サンプルアプリの以下の画面を例に見てみましょう。

この UI を構成する Widget ツリーの定義は以下のようになっています。

コード
    ...
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
    ...

こちらの記述ではScaffold Widget の子要素としてappBarbodyfloatingActionButtonのフィールドにそれぞれ Widget ツリーが指定されています。

appBarにはAppBarTextからなる widget ツリー、
bodyにはCenterColumn→ 2つのTextからなる Widget ツリー、
floatingActionButtonにはFloatingActionButtonIconからなる Widget ツリーが指定されています。

これを図式化すると以下のようツリー構造になります。

この Widget ツリーに従って先ほどの画面を分解してみると以下のようになります。

大元となるScaffoldwidget の中にAppBarCenterFloatingActionButtonが配置され、更にその中で子要素となるTextIconが配置されています。

StatelessWidget と StatefulWidget

構築した Widget ツリーはそのままでは描画されません。 そこで登場するのが StatelessWidgetクラスStatefulWidgetクラス です。これらは build というメソッドを持っており、このメソッド内で Widget をreturnすることで Widget の描画が行われます。

StatelessWidget
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return WidgetA(); // WidgetA を描画
  }
}

StatelessWidgetStatefulWidgetは抽象クラスの為、上記のようにこれらを継承したクラスを定義し、buildメソッドを実装します。

StatelessWidgetStatefulWidget の違いは、StatelessWidgetは状態を持たない一方で、StatefulWidgetは状態を持つことができるという点です。状態を持つことができるStatefulWidgetは、状態を変更することで UI を更新することができます。

状態を持つという事

「状態を持つ」ということを理解する為にサンプルアプリの StatefulWidgetを継承している MyHomePageクラスを見てみましょう。

MyHomePage
// StatefulWidgetクラス
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

// Stateクラス
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

こちらの記述を見ていただくとMyHomePageクラスは StatefulWidgetクラスStateクラス に分かれていることがわかります。

StateクラスはStatefulWidgetcreateStateメソッドで返され、この2つのクラスは必ず対になっている必要があります。

このStateクラスが状態を管理するクラスです。Stateクラスは 自身が持つ変数の更新を検知すると、自身のbuildメソッドを再度呼び出す という機能を持っています。

サンプルアプリの例ではStateクラスは_counterという変数を持っています。初期値は0です。

  int _counter = 0;

この変数は同クラス内の_incrementCounterというメソッド内で呼ばれているsetStateメソッドで更新する事ができます。

  void _incrementCounter() {
    setState(() {
      _counter++; // _counterの値をインクリメント
    });
  }

すると変数の値が更新され、それを検知した State クラスが再度 build メソッドを呼びます。これにより新しい Widget ツリーが描画されます。

build メソッド
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }

その結果、画面上で_counterの値を表示しているTextwidget が0の代わりに更新後の1を表示します。

再描画で Widget ツリーに起きていること

このように、状態の更新によって Widget が更新されることを「再描画」と呼びます。

この再描画が行われる際、Flutter では Widget ツリー の一部が更新されるのではなく、buildメソッド以下の Widget ツリーが丸ごと破棄され、再生成されます

Widget は軽量なオブジェクトの為、状態変化のたびに再生成されることが前提となっています。しかし Widget ツリーがより大きくなると、再描画の度に再生成するのはコストが高くなってしまい、描画パフォーマンスの低下を招くことになります。

その為、Widget ツリーを複数のクラスに適切に分割し、状態更新により再描画されるスコープを小さくすることが重要です。

まとめ

この章では Flutter の UI を構成する Widget の基本的な概念について学びました。

実際の UI 構築には Widget ツリーと合わせて Element ツリー、RenderObject ツリーという仕組みが裏で関わっています。Flutter をより深く理解したい方はぜひ公式のドキュメントを参照してみてください。

https://docs.flutter.dev/resources/architectural-overview#rendering-and-layout

次の章では Widget のサイズがどのように計算されるのかを学びます。