---
title: "StatelessWidgetとStatefulWidgetの違いと使い分け｜Flutter 3.47で実測した判定基準"
url: "https://www.issoh.co.jp/tech/details/4442/"
published: 2024-12-09
updated: 2026-11-03
categories: ["Flutter"]
publisher: "株式会社一創"
---

# StatelessWidgetとStatefulWidgetの違いと使い分け｜Flutter 3.47で実測した判定基準

Flutterでウィジェットを1つ書くたびに、StatelessWidgetとStatefulWidgetのどちらを継承するかを決めることになります。判断を外すと、入力した値が再ビルドのたびに消えたり、変化しない部分まで毎回作り直したりします。この記事では、Flutter 3.47.9（Dart 3.13.7、2026年10月29日UTCリリース）のフレームワーク実装と、手元で実行した`flutter test`・`flutter analyze`の結果をもとに、両者の判定基準、状態をどのウィジェットに持たせるか、再ビルドを減らす書き方、`setState`とライフサイクルの実際の挙動を順に整理します。

## まとめ

判断材料は1つだけです。**そのウィジェット自身が、再ビルドをまたいで覚えておかなければならない値を持つか**。持つならStatefulWidget、持たないならStatelessWidgetです。親から渡されるだけの色やラベルは「覚えておく値」ではないので、StatelessWidgetで足ります。チェック状態や入力値のようなユーザーデータは、公式ガイドの指針どおり親に持たせ、子はStatelessWidgetにして値とコールバックを受け取る形が基本です。

実装面で押さえるのは次の点です。

- `const`を付けた子は親が10回`setState`し、毎回1フレーム進めた実測では、`build`は初回の1回だけ。非constの子は11回（依存するInheritedWidgetの変化がない条件）
- UIの部品をhelper methodで返すと親の再ビルドのたびに11回実行され、widgetクラスに切り出して`const`で置くと1回で止まる
- `setState`は即時の再描画ではなく再ビルドの予約で、実際の`build`は次のフレーム。同じフレームで3回呼んでも`build`は1回
- ライフサイクルはマウント時が`initState`→`didChangeDependencies`→`build`、親の作り直しで`didUpdateWidget`→`build`、破棄で`deactivate`→`dispose`
- 画面をまたぐ状態は共有方法を検討する。StatefulWidgetでも保持できるが、値やコールバックの受け渡しが複雑になる場合は管理方法を分離する

## StatelessWidgetとStatefulWidgetの判定基準

### 状態の寿命で決まる継承先の選択

両者の差は「内部に可変の状態を置ける場所があるかどうか」の一点です。StatelessWidgetが持つのは`build`だけで、フィールドは`final`固定です。StatefulWidgetは自分とは別に`State`オブジェクトを持ち、同じツリー位置のウィジェットを同じ型・同じ`key`で更新する場合、既存の`State`が再利用されます。だから値が残ります。

Statelessは「表示が変わらない」という意味ではありません。**自分では変更のきっかけを持たない**という意味です。[StatelessWidgetの公式APIリファレンス](https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html)は、`build`が呼ばれる場面を「ツリーへの初回挿入時」「親が設定を変えたとき」「依存している`InheritedWidget`が変わったとき」の3つと説明しています。親が新しい値で作り直せば表示は変わりますし、`Widget`クラスに`@immutable`が付いているため、`final`でないフィールドを置くと`must_be_immutable`の指摘が出ます。変わらないのはウィジェットのインスタンスそのものです。

| 比較軸          | StatelessWidget             | StatefulWidget |
| ------------ | --------------------------- | -------------- |
| 再ビルドをまたぐ内部状態 | 持てない                        | Stateが保持       |
| 必須のオーバーライド   | build                       | createState    |
| 更新の起点        | 親の再ビルド＋依存InheritedWidgetの変化 | 左記＋setState    |
| constコンストラクタ | 可                           | 可（Stateは対象外）   |
| 破棄時の後始末      | 不要                          | disposeで解放     |

3つ目の起点は見落とされがちです。`Element.didChangeDependencies`は`markNeedsBuild`を呼ぶ実装で（[Flutter 3.47.9のframework.dart](https://github.com/flutter/flutter/blob/3.47.9/packages/flutter/lib/src/widgets/framework.dart)の5203〜5206行目）、`StatelessElement`はこれを上書きしていません。`const`で固定した子が`Theme.of(context)`を読む構成で、`MaterialApp`の`theme`だけを明るいテーマから暗いテーマへ差し替えたところ、子の`build`は1回から2回に増えました（`themeAnimationDuration: Duration.zero`で計測。既定のテーマ切り替えアニメーションを残すと補間中のフレームでも再ビルドされ、4回でした）。

実務で効くのは表の最終行です。`Timer`、`StreamSubscription`、`AnimationController`のように解放が要るものを持った時点で、StatefulWidgetにして`dispose`で止める必要が出ます。解放するものも覚えておく値も無いなら、StatefulWidgetにする理由はありません。

### 迷ったらStatelessWidgetから始めるべき理由

判断がつかない段階ではStatelessWidgetで書き始めてください。後戻りのコストが非対称だからです。StatelessからStatefulへの変更は、クラスを2つに割って可変フィールドを`State`へ移すだけで、呼び出し側のコードは変わりません。逆向きは、`setState`の呼び出し箇所を全部たどって「本当に自前で持つべき状態だったか」を1つずつ判定し直す作業になります。

「将来状態を持ちそうだから、とりあえずStatefulWidgetにしておく」書き方は避けます。使っていない`State`クラスはコストを払うだけで何も返しません。

## 状態の配置先と自身・親・混在の選択基準

### 公式ガイドの3方式と「迷ったら親」の指針

StatefulWidgetにすると決めたあとにもう1つ判断があります。状態を、表示するウィジェット自身が持つのか、親が持つのかです。[公式ガイドのAdding interactivity](https://docs.flutter.dev/ui/interactivity#managing-state)は、方式を「ウィジェット自身が管理」「親が管理」「両者の混在」の3つに分け、次の基準を示しています。

- チェックボックスのオン・オフやスライダーの位置のような**ユーザーデータ**は親が管理する
- アニメーションのような**見た目だけの状態**はウィジェット自身が管理する
- 迷ったら親で管理するところから始める（原文は “If in doubt, start by managing state in the parent widget.”）

前章の「迷ったらStatelessWidget」と同じ向きの指針です。子が状態を持たなければ子はStatelessWidgetのまま書け、値の持ち主が1か所にまとまります。保存ボタンや入力チェックのように同じ値を別の部品から読む必要が出ても、持ち主を探し回らずに済みます。

### 親が状態を持つStatelessWidgetの書き方

親管理の形では、子は現在値とコールバックだけを受け取ります。お気に入りの星ボタンを例にすると次のとおりです。

```
import 'package:flutter/material.dart';

class FavoriteToggle extends StatelessWidget {
  const FavoriteToggle({
    super.key,
    required this.active,
    required this.onChanged,
  });

  final bool active;
  final ValueChanged<bool> onChanged;

  @override
  Widget build(BuildContext context) {
    return IconButton(
      icon: Icon(active ? Icons.star : Icons.star_border),
      onPressed: () => onChanged(!active),
    );
  }
}

class ProductTile extends StatefulWidget {
  const ProductTile({super.key, required this.name});

  final String name;

  @override
  State<ProductTile> createState() => _ProductTileState();
}

class _ProductTileState extends State<ProductTile> {
  bool _favorite = false;

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(widget.name),
      trailing: FavoriteToggle(
        active: _favorite,
        onChanged: (bool value) {
          setState(() {
            _favorite = value;
          });
        },
      ),
    );
  }
}
```

このコードはFlutter 3.47.9の`flutter analyze`で`No issues found!`、ウィジェットテストでタップ前が`star_border`、タップ後が`star`に切り替わることを確認しています。`FavoriteToggle`は`active`を自分で書き換えず、`onChanged`で親に変更を依頼するだけです。Flutter標準の`Checkbox`や`Switch`も、値を`value`で受け取り、変更を`onChanged`で親へ返すAPIです。

逆に、展開アニメーションの進行度やホバー中かどうかのように親が知る必要のない状態は、子のStatefulWidgetに閉じ込めます。これが「混在」で、ユーザーデータは親、見た目の状態は子という分担になります。

## StatelessWidgetの最小実装とconstが再ビルドを止める仕組み

### 公式テンプレートに合わせた書き方とsuper.key

StatelessWidgetは`build`のオーバーライド1つで完結します。`flutter create`のテンプレート（SDK内の`packages/flutter_tools/templates/app/lib/main.dart.tmpl`）では、コンストラクタに`const`と`super.key`が付き、フィールドは`final`です。この形をそのまま真似るのが最短です。

```
import 'package:flutter/material.dart';

class GreetingLabel extends StatelessWidget {
  const GreetingLabel({super.key, required this.name});

  final String name;

  @override
  Widget build(BuildContext context) {
    return Text('Hello, $name');
  }
}
```

公開ウィジェットのコンストラクタに名前付きの`key`引数がないと、`use_key_in_widget_constructors`の指摘が出ます。ここでは`super.key`でその引数を宣言しています。3.47.9のテンプレートが生成する`pubspec.yaml`は`flutter_lints: ^6.0.0`を指定しており、このルールは[flutter\_lints](https://pub.dev/packages/flutter%5Flints) 6.0.0の`flutter.yaml`に含まれています。

### 親を10回更新した場合のconstの子と非constの子のbuild回数

`const`の効果はソースを追うとはっきりします。`Element.updateChild`は新旧のウィジェットが`child.widget == newWidget`を満たすと、その子ツリーの更新を丸ごと飛ばします（framework.dart 4027行目）。この`==`は`Widget`クラスで`@nonVirtual`が付き、`Object`の同一性判定に委譲されています（364〜366行目）。標準実装では「同じインスタンスかどうか」を比較します。`@nonVirtual`に反するオーバーライドは解析時に指摘されるため、値比較へ変更する設計は避けてください。

Dartの`const`式は同じ内容なら同一インスタンスに正規化されます。`const GreetingLabel(name: 'a')`と書いた箇所は、親が何度`build`されても同じインスタンスを返すので、子ツリーがスキップされます。`build`の回数を数える子を2つ用意し、片方だけ`const`を付けて親から10回`setState`を呼んだ結果がこちらです。

```
RESULT constChildBuilds=1 plainChildBuilds=11
```

`const`付きは初回の1回だけ、`const`なしは初回＋10回の11回です。固定文言やアイコンのように設定が変わらない部分は、`const`にすると、親の再ビルドに伴う子の更新を省略できます。ただし、子自身が依存する`InheritedWidget`の変化による再ビルドは防ぎません。

ここは**標準のlint設定だけでは呼び出し側のconst付け忘れを検出しない領域**です。`flutter_lints`の`prefer_const_constructors_in_immutables`はコンストラクタの**宣言側**を`const`にさせるルールで、呼び出し側の付け忘れは見ません。呼び出し側を促す`prefer_const_constructors`は、flutter\_lints 6.0.0にも、その土台のlints 6.1.0の`recommended.yaml`にも入っていません。上の計測で11回ビルドされた側も、宣言は`const`付きです。呼び出し側も検査したい場合は、`analysis_options.yaml`の`linter`で`prefer_const_constructors`を個別に有効にします。

条件が成立しなかった場合は`Widget.canUpdate`に進みます。見ているのは`runtimeType`と`key`の一致だけです（382〜384行目）。両方一致すればElementと`State`を再利用し、外れれば作り直します。

## 再ビルドの範囲を絞るウィジェット分割の書き方

### helper methodとwidgetクラスの実行回数比較

[StatefulWidgetの公式APIリファレンス](https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html)のPerformance considerationsは、再利用するUIの部品は`Widget _buildHeader()`のようなhelper methodではなく、widgetクラスとして書くよう勧めています。helper methodは親の`build`の一部として毎回実行されるからです。固定のTextを「helper methodで返す」「`const`のwidgetクラスで置く」の2通りで書き、親から10回`setState`した結果が次のとおりです。

```
RESULT_HELPER helperMethodCalls=11 widgetClassBuilds=1
```

helper methodは11回、widgetクラスは1回です。helper methodの中身が`const Text('header')`だけでも、メソッド自体の実行と、その中で組み立てる処理は親の再ビルドごとに走ります。部品が重いほど差が開くので、2回以上使う部品や中で計算をする部品はクラスに切り出してください。1か所でしか使わない数行の`Padding`まで分ける必要はありません。

### 葉のウィジェットへの状態配置と再ビルド範囲

同じリファレンスは「状態を葉へ押し下げる（Push the state to the leaves）」とも書いています。時計を表示するページを例に、秒の値をページ全体のStatefulWidgetに持たせた場合と、時計部分だけのStatefulWidgetに持たせた場合を比べました。どちらも時計を10回更新し、隣に置いた`const`なしの兄弟ウィジェットの`build`回数を数えています。

```
RESULT_LEAF topPage=11 topSibling=11 leafPage=1 clock=11 leafSibling=1
```

ページに状態を置くとページも兄弟も11回ずつ再ビルドされ、時計の中に移すと再ビルドされるのは時計の11回だけで、ページと兄弟は1回で止まりました。「迷ったら親」と矛盾するように見えますが、対象が違います。親に持たせるのは複数の部品が読むユーザーデータで、時計の秒やアニメーションの進行度のように1つの部品しか読まない値は、その部品の中に閉じ込めます。

## StatefulWidgetとStateの分離、setStateが実際に行う処理

### createStateの戻り値をState<CounterPanel>と書く理由

StatefulWidgetはウィジェット本体と`State`の2クラスで書きます。本体は`createState`だけを持ち、可変の値は`State`に置きます。

```
import 'package:flutter/material.dart';

class CounterPanel extends StatefulWidget {
  const CounterPanel({super.key, this.step = 1});

  final int step;

  @override
  State<CounterPanel> createState() => _CounterPanelState();
}

class _CounterPanelState extends State<CounterPanel> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count += widget.step;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Text('Count: $_count'),
        ElevatedButton(
          onPressed: _increment,
          child: const Text('Increment'),
        ),
      ],
    );
  }
}
```

`createState`の戻り値型を`_CounterPanelState`と書くと`library_private_types_in_public_api`に引っかかります（lints 6.1.0の`recommended.yaml`に含まれ、flutter\_lints経由で有効になります）。公開APIの戻り値にプライベート型を出さないよう、テンプレートと同じく`State<CounterPanel>`と書きます。

`State`から親のパラメータを読むときは`widget.step`のように`widget`プロパティを経由します。コンストラクタ引数を`State`のフィールドへコピーすると、親が新しい値で作り直してもコピーは古いまま残ります。

### setStateは即時再描画ではなく再ビルドの予約

`setState`の実装は、渡されたコールバックを同期実行したあと`_element!.markNeedsBuild()`を呼ぶだけです（framework.dart 1221行目）。`markNeedsBuild`はElementをdirtyとして登録するだけで、その場では`build`は走りません。`build`の回数を`setState`の直後と1フレーム進めた後で比べ、続けて同じフレームで3回`setState`してから1フレーム進めました。

```
RESULT_TIMING before=1 afterSetState=1 afterPump=2 after3setStateAnd1pump=3
```

呼んだ直後は1回のまま、フレームが進んで2回目が走っています。3回呼んでも増えたのは1回だけです。イベントハンドラの中で条件ごとに`setState`を分けて書いても、再ビルドは1回にまとまります。

`markNeedsBuild`はElementがactiveでなければ何もせず戻ります（5352〜5356行目）。ツリーから一時的に外れた（deactivate済みで、まだunmountされていない）段階でも、`setState`のコールバックは同期実行されます。ただし、Elementが非activeの間は、その呼び出しによる再ビルドの予約は行われません。完全に破棄されたあとの`setState`は扱いが変わり、次章のエラーになります。

## Stateのライフサイクルと各コールバックが呼ばれる条件

### マウント時と親再ビルド時の呼び出し順の実測

各メソッドをログに記録するウィジェットで、マウント・親からの更新・破棄の3局面の呼び出し順をFlutter 3.47.9で記録しました。

```
RESULT_MOUNT   initState > didChangeDependencies > build
RESULT_UPDATE  didUpdateWidget(a->b) > build
RESULT_UNMOUNT deactivate > dispose
```

初回マウントでは`initState`のあとに`didChangeDependencies`が必ず1回入ります。ソースのドキュメントにも「`initState`の直後にも呼ばれる」とあります（1471行目）。`Theme.of(context)`や`MediaQuery.of(context)`を使う初期化は、`initState`ではなくここに置きます。

親が同じ型・同じ`key`で作り直したときは`didUpdateWidget`が古いウィジェットを引数に呼ばれ、`initState`は呼ばれません。フレームワークは`didUpdateWidget`のあとに必ず`build`を呼ぶため、中で`setState`を呼ぶのは冗長だとソースのコメントが明言しています（1024行目）。購読先の切り替えのように、古い値と比べて処理を分ける用途に使ってください。

### disposeが呼ばれない場面と後始末の書き方

[State.disposeのリファレンス](https://api.flutter.dev/flutter/widgets/State/dispose.html)は、`dispose`が**アプリの終了時には呼ばれない**と注意しています。例に挙げているのはバッテリーの発火、端末をプールに落とす、メモリ逼迫によるOSのプロセス強制終了です。永続化が必要なデータを`dispose`で書き出す設計は成り立ちません。アプリのライフサイクルを見るなら`AppLifecycleListener`を使います。ただし、強制終了時には通知が届かない場合もあるため、重要なデータは変更時などに保存します。

次のコードは、`Stream<int> source`を持つStatefulWidgetの`FeedLabel`からState部分だけを抜粋したものです。別途`dart:async`とFlutterのウィジェットライブラリをimportし、`FeedLabel`の定義を用意してください。この例では、同じStateの存続中に`source`を変更しない前提で、`initState`で購読し、`dispose`で解除します。親から別のStreamへ差し替える設計では、`didUpdateWidget`で旧購読を解除して新しいStreamを購読します。どちらも`super`の呼び出しを忘れないでください。

```
class _FeedLabelState extends State<FeedLabel> {
  StreamSubscription<int>? _sub;
  int _latest = 0;

  @override
  void initState() {
    super.initState();
    _sub = widget.source.listen((int value) {
      if (!mounted) {
        return;
      }
      setState(() {
        _latest = value;
      });
    });
  }

  @override
  void dispose() {
    _sub?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Text('latest: $_latest');
  }
}
```

破棄したあとの`State`に`setState`を呼ぶと、3.47.9のデバッグ実行では次のエラーになりました。同じテストで`mounted`は破棄の前が`true`、後が`false`です。

```
setState() called after dispose(): LeakyState#1f4d7(lifecycle state: defunct, not mounted)
```

このメッセージが出るのはデバッグビルドだけです。判定が`assert`の中にあるためです（1160〜1164行目）。ただしリリースビルドで無害になるわけではありません。unmount時に`state._element = null`が実行されるので（6056行目）、assertが無効な状態では1221行目の`_element!`でnullチェックのエラーになるとソースからは読めます（ここは実行ではなく実装からの読み取りです）。

### 非同期処理のあとのmountedとcontext.mounted

`await`のあとで`setState`や`Navigator.of(context)`を使うと、待っている間にウィジェットがツリーから外れている可能性があります。`setState`やState自身の`context`を使う場合は`State.mounted`、引数やローカル変数などで受け取った別のcontextを使う場合は、ウィジェットの種類にかかわらず、そのcontextの[BuildContext.mounted](https://api.flutter.dev/flutter/widgets/BuildContext/mounted.html)で確かめます。`BuildContext.mounted`はタグ付きソースで比べると3.7.0のframework.dartにあり、3.3.0にはありません。それより古い解説に「StatelessWidgetでは確認する手段がない」とあれば、今は当てはまりません。

```
import 'package:flutter/material.dart';

class SaveButton extends StatelessWidget {
  const SaveButton({super.key, required this.save});

  final Future<void> Function() save;

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () async {
        await save();
        if (!context.mounted) {
          return;
        }
        Navigator.of(context).pop();
      },
      child: const Text('Save'),
    );
  }
}
```

確認の行を外すと、`flutter analyze`は`Navigator.of(context)`の位置で`use_build_context_synchronously`を出しました（`Don't use 'BuildContext's across async gaps.`）。上の形では`No issues found!`です。[use\_build\_context\_synchronouslyのルール説明](https://dart.dev/tools/linter-rules/use%5Fbuild%5Fcontext%5Fsynchronously)でも、`mounted`で守るよう案内されています。

## flutter analyzeが指摘する典型的なlint違反

入門記事に載っているコードは、そのまま貼ると`flutter analyze`の指摘（severityは`info`）が出るものが少なくありません。旧版のこの記事に載っていた2つのサンプル（`key`なしのStatelessWidgetと、`_CounterWidgetState createState()`と書いたStatefulWidget）を、flutter\_lints 6.0.0を有効にしたFlutter 3.47.9のプロジェクトで解析した出力です。

```
   info • Constructors for public widgets should have a named 'key' parameter. Try adding a named parameter to the constructor • lib/bad.dart:3:7 • use_key_in_widget_constructors
   info • Constructors for public widgets should have a named 'key' parameter. Try adding a named parameter to the constructor • lib/bad.dart:10:7 • use_key_in_widget_constructors
   info • Invalid use of a private type in a public API. Try making the private type public, or making the API that uses the private type also be private • lib/bad.dart:12:3 • library_private_types_in_public_api

3 issues found. (ran in 14.5s)
```

この記事の書き方に直すと`No issues found!`になります。直したのは`super.key`の追加（2件）と、`createState`の戻り値型を`State<CounterPanel>`にしたこと（1件）です。前章の`use_build_context_synchronously`と合わせ、ウィジェットを書くときに出やすいのはこの3ルールです。

もう1つ覚えておきたいのが`no_logic_in_create_state`です。`createState`の中で`State`に引数を渡したり条件分岐を書いたりする実装を禁止します。`createState`はフレームワークがElementを作るときに呼ぶもので、いつ何回呼ばれるかを開発者は制御できません。初期化は`initState`に置いてください。`flutter analyze`は`info`だけでも終了コード1を返したので、CIに組み込めば書いた時点で止められます。

## 共有状態の管理方法を見直す条件と移行先

`setState`で足りるのは、状態を持つウィジェットと、それを表示するウィジェットが同じサブツリーに収まっている場合です。開閉状態を持つアコーディオン、フォーム1つ分の入力値、アニメーションの進行度は`setState`のままで十分で、ライブラリは要りません。

本記事では、**共通の祖先から値やコールバックを何段も渡す構成になり、変更箇所を追いにくくなった時点**を、共有状態の管理方法を見直す目安とします。[公式のEphemeral vs app state](https://docs.flutter.dev/data-and-backend/state-mgmt/ephemeral-vs-app)も、アプリのすべての状態を`State`と`setState`で管理することは可能だとしたうえで、一時的な状態とアプリ全体の状態を分ける明確な規則は無いと書いています。つまり技術的な上限ではなく保守性の判断です。「迷ったら親」で持ち上げ続けた結果、祖先が`setState`するたびに無関係なサブツリーまで再ビルドされる状態になったら、前章の計測で兄弟が11回ビルドされた側の構造に近づいています。ログイン中のユーザー情報、カート、ダークモードの設定のように多くの画面が読む値は、早い段階で外に出す設計を検討してください。

移行先は段階的に選べます。値が1つで購読側も限られるなら`ValueNotifier`と`ValueListenableBuilder`で足ります。`Theme`や`MediaQuery`と同じ`InheritedWidget`を自作すれば、StatelessWidgetのまま祖先の値に追従することもできます。ライブラリを入れるなら、[RiverpodとBloc・Providerの違い](/tech/details/7039/)で候補を比べ、[Riverpod 3系の導入と使い方](/tech/details/9959/)から始めるのが早道です。画面単位で責務を切るところまで進めるなら、[公式アーキテクチャガイドに沿ったViewModel設計](/tech/details/4443/)と[Flutterアーキテクチャのベストプラクティス](/tech/details/7232/)が指針になります。

状態管理ライブラリを入れてもStatefulWidgetは不要になりません。`AnimationController`、`TextEditingController`、`ScrollController`のように破棄が必要なオブジェクトは、`State`の`initState`と`dispose`で管理するのが素直です。既存アプリの設計整理を外部に相談する場合は、[Flutter / React Native開発の支援内容](/service/apps/cross%5Fplatform/)も参考にしてください。

## よくある質問

### StatelessWidgetとStatefulWidgetはどちらを使うべきですか？

そのウィジェット自身が再ビルドをまたいで値を覚える必要があるかで決めます。必要が無ければStatelessWidgetです。チェック状態や入力値は親に持たせ、子はStatelessWidgetで値とコールバックを受け取る形にすると、StatefulWidgetは親の1か所で済みます。

### StatelessWidgetの中でsetStateは使えますか？

使えません。`setState`は`State`クラスのメソッドで、StatelessWidgetには存在しません。画面を変えたいならStatefulWidgetに書き換えるか、親から新しい値を渡して作り直してもらいます。後者のほうが、本記事で示した親管理の形になります。

### initStateとbuildはどちらが先に呼ばれますか？

`initState`が先です。初回マウント時の順序は`initState`→`didChangeDependencies`→`build`でした。`Theme.of(context)`のように`InheritedWidget`を読む初期化は`didChangeDependencies`に書きます。

### setStateを呼んでも画面が更新されないときは何を確認しますか？

更新したい値が`State`のフィールドにあるかを確認します。`widget`側の値は親が作り直さない限り変わりません。次に、組み立て済みのウィジェットを`State`のフィールドにキャッシュしていないかを見ます。キャッシュしたインスタンスは`Element.updateChild`の同一性判定で更新が省略され、手元のテストでもキャッシュした`Text`だけが初期値のまま残りました。

### リストを並べ替えると入力値が別の行に移ってしまうのはなぜですか？

`key`を指定していないためです。`Widget.canUpdate`は`runtimeType`と`key`の一致だけを見ます。`key`を付けないとどちらも`null`で一致するため、並べ替えても同じ位置のElementと`State`が再利用され、入力値やチェック状態が居残ります。各行に`ValueKey`などデータ由来の`key`を与えれば、行と`State`の対応が保たれます。

## 関連記事

- [RiverpodとBloc・Providerの違い｜GetX・get\_itも含めた状態管理の比較と選び方【2026年10月】](/tech/details/7039/)
- [Riverpodとは？Flutterの状態管理をRiverpod 3系で始める導入・使い方・移行ガイド](/tech/details/9959/)
- [FlutterのMVVM実装をRiverpodで書く手順｜公式アーキテクチャガイド準拠のViewModel設計](/tech/details/4443/)
- [Flutterアーキテクチャのベストプラクティス｜公式ガイドのレイヤー設計・ディレクトリ構成・設計パターン【Flutter 3.47対応】](/tech/details/7232/)
- [Flutter 3.47とは？material\_ui移行・iOS 15と3.47.7までの修正【2026年10月時点】](/tech/details/16894/)

---

出典: [StatelessWidgetとStatefulWidgetの違いと使い分け｜Flutter 3.47で実測した判定基準](<https://www.issoh.co.jp/tech/details/4442/>)（株式会社一創）
