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(). Именно этот контракт гарантирует, что ни одно изменение состояния не проскочит мимо интерфейса.

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>() подписывается только на проекцию: виджет перерисовывается, только если меняется именно это значение. Золотое правило: watch и select - только в build, 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),
        ),
      ),
    );
  }
}
watch в build, чтобы прочитать, 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. Зачем отдавать List.unmodifiable(_items), а не _items напрямую?
    • Чтобы улучшить производительность чтения списка.
    • Чтобы предотвратить внешнюю мутацию, которая изменила бы состояние без notifyListeners, а значит, без перерисовки.
    • Потому что Provider отказывается внедрять изменяемые списки.