К содержимому

StateProvider

StateProvider - провайдер, который предоставляет способ модифицировать свое состояние. Это упрощенный StateNotifierProvider, придуманный, чтобы не создавать StateNotifier для самых простых случаев использования.

StateProvider создан для модификации простых переменных. Состояние StateProvider обычно является:

  • перечислением, например тип фильтра
  • строкой, например содержимое текстового поля
  • булевым значением, например для чекбокса
  • числом, например для пагинации или поля возраста

StateProvider не следует использовать, если:

  • состояние нуждается в валидации
  • состояние является сложным объектом (кастомный класс, список/мапа, ...)
  • логика модификации состояние более сложная, чем просто count++.

Для более сложных ситуаций используйте StateNotifierProvider с StateNotifier. Несмотря на бойлерплейт, наличие StateNotifier играет важную роль в долгосрочной поддержке проекта, т. к. StateNotifier является единым местом хранения бизнес логики состояния.

Пример использования: Изменение типа фильтра с помощью выпадающего списка

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

Для простоты будем брать продукты из предопределенного списка:


class Product {
Product({required this.name, required this.price});

final String name;
final double price;
}

final _products = [
Product(name: 'iPhone', price: 999),
Product(name: 'cookie', price: 2),
Product(name: 'ps5', price: 500),
];

final productsProvider = Provider<List<Product>>((ref) {
return _products;
});

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

Теперь мы можем отобразить наш список продуктов следующим образом:


Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
body: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('${product.price} \$'),
);
},
),
);
}

Добавим выпадающий список, с помощью которого мы сможем сортировать наши продукты по цене или имени. Воспользуемся DropDownButton.


// Перечисление типов сортировки
enum ProductSortType {
name,
price,
}

Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
actions: [
DropdownButton<ProductSortType>(
value: ProductSortType.price,
onChanged: (value) {},
items: const [
DropdownMenuItem(
value: ProductSortType.name,
child: Icon(Icons.sort_by_alpha),
),
DropdownMenuItem(
value: ProductSortType.price,
child: Icon(Icons.sort),
),
],
),
],
),
body: ListView.builder(
// ... /* SKIP */
itemBuilder: (c, i) => Container(), /* SKIP END */
),
);
}

Далее создадим StateProvider и свяжем DropDownButton с провайдером.

Начнем с StateProvider:


final productSortTypeProvider = StateProvider<ProductSortType>(
// Возвращаем тип сортировки по умолчанию (name)
(ref) => ProductSortType.name,
);

Затем соединим провайдер и выпадающий список:

DropdownButton<ProductSortType>(
// При изменении типа сортировки произойдет перестройка DropdownButton
value: ref.watch(productSortTypeProvider),
// Обновляем состояние провайдера при взаимодействии пользователя с DropdownButton
onChanged: (value) =>
ref.read(productSortTypeProvider.notifier).state = value!,
items: [
// ...
],
),

Теперь мы можем менять тип сортировки. Однако это пока что никак не влияет на список продуктов. Так что попробуем заставить productsProvider реагировать на изменение сортировки.

Здесь нам понадобится ref.watch, чтобы productsProvider мог мгновенно реагировать на изменение типа сортировки.

Выглядеть это будет так:


final productsProvider = Provider<List<Product>>((ref) {
final sortType = ref.watch(productSortTypeProvider);
switch (sortType) {
case ProductSortType.name:
return _products.sorted((a, b) => a.name.compareTo(b.name));
case ProductSortType.price:
return _products.sorted((a, b) => a.price.compareTo(b.price));
}
});

На этом все! Этого достаточно для того, чтобы в ответ на действие пользователя список продуктов сортировался по заданному параметру.

Полный листинг кода на Dartpad:

Как обновлять состояние провайдера на основе предыдущего, не читая значение провайдера дважды

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


final counterProvider = StateProvider<int>((ref) => 0);

class HomeView extends ConsumerWidget {
const HomeView({super.key});


Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// Обновляем состояние провайдера, основываясь на предыдущем состоянии
// И в итоге, мы дважды читаем значение провайдера!
ref.read(counterProvider.notifier).state = ref.read(counterProvider.notifier).state + 1;
},
),
);
}
}

Хотя все сделано верно, синтаксис все же неудобен.

В таком случае вы можете воспользоваться функцией update. Эта функция принимает коллбэк, который в свою очередь принимает текущее состояние и должен вернуть новое. Зная это, мы может отрефакторить наш код следующим образом:


final counterProvider = StateProvider<int>((ref) => 0);

class HomeView extends ConsumerWidget {
const HomeView({super.key});


Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(counterProvider.notifier).update((state) => state + 1);
},
),
);
}
}

Мы получили тот же результат при более красивом коде.