Media

【Flutter入門講座 第4回】状態管理の第一歩。setStateとChangeNotifier・ListenableBuilderでメモアプリを動かす

This page is available in Japanese only.

【Flutter入門講座 第4回】状態管理の第一歩。setStateとChangeNotifier・ListenableBuilderでメモアプリを動かす

第 3 回では、決まったメモを一覧に並べました。第 4 回は、メモを追加したり、チェックを付けたりできるようにします。

そのために必要なのが「状態(State)」の考え方です。Flutter では、状態が変わったら、画面(ウィジェット)を作り直して描き直す、という仕組みで画面を更新します。

チェックを付けると「残り 3 件」が「残り 2 件」に変わる
この回で作る動き(Web で動かした画面)

状態には 2 種類ある

種類

例

管理の方法

画面の中だけで使う状態

入力中の文字、開いているタブ、アニメーションの途中

StatefulWidget と setState

アプリ全体で使う状態

メモの一覧、ログインしているユーザー、カートの中身

ChangeNotifier など(画面の外に置く)

Flutter の公式ドキュメントでも、setState は「ウィジェットの中だけの一時的な状態」向けの低レベルな方法と説明されています。まずは setState から見ていきましょう。

状態の流れ。ボタンを押す → 状態を変える → 知らせる → 画面を作り直す

setState: 画面の中だけで使う状態

lib/steps/part4_set_state.dartdart
class CounterMemoPage extends StatefulWidget {
  const CounterMemoPage({super.key});

  @override
  State<CounterMemoPage> createState() => _CounterMemoPageState();
}

class _CounterMemoPageState extends State<CounterMemoPage> {
  final _memos = <String>[];

  void _add() {
    setState(() {
      _memos.add('メモ ${_memos.length + 1}');
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('メモ(${_memos.length} 件)')),
      body: ListView(
        children: [for (final m in _memos) ListTile(title: Text(m))],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _add,
        child: const Icon(Icons.add),
      ),
    );
  }
}
  • StatefulWidget は、ウィジェット本体と、状態を持つ State クラスの 2 つで 1 組です
  • 値を変えるときは `setState(() { ... })` の中で変えます。こうすると Flutter が build を呼び直し、画面が更新されます
  • setState を使わずに _memos.add(...) だけ書くと、データは変わっても画面は変わりません(よくある間違いです)

この書き方は簡単ですが、メモの一覧を別の画面からも使いたい、保存もしたいとなると、画面のクラスの中に処理が増えていきます。そこで、データを画面の外に出します。

ChangeNotifier: アプリ全体のデータを画面の外に置く

ChangeNotifier は、Flutter に最初から入っている「変わったら知らせる」ための仕組みです。メモの一覧と、それを変える処理を 1 つのクラス(ストア)にまとめます。

lib/memo_store.dart(保存の処理は第 5 回で説明)dart
class MemoStore extends ChangeNotifier {
  MemoStore(this._repository);

  final MemoRepository _repository;
  List<Memo> _memos = [];
  int _nextId = 1;

  List<Memo> get memos => List.unmodifiable(_memos);
  int get remaining => _memos.where((m) => !m.done).length;

  void add(String text) {
    if (text.trim().isEmpty) return;
    _memos = [..._memos, Memo(id: _nextId++, text: text.trim())];
    _changed();
  }

  void toggle(int id) {
    _memos = [
      for (final m in _memos) m.id == id ? m.copyWith(done: !m.done) : m,
    ];
    _changed();
  }

  void remove(int id) {
    _memos = _memos.where((m) => m.id != id).toList();
    _changed();
  }

  void _changed() {
    notifyListeners(); // 画面を描き直す
    _repository.save(_memos); // 保存は裏で
  }
}

ポイントは 3 つです。

  1. データは外から直接変えさせない: memos は変更できないリスト(List.unmodifiable)で渡し、変更は add・toggle・remove を通してだけ行います
  2. 変えたら `notifyListeners()`: これで、このストアを見ている画面に「変わった」と知らせます
  3. 新しいリストを作って入れ替える: 第 2 回の copyWith のように、元のデータを書き換えずに新しいものを作ると、変更の流れを追いやすくなります

ListenableBuilder: 変わったら画面を作り直す

画面の側では、ListenableBuilder でストアを見張ります。notifyListeners() が呼ばれるたびに、builder の中が作り直されます。

lib/memo_list_page.dart(抜粋)dart
return ListenableBuilder(
  listenable: store,
  builder: (context, _) {
    final memos = store.memos;
    return Scaffold(
      appBar: AppBar(title: Text('メモ(残り ${store.remaining} 件)')),
      body: ListView.separated(
        itemCount: memos.length,
        separatorBuilder: (_, _) => const Divider(height: 1),
        itemBuilder: (context, index) {
          final memo = memos[index];
          return Dismissible(
            key: ValueKey(memo.id),
            onDismissed: (_) => store.remove(memo.id),  // 横にスワイプで削除
            background: Container(color: Colors.red.shade100),
            child: CheckboxListTile(
              value: memo.done,
              onChanged: (_) => store.toggle(memo.id),  // チェックの ON/OFF
              title: Text(memo.text),
            ),
          );
        },
      ),
    );
  },
);
  • CheckboxListTile をタップすると store.toggle が呼ばれ、ストアが notifyListeners() で知らせ、ListenableBuilder が画面を作り直します
  • Dismissible は、横にスワイプして消せるようにするウィジェットです。key には、メモごとに変わらない値(ここでは id)を指定します
  • (_, _) の _ は「使わない引数」です。Dart 3.7 からは、使わない引数を何個でも _ と書けます

画面のクラスは「表示と、ボタンが押されたらストアを呼ぶ」だけになり、データの扱いはストアに集まります。

Riverpod や provider は使わないの?

Flutter の状態管理には、provider・Riverpod・Bloc など、多くのパッケージがあります。公式ドキュメントでも、アプリの複雑さやチームの好みに応じて、こうしたパッケージを使うと便利な場合があると紹介されています。

この講座では、仕組みを理解するために、Flutter の標準の `ChangeNotifier` と `ListenableBuilder` だけで作っています。実はこれらのパッケージも、内部では同じような「変わったら知らせる」仕組みを使っています。まずはこの形を理解してから、規模が大きくなったときにパッケージを検討するのがおすすめです。

状態管理の選び方。画面の中だけなら setState、アプリ全体なら ChangeNotifier、大規模ならパッケージ

まとめ

  • Flutter は、状態が変わったら画面を作り直すことで表示を更新します
  • 画面の中だけの状態は `StatefulWidget` と `setState`。値は必ず setState の中で変えます
  • アプリ全体のデータは `ChangeNotifier` にまとめ、変えたら `notifyListeners()`
  • 画面は `ListenableBuilder` でストアを見張り、変わったら作り直します
  • 大きなアプリでは provider・Riverpod なども選択肢ですが、まずは標準の仕組みで考え方を身につけましょう

次回(第 5 回)は、API からデータを取ってきて、メモを端末に保存します。

Flutter 入門講座(全 6 回)

  1. Flutter とは?できることと開発環境の作り方
  2. Dart の基本文法
  3. ウィジェットで画面を作る
  4. 状態管理の第一歩(この記事・11月12日)
  5. API からデータを取得し、端末に保存する(11月13日公開予定)
  6. 画面遷移・テスト・リリースまで(11月14日公開予定)
Flutter・スマホアプリの開発を相談する

アプリの企画段階からのご相談も承ります。フォームは 24 時間受付です

※ Flutter は Google LLC の商標です。

参考

Contact

Let's talk about your project.

Web, system and app development, video and more. Get in touch with us here.

Request a quoteOther inquiries
Back to Media
Contact