Kodokon kodokon.com

構造化された状態管理:ChangeNotifierとProvider

ChangeNotifierとProviderでアプリケーションの状態を共有し、狙いを定めたリビルドのためにwatch、read、selectを使いこなしましょう。

10 分 · 3 問

このレッスンを Kodokon で開く

状態が単一の画面とともに生まれて消える限り、setStateで十分です。データが共有される、つまりカート、セッション、設定といったものになった瞬間、それをコンストラクタからコンストラクタへと渡し続けるのは手に負えなくなります。ChangeNotifierはSDKの最小限のオブザーバブルを提供します。providerはそれをツリーに注入し、購読しているウィジェットだけをリビルドします。RiverpodやBlocと比べると、この組み合わせは依然として最も監査しやすいものです。魔法はほとんどなく、依存関係は一つ、一文で説明できる仕組みです。

BASH
flutter pub add provider

うまく設計されたnotifierはカプセル化します。プライベートなフィールド、読み取り専用のゲッター、そしてnotifyListeners()で終わるメソッドを通じてのみ行う変更です。この契約こそが、どんな状態の変化もUIをすり抜けないことを保証します。

DART
import 'package:flutter/foundation.dart';

class CartModel extends ChangeNotifier {
  final List<String> _items = [];

  List<String> get items => List.unmodifiable(_items);

  int get count => _items.length;

  void add(String item) {
    _items.add(item);
    notifyListeners();
  }

  void remove(String item) {
    _items.remove(item);
    notifyListeners();
  }
}
プライベートな状態、外部には読み取り専用、そして通知。

三つの入り口、三つの用途です。context.watch<T>()はウィジェットを購読させます。notifyListenersのたびにリビルドします。context.read<T>()は購読せずに読み取ります。onPressedのようなコールバックのための道具です。context.select<T, R>()は射影だけを購読します。その正確な値が変わったときにだけウィジェットがリビルドします。黄金律は、watchselectbuildのなかだけ、readはハンドラーのなか、決してその逆はしない、です。

DART
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CounterModel extends ChangeNotifier {
  int _value = 0;

  int get value => _value;

  void increment() {
    _value++;
    notifyListeners();
  }
}

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CounterModel(),
      child: const CounterApp(),
    ),
  );
}

class CounterApp extends StatelessWidget {
  const CounterApp({super.key});

  @override
  Widget build(BuildContext context) {
    final value = context.watch<CounterModel>().value;
    return MaterialApp(
      home: Scaffold(
        body: Center(child: Text('Count: $value')),
        floatingActionButton: FloatingActionButton(
          onPressed: () =>
              context.read<CounterModel>().increment(),
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}
読むためにbuildのなかでwatch、動かすためにコールバックのなかでread。
DART
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CartModel extends ChangeNotifier {
  final List<String> _items = [];

  int get count => _items.length;
}

class CartBadge extends StatelessWidget {
  const CartBadge({super.key});

  @override
  Widget build(BuildContext context) {
    final count = context.select(
      (CartModel cart) => cart.count,
    );
    return Badge(
      label: Text('$count'),
      child: const Icon(Icons.shopping_cart),
    );
  }
}
CartBadgeはcountが変わったときにだけリビルドする。

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. onPressedのなかで、無用な購読を作らずにcart.add(...)を発火させるにはどうしますか?
    • context.watch<CartModel>().add(...)
    • context.read<CartModel>().add(...)
    • context.select((CartModel c) => c.add(...))
  2. notifyListeners()は正確には何を引き起こしますか?
    • 変わったデータを使うウィジェットだけのリビルド。
    • アプリケーションの完全なリビルド。
    • どのデータが変わったかにかかわらず、notifierを購読しているすべてのウィジェットのリビルド。
  3. _itemsを直接ではなくList.unmodifiable(_items)を公開するのはなぜですか?
    • リストの読み取り性能を上げるため。
    • notifyListenersなしに、したがってリビルドなしに状態を変えてしまう外部からの変更を防ぐため。
    • Providerが可変のリストの注入を拒否するから。