Flutter StatelessWidget vs StatefulWidget を深く理解する

StatelessWidget と StatefulWidget の違いを理解することは Flutter 開発の基礎です。

このセクションでは、これら2種類の Widget の使用シーンと内部の動作メカニズムを詳しく分析します。


核心的な違い

Flutter には主に2種類の Widget があります。それらの根本的な違いは、可変状態を保持できるかどうかです。

特性StatelessWidgetStatefulWidget
状態不変(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],
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

StatefulWidget 詳細解説

StatefulWidget は、ユーザーインタラクションやデータ変化に応答する必要があるシーンに適しています。これは Widget 自体(不変)と State オブジェクト(可変)の2つの部分で構成されます。

使用シーン

  • カウンター、タイマーなどの動的データ
  • フォーム入力
  • アニメーション制御
  • ネットワークリクエストの状態表示
  • ユーザーインタラクションへの応答

State オブジェクトのライフサイクル

メソッド説明
initStateWidget が最初に作成されたときに呼び出され、状態を初期化します。
didChangeDependenciesWidget の依存関係が変化したときに呼び出されます。
buildUI を構築します。状態が変化するたびに呼び出されます。
didUpdateWidget親 Widget が再構築されたときに呼び出されます。
disposeWidget が破棄されるときに呼び出され、リソースをクリーンアップします。

実例: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();
  }
}

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;
  });
}

誤った使い方

実例:誤った setState の使い方

// 誤り:状態を変更するが setState を呼び出さない
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 が内部状態を維持する必要がある
  • ユーザー入力(クリック、スワイプなど)に応答する必要がある
  • データの変化(ネットワークリクエストの戻りなど)に応答する必要がある
  • アニメーションを制御する必要がある
その他の拡張