Flutter初心者の学習記録 第14回:Statelessウィジェット vs Statefulウィジェット

こんにちは、Flutterエンジニアの皆さん!今回は、Flutterアプリケーション開発における基本的かつ非常に重要な概念である「StatelessウィジェットとStatefulウィジェットの違い」について詳しく解説します。

はじめに

Flutterでは、すべてがウィジェットです。ボタン、テキスト、画像、レイアウト要素など、画面上に表示されるものはすべてウィジェットとして実装されています。Flutterのウィジェットは主に2つのカテゴリに分けられます:

  1. Statelessウィジェット:状態を持たないウィジェット
  2. Statefulウィジェット:状態を持つウィジェット

この違いを理解することは、効率的で保守しやすいFlutterアプリを構築するために欠かせません。それでは、それぞれの特徴、使い分け、そして実際のコード例を見ていきましょう。

Statelessウィジェット

特徴

Statelessウィジェットは、**不変(イミュータブル)**です。つまり、一度作成されると、そのプロパティ(構成)を変更することはできません。

  • 単純な表示要素に適しています
  • 親ウィジェットから渡されたデータのみを使用します
  • 内部状態を持ちません
  • buildメソッドは通常、親ウィジェットが再構築されたときにのみ呼び出されます

コード例





import 'package:flutter/material.dart';

class MyStatelessWidget extends StatelessWidget {
  final String title;
  
  // コンストラクタ
  const MyStatelessWidget({Key? key, required this.title}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16.0),
      child: Text(
        title,
        style: const TextStyle(fontSize: 24.0),
      ),
    );
  }
}

この例では、MyStatelessWidgetは単に渡されたtitleを表示するだけです。ウィジェット自体が内部状態を持たないため、表示を変更するには親ウィジェットから新しい値を渡す必要があります。

使用シーン

  • テキストラベル、アイコン、固定画像など、表示のみを行うUI要素
  • データを表示するだけで、インタラクティブな要素が少ないスクリーン
  • 親ウィジェットから渡されたデータに基づいて表示を行うコンポーネント

Statefulウィジェット

特徴

Statefulウィジェットは、**可変(ミュータブル)**です。つまり、内部状態を持ち、その状態に基づいて外観や動作を変更できます。

  • インタラクティブな要素に適しています
  • 内部状態(Stateオブジェクト)を持ちます
  • ユーザーの操作に応じて状態を更新し、UIを再構築できます
  • setState()メソッドを呼び出すことで再描画をトリガーします

コード例





import 'package:flutter/material.dart';

class MyStatefulWidget extends StatefulWidget {
  const MyStatefulWidget({Key? key}) : super(key: key);
  
  @override
  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  int _counter = 0;
  
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(
          '$_counter',
          style: const TextStyle(fontSize: 48.0),
        ),
        const SizedBox(height: 16.0),
        ElevatedButton(
          onPressed: _incrementCounter,
          child: const Text('カウントアップ'),
        ),
      ],
    );
  }
}

この例では、ボタンをタップするたびに_counterの値が増加し、それに応じてUIが更新されます。この状態の変更と更新はsetState()メソッドによって管理されています。

使用シーン

  • フォーム入力
  • アニメーション
  • カウンター、トグル、スライダーなどのインタラクティブな要素
  • ユーザー操作によって表示が変わるスクリーン
  • データのフェッチと表示を行うスクリーン

パフォーマンスの考慮事項

Flutterでは、ウィジェットの再構築(rebuild)は比較的安価な操作です。しかし、アプリケーションが大きくなるにつれて、不必要な再構築を避けることが重要になります。

Statelessウィジェットの場合:

  • 親ウィジェットが再構築されるたびに、すべての子ウィジェットも再構築されます
  • データが変更されない限り、同じ見た目を維持します

Statefulウィジェットの場合:

  • setState()が呼び出されると、そのStatefulウィジェットとその子ウィジェットが再構築されます
  • 状態が変更されたときにのみ再構築が必要な場合に適しています

実践的な選択基準

ウィジェットを設計する際、以下の質問を自問してみるとよいでしょう:

  1. このウィジェットは内部状態を持つ必要があるか?
    • ユーザー入力を受け付けるか?
    • 時間の経過とともに表示が変わるか?
    • 外部データソースから情報を取得するか?
  2. どのレベルで状態を管理すべきか?
    • ローカルな状態(単一ウィジェット内)
    • 親子関係の状態(親から子への値の受け渡し)
    • グローバルな状態(Provider、Riverpod、Bloc、Reduxなどの状態管理ライブラリの使用)

具体的な例: カウンターアプリ

以下に、同じカウンターアプリを異なるアプローチで実装した例を示します。

Statefulアプローチ(ローカル状態管理)





dartimport 'package:flutter/material.dart';

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

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

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

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Statefulカウンター'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'ボタンを押した回数:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

Statelessアプローチ(親による状態管理)





import 'package:flutter/material.dart';

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

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

  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  int _counter = 0;

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(
        counter: _counter,
        onIncrement: _incrementCounter,
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  final int counter;
  final VoidCallback onIncrement;

  const MyHomePage({
    Key? key,
    required this.counter,
    required this.onIncrement,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Statelessカウンター'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'ボタンを押した回数:',
            ),
            Text(
              '$counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: onIncrement,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

2つ目の例では、MyHomePageはStatelessウィジェットですが、その親(MyApp)がStatefulになっており、状態の管理とUIの更新を担当しています。

まとめ

  1. Statelessウィジェット:
    • 不変、内部状態なし
    • 単純な表示要素に適している
    • コンストラクタで受け取ったデータのみを使用
    • パフォーマンスが良く、シンプル
  2. Statefulウィジェット:
    • 可変、内部状態あり
    • インタラクティブなUI要素に適している
    • setState()で状態を更新してUIを再構築
    • より複雑だが、動的なUIを作成可能

ベストプラクティス

  • できるだけStatelessウィジェットを使う(シンプルで保守しやすいため)
  • 必要な場合にのみStatefulウィジェットを使う
  • 状態管理を適切なレベルで行う(グローバル、ローカル)
  • パフォーマンスを考慮して、必要最小限のウィジェットだけが再構築されるようにする

Flutterでは、これらのウィジェットを適切に組み合わせることで、美しく、パフォーマンスの高いアプリケーションを構築することができます。状態管理の概念を理解し、適切なウィジェットタイプを選択することが、Flutterマスターへの第一歩です!


皆さんの Flutter 開発がよりスムーズになることを願っています!質問やコメントがありましたら、下のコメント欄でお気軽にシェアしてください。一緒に Flutter の旅を楽しみましょう!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です