Flutter StatelessWidget vs StatefulWidget を深く理解する
StatelessWidget と StatefulWidget の違いを理解することは Flutter 開発の基礎です。
このセクションでは、これら2種類の Widget の使用シーンと内部の動作メカニズムを詳しく分析します。
核心的な違い
Flutter には主に2種類の Widget があります。それらの根本的な違いは、可変状態を保持できるかどうかです。
| 特性 | StatelessWidget | StatefulWidget |
|---|---|---|
| 状態 | 不変(immutable) | 可変(mutable) |
| 再構築のタイミング | 親 Widget が再構築された時のみ | 状態が変化した時 + 親 Widget の再構築 |
| パフォーマンス | より高い(状態を監視する必要がない) | やや低い(状態管理が必要) |
| 使用シーン | 静的コンテンツ | 動的なインタラクティブコンテンツ |
StatelessWidget 詳細解説
StatelessWidget は、時間とともに変化しないコンテンツに適しています。一度作成されると、そのすべてのプロパティは final となり、変更できません。
使用シーン
- 静的テキスト、画像、アイコンの表示
- データリストの表示(データソースは親 Widget から)
- ユーザーインタラクションを含まないレイアウトコンテナ
- フォーム内の読み取り専用表示部分
実例:StatelessWidget 完全なサンプル
import 'package:flutter/material.dart';
// ユーザー情報表示コンポーネント(ステートレス)
class UserCard extends StatelessWidget {
// プロパティを final として定義し、不変を保証
final String name;
final String email;
final String avatarUrl;
// const コンストラクタでパフォーマンス向上
const UserCard({
super.key,
required this.name,
required this.email,
this.avatarUrl = '',
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
// アバター
CircleAvatar(
backgroundImage: avatarUrl.isNotEmpty
? NetworkImage(avatarUrl)
: null,
child: avatarUrl.isEmpty
? const Icon(Icons.person)
: null,
),
const SizedBox(width: 16),
// ユーザー情報
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
email,
style: TextStyle(
color: Colors.grey[600],
),
),
],
),
),
],
),
),
);
}
}
// ユーザー情報表示コンポーネント(ステートレス)
class UserCard extends StatelessWidget {
// プロパティを final として定義し、不変を保証
final String name;
final String email;
final String avatarUrl;
// const コンストラクタでパフォーマンス向上
const UserCard({
super.key,
required this.name,
required this.email,
this.avatarUrl = '',
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
// アバター
CircleAvatar(
backgroundImage: avatarUrl.isNotEmpty
? NetworkImage(avatarUrl)
: null,
child: avatarUrl.isEmpty
? const Icon(Icons.person)
: null,
),
const SizedBox(width: 16),
// ユーザー情報
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
email,
style: TextStyle(
color: Colors.grey[600],
),
),
],
),
),
],
),
),
);
}
}
StatefulWidget 詳細解説
StatefulWidget は、ユーザーインタラクションやデータ変化に応答する必要があるシーンに適しています。これは Widget 自体(不変)と State オブジェクト(可変)の2つの部分で構成されます。
使用シーン
- カウンター、タイマーなどの動的データ
- フォーム入力
- アニメーション制御
- ネットワークリクエストの状態表示
- ユーザーインタラクションへの応答
State オブジェクトのライフサイクル
| メソッド | 説明 |
|---|---|
| initState | Widget が最初に作成されたときに呼び出され、状態を初期化します。 |
| didChangeDependencies | Widget の依存関係が変化したときに呼び出されます。 |
| build | UI を構築します。状態が変化するたびに呼び出されます。 |
| didUpdateWidget | 親 Widget が再構築されたときに呼び出されます。 |
| dispose | Widget が破棄されるときに呼び出され、リソースをクリーンアップします。 |
実例:StatefulWidget 完全なサンプル
import 'package:flutter/material.dart';
// カウンターアプリ
class CounterApp extends StatefulWidget {
const CounterApp({super.key});
@override
State<CounterApp> createState() => _CounterAppState();
}
class _CounterAppState extends State<CounterApp> {
// カウンターの状態
int _counter = 0;
// タイマー(dispose のデモ用)
Timer? _timer;
// 1. initState - 状態の初期化
@override
void initState() {
super.initState();
// カウンターの初期化
_counter = 0;
// タイマーを開始(毎秒1回増加)
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
_increment();
});
}
// 2. didChangeDependencies - 依存関係が変化したときに呼び出される
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
// 3. build - UI の構築
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('计数器'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'当前计数:',
style: TextStyle(fontSize: 20),
),
Text(
'$_counter',
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
),
),
],
),
),
// 増加ボタン
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: const Icon(Icons.add),
),
);
}
// カウンターを増加させるメソッド
void _increment() {
// setState を呼び出して UI 更新をトリガーする必要がある
setState(() {
_counter++;
});
}
// 4. didUpdateWidget - 親 Widget が再構築されたときに呼び出される
@override
void didUpdateWidget(CounterApp oldWidget) {
super.didUpdateWidget(oldWidget);
}
// 5. dispose - リソースのクリーンアップ
@override
void dispose() {
// タイマーをキャンセルし、メモリリークを防ぐ
_timer?.cancel();
super.dispose();
}
}
// カウンターアプリ
class CounterApp extends StatefulWidget {
const CounterApp({super.key});
@override
State<CounterApp> createState() => _CounterAppState();
}
class _CounterAppState extends State<CounterApp> {
// カウンターの状態
int _counter = 0;
// タイマー(dispose のデモ用)
Timer? _timer;
// 1. initState - 状態の初期化
@override
void initState() {
super.initState();
// カウンターの初期化
_counter = 0;
// タイマーを開始(毎秒1回増加)
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
_increment();
});
}
// 2. didChangeDependencies - 依存関係が変化したときに呼び出される
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
// 3. build - UI の構築
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('计数器'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'当前计数:',
style: TextStyle(fontSize: 20),
),
Text(
'$_counter',
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
),
),
],
),
),
// 増加ボタン
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: const Icon(Icons.add),
),
);
}
// カウンターを増加させるメソッド
void _increment() {
// setState を呼び出して UI 更新をトリガーする必要がある
setState(() {
_counter++;
});
}
// 4. didUpdateWidget - 親 Widget が再構築されたときに呼び出される
@override
void didUpdateWidget(CounterApp oldWidget) {
super.didUpdateWidget(oldWidget);
}
// 5. dispose - リソースのクリーンアップ
@override
void dispose() {
// タイマーをキャンセルし、メモリリークを防ぐ
_timer?.cancel();
super.dispose();
}
}
setState の正しい使い方
StatefulWidget では、setState を通じて Flutter に状態が変更されたことを通知する必要があります。
正しい使い方
実例:正しい setState の使い方
// 正しい:setState 内で状態を更新する
void _increment() {
setState(() {
_counter++;
});
}
// 正しい:状態更新が UI と無関係な場合は setState を使わなくてもよい
void _someInternalLogic() {
// 内部ロジック、UI に影響しない
_calculateSomething();
}
// 正しい:非同期操作の後に状態を更新する
Future<void> _loadData() async {
final data = await fetchData();
setState(() {
_items = data;
});
}
void _increment() {
setState(() {
_counter++;
});
}
// 正しい:状態更新が UI と無関係な場合は setState を使わなくてもよい
void _someInternalLogic() {
// 内部ロジック、UI に影響しない
_calculateSomething();
}
// 正しい:非同期操作の後に状態を更新する
Future<void> _loadData() async {
final data = await fetchData();
setState(() {
_items = data;
});
}
誤った使い方
実例:誤った setState の使い方
// 誤り:状態を変更するが setState を呼び出さない
void _increment() {
_counter++; // 状態は変わったが、UI は更新されない!
}
// 誤り:build メソッド内で setState を呼び出す
@override
Widget build(BuildContext context) {
// build 内で setState を絶対に呼び出さないこと!
setState(() {
_counter++; // これは無限ループを引き起こす
});
return Text('$_counter');
}
void _increment() {
_counter++; // 状態は変わったが、UI は更新されない!
}
// 誤り:build メソッド内で setState を呼び出す
@override
Widget build(BuildContext context) {
// build 内で setState を絶対に呼び出さないこと!
setState(() {
_counter++; // これは無限ループを引き起こす
});
return Text('$_counter');
}
setState は、次のフレーム描画時に build メソッドを再呼び出しします。
setState を頻繁に呼び出したり、build メソッド内で setState を呼び出したりすると、パフォーマンスの問題を引き起こす可能性があります。
選択方法
開発時には、実際のニーズに応じて適切な Widget タイプを選択する必要があります:
StatelessWidget を選択する場合
- Widget の外観が可変状態に依存しない
- 必要なすべてのデータがコンストラクタを通じて親 Widget から渡される
- Widget がユーザーインタラクションに応答する必要がない
- 最高のパフォーマンスを追求する
StatefulWidget を選択する場合
- Widget が内部状態を維持する必要がある
- ユーザー入力(クリック、スワイプなど)に応答する必要がある
- データの変化(ネットワークリクエストの戻りなど)に応答する必要がある
- アニメーションを制御する必要がある