さてシミュレーターで立ち上げたアプリを参考に 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 の子要素としてappBar
、body
、floatingActionButton
のフィールドにそれぞれ Widget ツリーが指定されています。
appBar
にはAppBar
→Text
からなる widget ツリー、
body
にはCenter
→Column
→ 2つのText
からなる Widget ツリー、
floatingActionButton
にはFloatingActionButton
→Icon
からなる Widget ツリーが指定されています。
これを図式化すると以下のようツリー構造になります。
この Widget ツリーに従って先ほどの画面を分解してみると以下のようになります。
大元となるScaffold
widget の中にAppBar
、Center
、FloatingActionButton
が配置され、更にその中で子要素となるText
やIcon
が配置されています。
StatelessWidget と StatefulWidget
構築した Widget ツリーはそのままでは描画されません。 そこで登場するのが StatelessWidget
クラス と StatefulWidget
クラス です。これらは build
というメソッドを持っており、このメソッド内で Widget をreturn
することで Widget の描画が行われます。
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return WidgetA(); // WidgetA を描画
}
}
StatelessWidget
とStatefulWidget
は抽象クラスの為、上記のようにこれらを継承したクラスを定義し、build
メソッドを実装します。
StatelessWidget
と StatefulWidget
の違いは、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
クラスはStatefulWidget
のcreateState
メソッドで返され、この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
の値を表示しているText
widget が0
の代わりに更新後の1
を表示します。
再描画で Widget ツリーに起きていること
このように、状態の更新によって Widget が更新されることを「再描画」と呼びます。
この再描画が行われる際、Flutter では Widget ツリー の一部が更新されるのではなく、build
メソッド以下の Widget ツリーが丸ごと破棄され、再生成されます。
Widget は軽量なオブジェクトの為、状態変化のたびに再生成されることが前提となっています。しかし Widget ツリーがより大きくなると、再描画の度に再生成するのはコストが高くなってしまい、描画パフォーマンスの低下を招くことになります。
その為、Widget ツリーを複数のクラスに適切に分割し、状態更新により再描画されるスコープを小さくすることが重要です。
まとめ
この章では Flutter の UI を構成する Widget の基本的な概念について学びました。
実際の UI 構築には Widget ツリーと合わせて Element ツリー、RenderObject ツリーという仕組みが裏で関わっています。Flutter をより深く理解したい方はぜひ公式のドキュメントを参照してみてください。
次の章では Widget のサイズがどのように計算されるのかを学びます。