Provider — База

Provider

Provider — это популярный пакет для управления состоянием в приложениях Flutter. Он представляет собой обертку над InheritedWidget, делая его проще в использовании и более гибким для многократного применения.

Пакет установим с сайта https://pub.dev/packages/provider
В этом уроке будем использовать версию provider 6.1.5

На прошлых уроках мы научились пользоваться InheritedWidget и ChangeNotifier для элегантной передачи состояния между виджетами, без дополнительных build()

Provider позволяет делать тоже самое, но более профессионально и легче.

Некоторые ключевые преимущества использования Provider:

Кратко принцип работы:

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 Demo

Проблемы

  1. Можно легко ошибиться
  2. Утомительное занятие
  3. Ухудшение производительности приложения, каждый виджет будет перестраиваться

Используем `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 добавим новы директории

В 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) состояния в виджетах

Чтобы получить данные и правильно их обработать есть несколько методов:

  1. Consumer - Слушает изменения в ChangeNotifier и перестраивать только определенную часть UI.
  2. Selector - Продвинутый Consumer позволяет указать, какую часть данных из ChangeNotifier нужно "слушать". Виджет перестроится только тогда, когда изменится только выбранная часть данных.
  3. Provider.of<T>(context) - Получить данные и подписаться или нет на изменения
  4. context.watch<T>() - Получить данные и подписаться на изменения
  5. 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 нет смысла лишний раз перестраивать.

Используем

Файл 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);  
  }  
}

Или более красивые современные методы

Файл 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()

Build logs

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 не перестраивается!!!
Обновляется только иконка сердечка!
Очень круто!

Provider with Consumer

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.

2. `Selector<T, S>`

Selector — это более продвинутая версия Consumer, которая позволяет вам указать, какую именно часть данных из ChangeNotifier нужно "слушать". Виджет перестроится только тогда, когда изменится выбранная часть данных.

3. `Provider.of<T>(context)`

Этот метод является одним из самых распространенных способов получения данных из провайдера. Он позволяет получить экземпляр ChangeNotifier из ближайшего ChangeNotifierProvider в дереве виджетов.

4. `context.watch<T>()`

Это более лаконичная и современная синтаксическая "сахарная" обертка над Provider.of<T>(context, listen: true). Она делает код более читабельным.

5. `context.read<T>()`

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