【Flutter/Dart】Widgetとは?StatelessWidgetとStatefulWidgetの違いと使い方

この記事からわかること
- Flutter/DartのWidgetとは?
- Widgetのツリー(階層)構造の仕組み
- buildメソッドの役割
- StatelessWidgetとStatefulWidgetの違い
index
[open]
\ アプリをリリースしました /
環境
- Android Studio:Koala
- Xcode:16.0
- Flutter:3.29.2
- Dart:3.7.2
- Mac M1:Sequoia 15.4
Widgetとは?
Flutterの「Widget」とはFlutterアプリのUIを構成する基本的な要素です。ボタンやテキスト、画像、レイアウトなどUIに関わる要素がWidgetとして扱われており、それらを組み合わせることでアプリが完成します。
またiOSやAndroidなどと同じようにWidgetもツリー(階層)構造で管理されています。基本的に親Widgetがあり、親Widgetが子Widgetを持つという構造になっています。例えば以下のようなUIが実装されているとします。
Scaffold(
appBar: AppBar(title: Text('Flutter App')), // AppBarウィジェット
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Hello, Flutter!'), // Textウィジェット
ElevatedButton(
onPressed: () {},
child: Text('Click Me'), // ボタン内のTextウィジェット
),
],
),
),
)
その場合、ツリー構造はScaffold
をルートとして子どもにAppBar
やCenter
を保持しているような構造になります。
Scaffold
├── AppBar
│ └── Text('Flutter App')
└── Center
└── Column
├── Text('Hello, Flutter!')
└── ElevatedButton
└── Text('Click Me')
Widget自体の構造
例としてText Widget
を参考に構造を確認してみます。Widgetの実態はクラス
でありStatelessWidget
(後述します)を継承しています。中には初期化するためのコンストラクタと実際にWidgetを構築して返却するbuild
メソッドが記述されています。
class Text extends StatelessWidget {
/// コンストラクタ(初期化する際に色々な引数を受け取れる)
const Text(
String this.data, {
super.key,
this.style,
this.textAlign,
...
});
/// 表示する文字列
final String? data;
/// テキストのスタイル(フォントサイズ、色など)
final TextStyle? style;
/// UI(Widget)を構築する
@override
Widget build(BuildContext context) {
TextStyle? effectiveTextStyle = style;
Widget result = RichText(
text: TextSpan(
style: effectiveTextStyle,
text: data,
),
);
}
}
Widgetの種類
Widget
は大きく分けてStatelessWidget
とStatefulWidget
に分かれます。両者の違いは名前の通り状態(State)を保持するか否かで、Flutterでは抽象クラスとして定義されています。そのためどちらかを継承した状態でWidget
は定義されます。
StatelessWidget
- 状態を保持しないWidget
- buildメソッド実行時に常に同じUIを描画
- 画面の更新が必要ない、静的なUIに適している
StatelessWidget
は変化のない固定のUIを構築する際に活用されます。例えばヘッダーのタイトルやボタンタイトルなど更新することのない箇所にはStatelessWidget
を使用します。
StatelessWidget
はシンプルにコンストラクタとbuild
メソッドが実装されているだけです。
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
StatefulWidget
- 内部に状態を保持するWidget
- 再描画を行うためのsetStateメソッドが定義
- UI更新のある、動的なUIに適している
StatefulWidget
は頻繁に更新したいUIをを構築する際に活用されます。例えばカウンターや入力フォームなど任意のトリガーにおいてUIを更新したい箇所にはStatefulWidget
を使用します。
例えば以下は「ボタンを押下するごとにカウンターが増加するUI」です。この一連のWidgetはStatefulWidget
を継承したクラスとState<MyHomePage>
を継承したクラスの2つが存在することがわかります。
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// 状態を保持
int _counter = 0;
void _incrementCounter() {
setState(() {
// 状態を更新
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
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.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
コードを少し分解しながら読み解いていきます。まずはStatefulWidget
を継承しているクラスです。createState
メソッドをオーバーライドして任意のState
インスタンスを作成し、状態を管理します。つまり状態を保持し、更新処理を行うのはStateクラスが実態ということになります。
class MyHomePage extends StatefulWidget {
// コンストラクタ
const MyHomePage({super.key, required this.title});
// 呼び出し時に指定されるタイトル
final String title;
// Stateインスタンスを作成
@override
State<MyHomePage> createState() => _MyHomePageState();
}
次は先ほど定義したWidget型を管理ためのState<MyHomePage>
を継承したクラスです。State
クラスもbuild
メソッドを保持しており、ここでUIの構築を行います。ポイントになるのは状態を保持するための変数_counter
とsetState
メソッドです。setState
メソッドは状態を更新するためのメソッドでこの中で状態を更新することでWidgetが再描画される(buildメソッドが再度呼ばれる)ようになります。
class _MyHomePageState extends State<MyHomePage> {
// 状態を保持
int _counter = 0;
void _incrementCounter() {
setState(() {
// 状態を更新
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
// 〜〜〜〜〜〜省略
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
// 状態をUIと紐付け
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
// ボタンに`setState`で状態を更新する`_incrementCounter`メソッドを紐付け
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
あとはUI側と紐づけるために$_counter
でText
と紐付け、ボタン押下で状態が変化するようにFloatingActionButton
に_incrementCounter
メソッドを紐付けています。これでボタンを押下して状態が変化すると即座にUIに反映される実装が完了しました。

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。