はじめに
Flutterを学び始めると、最初に直面するのが「状態管理」です。
この記事では、TODOアプリを題材に setState → Provider の順で実装し、それぞれの違いと使い分けを解説します。
対象読者
- Flutter の Widget 基礎を学んだ方
- setState は使えるが、Provider がよくわからない方
状態管理とは?
「状態(State)」とは、UIに影響を与えるデータのことです。
TODOアプリでいえば:
- TODOのリスト
- 各TODOの完了フラグ
- 入力欄のテキスト
これらが変化するたびにUIを更新する仕組みが「状態管理」です。
① setState
仕組み
StatefulWidget の State クラス内で setState() を呼ぶと、build() が再実行されUIが更新されます。
void _addTodo(Todo todo) {
setState(() {
_todos.add(todo); // ← setState内で状態を変更
});
}
データフロー
ポイント
setState を呼ぶのは「状態を変えるとき」だけ。読むだけの処理は setState 不要です。
// ✅ 状態を変える → setState必要
void _addTodo(Todo todo) {
setState(() {
_todos.add(todo);
_sortTodos(); // ← ソートもsetState内で
});
}
// ✅ 読むだけ → setState不要
int get _doneCount => _todos.where((t) => t.isCompleted).length;
向いているケース
- 単一 Widget 内で完結する状態
- チェックボックス、スイッチなど小さな UI パーツ
限界
複数の Widget で状態を共有したいとき、setState だけでは親から子へ何度もデータを受け渡す必要があり、管理が複雑になります。
② Provider
仕組み
ChangeNotifier を継承したクラスに状態とロジックをまとめ、notifyListeners() で変更をUIに通知します。
セットアップ
# pubspec.yaml
dependencies:
provider: ^6.1.2
TodoNotifier(状態・ロジック)
class TodoNotifier extends ChangeNotifier {
final List<Todo> _todos = [];
List<Todo> get todos => _todos; // 読み取り専用で公開
void addTodo(Todo todo) {
_todos.add(todo);
_sortTodo();
notifyListeners(); // ← setStateの代わり
}
void delete(int index) {
_todos.removeAt(index);
notifyListeners();
}
void toggle(Todo todo) {
todo.isCompleted = !todo.isCompleted;
_sortTodo();
notifyListeners();
}
void _sortTodo() {
_todos.sort(
(a, b) => a.isCompleted.toString().compareTo(b.isCompleted.toString()),
);
}
}
main.dart(Providerを登録)
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => TodoNotifier(),
child: const MyApp(),
),
);
}
TodoPage(UI)
UIはロジックを持たず、Notifier のメソッドを呼ぶだけになります。
class _TodoPageState extends State<TodoPage> {
// ControllerのためだけにStatefulWidget
final _titleController = TextEditingController();
@override
void dispose() {
_titleController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// Notifierを購読(変化があればbuildが再実行される)
final notifier = context.watch<TodoNotifier>();
final todos = notifier.todos;
return Scaffold(
body: Column(
children: [
TextField(controller: _titleController),
ElevatedButton(
onPressed: () {
notifier.addTodo(Todo(title: _titleController.text));
_titleController.clear();
},
child: const Text('追加'),
),
Expanded(
child: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) => ListTile(
title: Text(todos[index].title),
leading: IconButton(
icon: Icon(todos[index].isCompleted
? Icons.check_box
: Icons.check_box_outline_blank),
onPressed: () => notifier.toggle(todos[index]),
),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => notifier.delete(index),
),
),
),
),
],
),
);
}
}
setState vs Provider 比較
| setState | Provider | |
|---|---|---|
| 状態の場所 | StatefulWidget の State 内 | ChangeNotifier クラス |
| UI更新の合図 | setState() |
notifyListeners() |
| ロジックの場所 | UIと同じファイル | 別クラスに分離 |
| 複数Widgetで共有 | 困難(props drilling) | 簡単 |
| 向いている規模 | 小〜中 | 中〜大 |
よくある疑問
Q. StatelessWidget で dispose() は使えない?
StatelessWidget にはライフサイクルがないため dispose() は呼ばれません。
TextEditingController を使う場合は StatefulWidget にする必要があります。
// ❌ StatelessWidgetにdispose → 呼ばれない
class TodoPage extends StatelessWidget {
final _controller = TextEditingController();
@override
void dispose() { ... } // ← 無視される
}
// ✅ StatefulWidgetにする
class _TodoPageState extends State<TodoPage> {
final _controller = TextEditingController();
@override
void dispose() {
_controller.dispose(); // ← ちゃんと呼ばれる
super.dispose();
}
}
Q. setStateを使わなくても StatefulWidget にすべき?
はい。判断基準は「setState を使うか」ではなく 「ライフサイクルが必要か」 です。
TextEditingController や AnimationController を使う場合は dispose() が必要なので、StatefulWidget にします。
まとめ
- setState → シンプル。Widget 内で完結する状態に使う
- Provider → 状態とロジックをUIから分離できる。複数Widgetで共有したいときに使う
- Provider を使っても
TextEditingControllerのためにStatefulWidgetにすることはある
次のステップとして Riverpod を学ぶと、Provider のより型安全な書き方ができるようになります。