Provider — База
Provider
Provider — это популярный пакет для управления состоянием в приложениях Flutter. Он представляет собой обертку над InheritedWidget, делая его проще в использовании и более гибким для многократного применения.
Пакет установим с сайта https://pub.dev/packages/provider
В этом уроке будем использовать версию provider 6.1.5
На прошлых уроках мы научились пользоваться InheritedWidget и ChangeNotifier для элегантной передачи состояния между виджетами, без дополнительных build()
Provider позволяет делать тоже самое, но более профессионально и легче.
Некоторые ключевые преимущества использования Provider:
- Упрощенное выделение/освобождение ресурсов: Provider берет на себя заботу о жизненном цикле объектов состояния
- Ленивая загрузка (lazy-loading): Объекты состояния создаются только тогда, когда они действительно нужны
- Значительно уменьшенный шаблонный код: По сравнению с ручным написанием
InheritedWidget - Производительность: Provider перестраивает только те виджеты, которые зависят от конкретного изменившегося значения
- Гибкость: Позволяет комбинировать несколько провайдеров и вкладывать их, поддерживая модульную архитектуру
Кратко принцип работы:
ChangeNotifier: Предоставляет уведомления об изменениях своим слушателям (виджетам). Модель данных обычно наследуется отChangeNotifier. Когда данные в модели изменяются, вы вызываете методnotifyListeners(), чтобы уведомить всех подписчиков (виджеты) о необходимости перестроиться.ChangeNotifierProvider: Это один из типов провайдеров, который предоставляет экземпляр вашегоChangeNotifier(модели данных) дереву виджетов. Он также автоматически вызываетdispose()дляChangeNotifier, когда провайдер удаляется из дерева виджетов.- Потребители состояния: Чтобы получить доступ к данным из модели и реагировать на их изменения, используются виджеты
Consumer,Selectorили методыProvider.of<T>(context),context.watch<T>()иcontext.read<T>().
Provider Практика
Рассмотрим на практике, вспомним прошлый пример про музыкальную карточку и проблему Lifting State Up, когда гоняем данные через все виджеты и лишний раз их перестраиваем.
Файл main.dart
import 'package:flutter/material.dart';
import 'package:flutter_apps_2025_1/views/track_card.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
// 👉 Данные хранит 1й виджет MyApp String title = "Provider";
String author = "ChangeNotifier";
bool isFavorite = false;
// Пример для изменения состояния и передачи вниз по дереву виджетов
void onLike() {
setState(() {
isFavorite = !isFavorite;
});
}
@override
Widget build(BuildContext context) {
debugPrint("🟢 MyApp() build");
return MaterialApp(
title: "Flutter Course 2025",
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.greenAccent),
fontFamily: "Montserrat",
),
home: Scaffold(
body: HomeWidget(
title: title, // 👉 Передаём данные во 2й виджет
author: author,
isFavorite: isFavorite,
onLike: onLike, // 👉 Передаём callback во 2й виджет
),
),
);
}
}
class HomeWidget extends StatelessWidget {
final String title;
final String author;
final bool isFavorite;
final VoidCallback onLike; // 👉 Принимаем callback во 2й виджет
const HomeWidget({
super.key,
required this.title,
required this.author,
required this.isFavorite,
required this.onLike,
});
@override
Widget build(BuildContext context) {
debugPrint("🟢 HomeWidget() build");
return Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFFBFF098), Color(0xFF6FD6FF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: SafeArea(
child: Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: TrackCard(
title: title, // 👉 Передаём данные во 3й виджет
author: author,
isFavorite: isFavorite,
onLike: onLike, // 👉 Передаём callback во 3й виджет
),
),
),
),
);
}
}
Файл track_card.dart
import 'package:flutter/material.dart';
class TrackCard extends StatelessWidget {
final String title;
final String author;
final bool isFavorite;
final VoidCallback onLike; // 👉 Принимаем callback во 3й виджет
const TrackCard({
super.key,
required this.title,
required this.author,
required this.isFavorite,
required this.onLike,
});
@override
Widget build(BuildContext context) {
debugPrint("🟢 TrackCard() build");
return Card(
color: Colors.white,
child: ListTile(
leading: Image.asset("assets/images/kapa.jpg"),
title: TitleWidget(
title: title,
isFavorite: isFavorite,
),
subtitle: SubtitleWidget(author: author),
trailing: LikeButton(
isFavorite: isFavorite,
onLike: onLike, // 👉 Передаём callback во 4й виджет ),
),
);
}
}
class TitleWidget extends StatelessWidget {
final String title;
final bool isFavorite;
const TitleWidget({
super.key,
required this.title,
required this.isFavorite,
});
@override
Widget build(BuildContext context) {
debugPrint("🟢 TitleWidget() build");
return Text(title, style: TextStyle(fontWeight: FontWeight.bold));
}
}
class SubtitleWidget extends StatelessWidget {
final String author;
const SubtitleWidget({super.key, this.author = ""});
@override
Widget build(BuildContext context) {
return Text(author.toUpperCase());
}
}
class LikeButton extends StatelessWidget {
final bool isFavorite;
final VoidCallback onLike;
const LikeButton({super.key, required this.isFavorite, required this.onLike,});
@override
Widget build(BuildContext context) {
debugPrint("🟢 HomeWidget() build");
final iconButton = IconButton(
onPressed: () {
onLike(); // 👉 Вызываем callback из 1 виджета },
icon: isFavorite
? Icon(Icons.favorite, color: Colors.red)
: Icon(Icons.favorite_border),
);
return iconButton;
}
}
Проблемы
- Можно легко ошибиться
- Утомительное занятие
- Ухудшение производительности приложения, каждый виджет будет перестраиваться
Используем `Provider`
Шаг 1: Добавление зависимости
Добавим пакет `provider` в файл `pubspec.yaml` проекта
Файл pubspec.yaml
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
provider: ^6.1.5
Или устанавливаем через терминал
Терминал
flutter pub add provider
Шаг 2: Структура проекта
Раскидаем разные файлы по нужным директориям
В директории lib добавим новы директории
modelsprovidersviews
В models добавим track_model.dart
В providers добавим track_provider.dart
В views добавим track_card.dart
Шаг 3: Модель данных
Файл models/track_model.dart
class TrackData {
final String title;
final String author;
final bool isFavorite;
const TrackData({
required this.title,
required this.author,
required this.isFavorite,
});
TrackData copyWith({
String? title,
String? author,
bool? isFavorite,
}) {
return TrackData(
title: title ?? this.title,
author: author ?? this.author,
isFavorite: isFavorite ?? this.isFavorite,
);
}
}
Шаг 4: Используем `ChangeNotifier`
Файл providers/track_provider.dart
import 'package:flutter/material.dart';
import '../models/track_model.dart';
class TrackModel extends ChangeNotifier {
TrackData _trackData;
TrackModel(this._trackData);
// Геттеры для доступа к данным
TrackData get trackData => _trackData;
String get title => _trackData.title;
String get author => _trackData.author;
bool get isFavorite => _trackData.isFavorite;
// Метод для изменения состояния
void toggleFavorite() {
_trackData = _trackData.copyWith(isFavorite: !_trackData.isFavorite);
notifyListeners(); // Уведомляем всех слушателей об изменении
}
}
Шаг 5: Предоставление модели данных дереву виджетов
Необходимо обернуть корневой виджет приложения (или часть дерева виджетов, для которой нужен доступ к состоянию) в ChangeNotifierProvider. Это сделает экземпляр TrackModel доступным для всех дочерних виджетов.
Файл main.dart
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
debugPrint("🟢 MyApp() build");
return MaterialApp(
title: "Flutter Course 2025",
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.greenAccent),
fontFamily: "Montserrat",
),
home: Scaffold(
// Оборачиваем в ChangeNotifierProvider для передачи данных вниз по дереву
body: ChangeNotifierProvider(
// Через create создаем экземпляр класса TrackModel и передаем данные
create: (context) => TrackModel(
TrackData(
title: "Provider",
author: "Change Notifier",
isFavorite: false,
),
),
child: HomeWidget()
),
),
);
}
}
class HomeWidget extends StatelessWidget {
const HomeWidget({super.key});
@override
Widget build(BuildContext context) {
debugPrint("🟢 HomeWidget() build");
return Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFFBFF098), Color(0xFF6FD6FF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: SafeArea(
child: Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: TrackCard(),
),
),
),
);
}
}
ChangeNotifierProvider предоставляет (provides) экземпляр ChangeNotifier своим потомкам в дереве виджетов. Он "слушает" (listens) изменения в этом ChangeNotifier и автоматически перестраивает (rebuilds) виджеты-потомки, которые зависят от этого ChangeNotifier, когда его состояние меняется.
Шаг 6: Использование (потребление/consume) состояния в виджетах
Чтобы получить данные и правильно их обработать есть несколько методов:
Consumer- Слушает изменения вChangeNotifierи перестраивать только определенную часть UI.Selector- ПродвинутыйConsumerпозволяет указать, какую часть данных изChangeNotifierнужно "слушать". Виджет перестроится только тогда, когда изменится только выбранная часть данных.Provider.of<T>(context)- Получить данные и подписаться или нет на измененияcontext.watch<T>()- Получить данные и подписаться на измененияcontext.read<T>()- Поучить данные и не подписываться на изменения
Файл track_card.dart
class TrackCard extends StatelessWidget {
const TrackCard({super.key});
@override
Widget build(BuildContext context) {
debugPrint("🟢 TrackCard() build");
return Card(
color: Colors.white,
child: ListTile(
leading: Image.asset("assets/images/kapa.jpg"),
title: TitleWidget(),
subtitle: SubtitleWidget(),
trailing: LikeButton(),
),
);
}
}
class TitleWidget extends StatelessWidget {
const TitleWidget({super.key});
@override
Widget build(BuildContext context) {
debugPrint("🟢 TitleWidget() build");
// Получаем весь TrackModel
final trackModel = Provider.of<TrackModel>(context, listen: false);
return Text(trackModel.title, style: TextStyle(fontWeight: FontWeight.bold,),);
}
}
class SubtitleWidget extends StatelessWidget {
const SubtitleWidget({super.key});
@override
Widget build(BuildContext context) {
debugPrint("🟢 SubtitleWidget() build");
// Получаем весь TrackModel
final trackModel = Provider.of<TrackModel>(context, listen: false);
return Text(trackModel.author);
}
}
class LikeButton extends StatelessWidget {
const LikeButton({super.key});
@override
Widget build(BuildContext context) {
debugPrint("🟢 HomeWidget() build");
// Получаем весь TrackModel и слушаем его изменения
final trackModel = context.watch<TrackModel>();
final iconButton = IconButton(
onPressed: () {
trackModel.toggleFavorite();
},
icon: trackModel.isFavorite
? Icon(Icons.favorite, color: Colors.red)
: Icon(Icons.favorite_border),
);
return iconButton;
}
}
Супер! Всё прекрасно работает через Provider
Оптимизируем перестройку виджетов
Используя Provider.of<TrackModel>(context) мы ни только получаем данные, но и подписываемся на изменения в модели данных. В данном случае, изменяется только isFavorite , но notifyListeners() сообщаем всем своим подписчикам об изменении, и просит эти виджеты перестроится. TitleWidget и SubtitleWidget нет смысла лишний раз перестраивать.
Используем
Provider.of<TrackModel>(context, listen: false)- без подписки на измененияProvider.of<TrackModel>(context)- с подпиской на изменения
Файл track_card.dart
class TitleWidget extends StatelessWidget {
const TitleWidget({super.key});
@override
Widget build(BuildContext context) {
debugPrint("🟢 TitleWidget() build");
// Получаем весь TrackModel
final trackModel = Provider.of<TrackModel>(context, listen: false);
return Text(trackModel.title, style: TextStyle(fontWeight: FontWeight.bold,),);
}
}
class SubtitleWidget extends StatelessWidget {
const SubtitleWidget({super.key});
@override
Widget build(BuildContext context) {
debugPrint("🟢 SubtitleWidget() build");
// Получаем весь TrackModel
final trackModel = Provider.of<TrackModel>(context, listen: false);
return Text(trackModel.author);
}
}
Или более красивые современные методы
context.read<TrackModel>()- без подписки на измененияcontext.watch<TrackModel>()- с подпиской на измененияcontext.select<TrackModel>(() {})- с подпиской на изменения части модели
Файл track_card.dart
class TrackCard extends StatelessWidget {
const TrackCard({super.key});
@override
Widget build(BuildContext context) {
debugPrint("🟢 TrackCard() build");
return Card(
color: Colors.white,
child: ListTile(
leading: Image.asset("assets/images/kapa.jpg"),
title: TitleWidget(),
subtitle: SubtitleWidget(),
trailing: LikeButton(),
),
);
}
}
class TitleWidget extends StatelessWidget {
const TitleWidget({super.key});
@override
Widget build(BuildContext context) {
debugPrint("🟢 TitleWidget() build");
// Получаем весь TrackModel
final trackModel = context.read<TrackModel>();
return Text(
trackModel.title,
style: TextStyle(fontWeight: FontWeight.bold),
);
}
}
class SubtitleWidget extends StatelessWidget {
const SubtitleWidget({super.key});
@override
Widget build(BuildContext context) {
debugPrint("🟢 SubtitleWidget() build");
// Получаем весь TrackModel
final trackModel = context.read<TrackModel>();
return Text(trackModel.author);
}
}
class LikeButton extends StatelessWidget {
const LikeButton({super.key});
@override
Widget build(BuildContext context) {
debugPrint("🟢 HomeWidget() build");
// Получаем весь TrackModel и слушаем его изменения
final trackModel = context.watch<TrackModel>();
final iconButton = IconButton(
onPressed: () {
trackModel.toggleFavorite();
},
icon: trackModel.isFavorite
? Icon(Icons.favorite, color: Colors.red)
: Icon(Icons.favorite_border),
);
return iconButton;
}
}
Перестраивается только HomeWidget()
Consumer
Теперь воспользуемся виджетом Consumer, чтобы обновить только определенную часть виджета
Файл track_card.dart
class LikeButton extends StatelessWidget {
const LikeButton({super.key});
@override
Widget build(BuildContext context) {
debugPrint("🟢 LikeButton() build");
// Используем context.read для вызова метода toggleFavorite
// т.к. не нужно перестраивать IconButton
final trackModel = context.read<TrackModel>();
return IconButton(
onPressed: () {
trackModel.toggleFavorite();
},
// Используем Consumer для динамической иконки
// Consumer/Selector лучше для тонкой настройки чем watch()
icon: Consumer<TrackModel>(
builder: (context, model, child) {
return model.trackData.isFavorite
? const Icon(Icons.favorite, color: Colors.red)
: const Icon(Icons.favorite_border);
},
),
);
}
}
Теперь даже виджет LikeButton не перестраивается!!!
Обновляется только иконка сердечка!
Очень круто!
Selector
Тот же самый Consumer но работает ещё гибче! Следить не за всей моделью, а только за определённой частью
Файл track_card.dart
class LikeButton extends StatelessWidget {
const LikeButton({super.key});
@override
Widget build(BuildContext context) {
debugPrint("🟢 LikeButton() build");
final trackModel = context.read<TrackModel>();
return IconButton(
onPressed: () {
trackModel.toggleFavorite();
},
// Используем Selector для получения только состояния isFavorite
icon: Selector<TrackModel, bool>(
selector: (context, trackModel) => trackModel.trackData.isFavorite,
builder: (context, isFavorite, child) {
return isFavorite
? const Icon(Icons.favorite, color: Colors.red)
: const Icon(Icons.favorite_border);
},
),
);
}
}
Особенности работы Provider
1. `Consumer<T>`
Consumer — это виджет, который позволяет "слушать" изменения в ChangeNotifier и перестраивать только определенную часть UI.
- Особенности:
- Перестраивает только себя: Это главное преимущество. Если ваш
buildметод виджета большой, и только небольшая его часть зависит от данных провайдера,Consumerпоможет избежать лишних перестроек всего виджета, что улучшает производительность. - Не требует
contextдля доступа к провайдеру: Он передает экземпляр провайдера в своейbuilderфункции. - Третий аргумент
child: Вbuilderфункции есть необязательный третий аргументchild. Это позволяет вам передать дочерний виджет, который не зависит от провайдера, и он не будет перестраиваться.
- Перестраивает только себя: Это главное преимущество. Если ваш
- "Подводные камни":
- Может сделать дерево виджетов более глубоким из-за вложенности
Consumer'ов, хотя это редко является большой проблемой. - Если вам нужно только вызвать метод провайдера (т.е., не нужно "слушать" его изменения), то
Provider.of<T>(context, listen: false)илиcontext.read<T>()будут более подходящими, так какConsumerвсегда "слушает".
- Может сделать дерево виджетов более глубоким из-за вложенности
2. `Selector<T, S>`
Selector — это более продвинутая версия Consumer, которая позволяет вам указать, какую именно часть данных из ChangeNotifier нужно "слушать". Виджет перестроится только тогда, когда изменится выбранная часть данных.
- Особенности:
- Точечная оптимизация перестроек:
Selectorпринимает функциюselector, которая извлекает ("селектирует") часть данных из вашего провайдера. Виджет будет перестраиваться только если результат этой функции изменится. Это очень мощный инструмент для тонкой настройки производительности. shouldRebuild(необязательно): Можно также передать функциюshouldRebuild, чтобы еще точнее контролировать, когда должна произойти перестройка (например, еслиnewCount != oldCount— перестраивать, иначе нет).
- Точечная оптимизация перестроек:
- "Подводные камни":
- Функция
selectorдолжна быть "чистой" (pure function) — она не должна вызывать побочных эффектов и должна возвращать один и тот же результат для одних и тех же входных данных.
- Функция
3. `Provider.of<T>(context)`
Этот метод является одним из самых распространенных способов получения данных из провайдера. Он позволяет получить экземпляр ChangeNotifier из ближайшего ChangeNotifierProvider в дереве виджетов.
- Особенности:
- Автоматически "слушает" (по умолчанию): Если вы используете
Provider.of<T>(context), виджет, в котором он вызывается, будет перестраиваться каждый раз, когдаnotifyListeners()вызывается в соответствующемChangeNotifier. - Гибкость: Можно использовать
listen: falseдля случаев, когда вам просто нужен доступ к методам провайдера, но не нужно перестраивать виджет при изменении его состояния.
- Автоматически "слушает" (по умолчанию): Если вы используете
- "Подводные камни":
- Перестраивает весь виджет: Если вы используете
Provider.of<T>(context)(безlisten: false) вbuildметодеStatelessWidgetилиStatefulWidget, то весь этот виджет будет перестраиваться при любом изменении вChangeNotifier. Это может привести к ненужным перестройкам и снижению производительности, если только небольшая часть виджета зависит от изменяющихся данных. - Должен быть вызван в
buildметоде или в методе жизненного цикла (например,didChangeDependencies) послеinitState.
- Перестраивает весь виджет: Если вы используете
4. `context.watch<T>()`
Это более лаконичная и современная синтаксическая "сахарная" обертка над Provider.of<T>(context, listen: true). Она делает код более читабельным.
- Особенности:
- То же самое, что
Provider.of<T>(context, listen: true): Виджет, использующийcontext.watch<T>(), будет перестраиваться при любых изменениях вChangeNotifier. - Короткий синтаксис: Удобно и чисто.
- Доступно только в
buildметоде: Как иProvider.of<T>(context),context.watch<T>()должен использоваться вbuildметоде виджета.
- То же самое, что
- "Подводные камни":
- Те же, что и у
Provider.of<T>(context)безlisten: false: перестраивает весь виджет, если не использоватьConsumerилиSelectorдля более точечных перестроек.
- Те же, что и у
5. `context.read<T>()`
Этот метод используется, когда вам нужно получить экземпляр ChangeNotifier для вызова его методов, но вам не нужно, чтобы виджет перестраивался при изменении состояния ChangeNotifier.
- Особенности:
- Не "слушает" изменения: Виджет, использующий
context.read<T>(), не будет перестраиваться, когдаnotifyListeners()вызывается в провайдере. Это делает его идеальным для вызова методов в обработчиках событий (например,onPressed). - Доступен в любом месте: Можно использовать как в
buildметоде, так и за его пределами (например, в колбэкахonPressed), потому что он не зависит от жизненного цикла перестроек.
- Не "слушает" изменения: Виджет, использующий
- "Подводные камни":
- Не используйте для отображения данных, которые должны меняться: Если вы используете
context.read<T>()для получения данных, которые должны обновляться в UI, эти данные не будут обновляться, так как виджет не "слушает" изменения.
- Не используйте для отображения данных, которые должны меняться: Если вы используете