Стейт-менеджер с ChangeNotifier и ChangeNotifierProvider во Flutter

Стейт-менеджер с ChangeNotifier и ChangeNotifierProvider во Flutter

Пример простой корзины в связке ChangeNotifier и ChangeNotifierProvider.

Класс ChangeNotifier из стандартного набора Flutter SDK, который служит для уведомления о изменениях всех слушателей.

Основные методы:
addListener(VoidCallback listener) - добавить слушателя, выполняется за O(1);
removeListener(VoidCallback listener) - удалить слушателя, выполняется за O(N), где N - количество слушателей;
notifyListeners() - уведомить слушателей, то есть вызывать все функции, переданные в addListener;

Так же есть свойство hasListeners для проверки, есть ли хоть один слушатель.

Данный класс может быть расширен путем наследования или добавлен примесью.

Рассмотрим использование ChangeNotifier в связке с Provider на примере создания стейт менеджера для корзины.

Для начала сделаем простую модель товара в магазине и модель корзины, которая расширяет ChangeNotifier:

class Item {
  final String name;
  final double price;
  Item({ @required this.name, @required this.price }):
        assert(name != null), assert(price != null);
}

class Cart extends ChangeNotifier {
  final List<Item> _items = [];
  double get amount => _items.fold(0, (value, item) => value + item.price);
  int get count => _items.length;
  bool hasItem(Item item) => _items.any((element) => item == element);

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

  void clear() {
    _items.clear();
    notifyListeners();
  }
}

В корзине есть методы, позволяющие добавить товар, проверить наличие товара и очистить корзину, а так же геттеры для получения количества товаров в корзине и их суммы.

Нам нужно перерисовать виджеты при изменении списка товаров в корзине. Вызываем notifyListeners только для методов, меняющих состав корзины: add и clear.

Создадим виджет, который будет отображать состояние корзины:

class CartBar extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final cart = context.watch<Cart>();
    return Padding(
      padding: const EdgeInsets.all(20.0,),
      child: Row(
        children: [
          Expanded(
              child: Text("Товаров: ${cart.count}")
          ),
          Text("сумму: ${cart.amount}")
        ],
      ),
    );
  }
}

Метод watch подпишет виджет на обновления при изменении корзины.

Создадим карточку отображения товара:

class ItemView extends StatelessWidget {
  final Item item;
  ItemView(this.item): assert(item != null);

  @override
  Widget build(BuildContext context) => InkWell(
    onTap: () => context.read<Cart>().add(item),
    child: Consumer<Cart>(
      builder: (context, cart, child) => Card(
        color: cart.hasItem(item)
            ? Colors.lightGreen
            : Colors.white,
        child: Padding(
          padding: const EdgeInsets.all(20.0),
          child: Row(
            children: [
              Expanded(child: Text(item.name)),
              Text(item.price.toStringAsFixed(2))
            ],
          ),
        ),
      ),
    ),
  );
}

В функции onTap вызываем метод read, так как нам не нужно тут получать актуальные данные из корзины, в результате функция build не будет вызываться при каждом обновлении корзины.

Чтобы отобразить в корзине товар или нет, можно использовать watch для вызова build каждый раз когда корзина изменяется. Но иногда это может быть накладно, так как только небольшую часть виджета необходимо перерисовать. Я не берусь судить конкретно в данном примере что накладней, так как по сути весь виджет кроме InkWell будет перерисован, но, это хорошее место для демонстрации виджета Consumer. Этот виджет будет вызывать функцию builder при каждом изменении Cart. Вторым параметром является сама корзина, а третий это виджет, переданный через свойство child в Consumer. В данном примере он не используется, но это может быть полезно, когда нужно не перерисовывать какую то часть виджета, тогда мы просто передаем его через этот параметр.

Соберем эти виджеты вместе:

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(
      title: Text("Магазин"),
      actions: [
        IconButton(
          icon: Icon(Icons.clear),
          onPressed: () => context.read<Cart>().clear(),
        ),
      ],
    ),
    body: Column(
      children: [
        CartBar(),
        Expanded(
          child: Consumer<UnmodifiableListView>(
            builder: (context, list, child) =>
                ListView.builder(
                  itemCount: list.length,
                  itemBuilder: (context, idx) => ItemView(list[idx]),
                ),
          ),
        ),
      ],
    ),
  );
}

В этом виджете c помощью Consumer получаем список всех товаров магазина. Здесь было бы неправильно использовать watch, так как была бы вызвана функция build при обновлении списка товаров, допустим при подгрузке с репозитория. В свою очередь был бы перерисован весь экран, включая AppBar и CartBar.

В AppBar я добавил кнопку очистки корзины. Для получения корзины опять используем функцию read. Таким образом мы не будет перерисовывать весь экран при клике на кнопку.

Обернем это все в провайдеры:

class MyApp extends StatelessWidget {
  final UnmodifiableListView products;
  MyApp({ @required this.products }): assert(products != null);

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        Provider.value(value: products),
        ChangeNotifierProvider(create: (_) => Cart(),),
      ],
      child: MaterialApp(
          title: 'Flutter App',
          theme: ThemeData(
            primarySwatch: Colors.blue,
            visualDensity: VisualDensity.adaptivePlatformDensity,
          ),
          home: HomePage()
      ),
    );
  }
}

Список продуктов я передал просто в конструктор, в реальном приложении скорее всего это будет загружено с бекэнда, например через репозиторий и BLoC.

Так как я не создаю в этом месте список продуктов, я использую Provider.value для передачи списка по контексту.

Провайдер должен быть выше Navigator, чтобы делится ресурсами со всеми страницами. Дефолтный навигатор есть внутри виджета MaterialApp. Поместите Provider выше вашего MaterialApp если нужно передать ресурсы на все страницы.

И наконец сам список товаров:

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

void main() {
  final List<Item> products = [
    Item(name: "Ноутбук", price: 100.0),
    Item(name: "Телефон", price: 200.0),
    Item(name: "Холодильник", price: 150.0),
    Item(name: "Телевизор", price: 120.0),
    Item(name: "Утюг", price: 60.0),
  ];
  runApp(MyApp(products: UnmodifiableListView(products)));
}
При копировании материалов ссылка на https://terraideas.ru/ обязательна

Комментарии к статье: Стейт-менеджер с ChangeNotifier и ChangeNotifierProvider во Flutter

Нет ни одного комментария. Будьте первым!