Урок: Lifting State Up и Provider

Проблема Lifting State Up

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

Чтобы синхронизировать состояние по иерархии Flutter-виджетов, приходится поднимать данные выше и передавать их через конструкторы. Такой процесс называют Lifting State Up.

Посмотрим, как выглядит ручное поднятие состояния.

Поднимаем состояние вручную

Шаг 1. Делаем корневой виджет MyApp stateful и храним в нём состояние карточки.

Файл main.dart — состояние в корне
Светлая тема Тёмная тема
import 'package:flutter/material.dart';
import 'package:flutter_application_1/1_stateless_widget.dart';

void main() => runApp(const MyApp());

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String title = "Flutter Course 2025";
  String author = "IT-Квантум";
  bool isFavorite = false;

  void onLike() {
    setState(() {
      isFavorite = !isFavorite;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Flutter Course 2025",
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.greenAccent),
        fontFamily: "Montserrat",
      ),
      home: Scaffold(
        body: HomeWidget(
          title: title,
          author: author,
          isFavorite: isFavorite,
          onLike: onLike,
        ),
      ),
    );
  }
}

class HomeWidget extends StatelessWidget {
  final String title;
  final String author;
  final bool isFavorite;
  final VoidCallback onLike;

  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(
      decoration: const BoxDecoration(
        gradient: LinearGradient(
          colors: [Color(0xFFBFF098), Color(0xFF6FD6FF)],
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
        ),
      ),
      child: SafeArea(
        child: Center(
          child: Padding(
            padding: const EdgeInsets.all(64.0),
            child: TrackCard(
              title: title,
              author: author,
              isFavorite: isFavorite,
              onLike: onLike,
            ),
          ),
        ),
      ),
    );
  }
}

Шаг 2. Каждый дочерний виджет принимает свои параметры и пробрасывает их дальше.

Файл 1_stateless_widget.dart — проброс параметров
Светлая тема Тёмная тема
import 'package:flutter/material.dart';

class TrackCard extends StatelessWidget {
  final String title;
  final String author;
  final bool isFavorite;
  final VoidCallback onLike;

  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/s.png"),
        title: TitleWidget(title: title),
        subtitle: SubtitleWidget(author: author),
        trailing: LikeButton(
          isFavorite: isFavorite,
          onLike: onLike,
        ),
      ),
    );
  }
}

class TitleWidget extends StatelessWidget {
  final String title;

  const TitleWidget({super.key, required this.title});

  @override
  Widget build(BuildContext context) {
    debugPrint("💛TitleWidget build");
    return Text(
      title,
      style: const TextStyle(fontWeight: FontWeight.bold),
    );
  }
}

class SubtitleWidget extends StatelessWidget {
  final String author;

  const SubtitleWidget({super.key, required this.author});

  @override
  Widget build(BuildContext context) {
    debugPrint("💛SubtitleWidget build");
    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("💛LikeButton build");

    return IconButton(
      onPressed: onLike,
      icon: isFavorite
          ? const Icon(Icons.favorite, color: Colors.red)
          : const Icon(Icons.favorite_border),
    );
  }
}

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

Почему это неудобно

Любое изменение isFavorite заставляет перестраиваться все виджеты, даже те, кто не использует это состояние. Чем больше приложение, тем заметнее удар по производительности.

Минусы ручного поднятия состояния

  • Много «прокладок» в конструкторе у каждого виджета.
  • Сложно масштабировать и переиспользовать компоненты.
  • Все уровни подписаны на одно и то же обновление — происходит массовый rebuild.
Все виджеты постоянно перерисовываются

Массовые перерисовки при ручном Lifting State Up

Работа приложения с Provider

Хотим управлять состоянием в одном месте и перерисовывать только те виджеты, которые действительно зависят от данных. Поможет популярный пакет provider.

Устанавливаем Provider

Шаг 1. Находим пакет на pub.dev.

Страница поиска provider Карточка пакета provider Вкладка Installing

Шаг 2. Добавляем зависимость в pubspec.yaml.

Файл pubspec.yaml
Светлая тема Тёмная тема
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.8
  provider: ^6.1.5+1

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^5.0.0

flutter:
  uses-material-design: true
  assets:
    - assets/images/

После обновления зависимостей запустите flutter pub get, чтобы скачать пакет.

Шаг 3. Организуем код в папках:

Готовим провайдер данных

Создадим TrackProvider, который будет хранить, менять и оповещать слушателей об изменении состояния.

Файл lib/providers/track_provider.dart
Светлая тема Тёмная тема
import 'package:flutter/foundation.dart';

class TrackProvider extends ChangeNotifier {
  final String _author;
  final String _title;
  bool _isFavorite = false;

  TrackProvider(this._author, this._title);

  String get title => _title;
  String get author => _author;
  bool get isFavorite => _isFavorite;

  void toggleFavorite() {
    _isFavorite = !_isFavorite;
    notifyListeners();
  }
}

Метод notifyListeners() сообщит всем подписчикам, что данные изменились, и нужно перерисовать только их.

Используем Provider в виджетах

Теперь можем оставить все виджеты stateless. Они будут брать данные напрямую из провайдера через context.read или context.watch.

Файл lib/views/track_card.dart
Светлая тема Тёмная тема
import 'package:flutter/material.dart';
import 'package:flutter_application_1/providers/track_provider.dart';
import 'package:provider/provider.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/s.png"),
        title: const TitleWidget(),
        subtitle: const SubtitleWidget(),
        trailing: const LikeButton(),
      ),
    );
  }
}

class TitleWidget extends StatelessWidget {
  const TitleWidget({super.key});

  @override
  Widget build(BuildContext context) {
    final title = context.read<TrackProvider>().title;
    debugPrint("💛TitleWidget build");

    return Text(
      title,
      style: const TextStyle(fontWeight: FontWeight.bold),
    );
  }
}

class SubtitleWidget extends StatelessWidget {
  const SubtitleWidget({super.key});

  @override
  Widget build(BuildContext context) {
    final author = context.read<TrackProvider>().author;
    debugPrint("💛SubtitleWidget build");
    return Text(author.toUpperCase());
  }
}

class LikeButton extends StatelessWidget {
  const LikeButton({super.key});

  @override
  Widget build(BuildContext context) {
    final isFavorite = context.watch<TrackProvider>().isFavorite;
    debugPrint("💛LikeButton build");

    return IconButton(
      onPressed: () => context.read<TrackProvider>().toggleFavorite(),
      icon: isFavorite
          ? const Icon(Icons.favorite, color: Colors.red)
          : const Icon(Icons.favorite_border),
    );
  }
}

context.watch подписывает кнопку на изменения. Остальные виджеты используют context.read и не перерисовываются при каждом лайке.

Подключаем Provider в приложении

Обернём приложение в ChangeNotifierProvider, чтобы передать провайдер во всё дерево виджетов.

Файл lib/main.dart — итоговая схема
Светлая тема Тёмная тема
import 'package:flutter/material.dart';
import 'package:flutter_application_1/providers/track_provider.dart';
import 'package:flutter_application_1/views/track_card.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => TrackProvider("IT-Квантум", "Flutter Course 2025"),
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Flutter Course 2025",
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.greenAccent),
        fontFamily: "Montserrat",
      ),
      home: const Scaffold(
        body: SafeArea(
          child: Center(
            child: Padding(
              padding: EdgeInsets.all(64.0),
              child: TrackCard(),
            ),
          ),
        ),
      ),
    );
  }
}

Provider заставляет перерисовываться только LikeButton

Итоги

Используйте provider (или похожие решения), когда данные нужны многим компонентам. Так вы уменьшите количество дублирования и получите масштабируемую архитектуру.