Поднятие состояния и передача данных

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

Суть: данные хранятся в общем родителе и передаются вниз через параметры, а изменения «всплывают» вверх через колбэки-функции. Это базовый паттерн без сторонних библиотек.

Паттерн «данные вниз, события вверх» — это не прихоть Flutter, а проверенный принцип однонаправленного потока данных, лежащий в основе многих современных фреймворков. Его сила в предсказуемости: чтобы понять, почему на экране именно эти данные, достаточно посмотреть на одно место — источник правды в родителе. Не нужно гадать, какой из десяти виджетов втихую поменял значение, ведь менять его имеет право только владелец.

Представьте: кнопка «+» в одном виджете, а число-счётчик в другом. Если состояние спрятано внутри кнопки, счётчик о нём не узнает. Решение — lifting state up: вынести общие данные в ближайшего общего родителя. Родитель хранит состояние, передаёт значение вниз дочерним виджетам и даёт им колбэк, чтобы сообщать о действиях.

class CounterPage extends StatefulWidget {
  const CounterPage({super.key});
  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int count = 0;   // состояние живёт у общего предка

  @override
  Widget build(BuildContext context) {
    return Column(children: [
      CounterDisplay(count: count),                 // данные вниз
      AddButton(onPressed: () => setState(() => count++)), // событие вверх
    ]);
  }
}

class CounterDisplay extends StatelessWidget {
  final int count;
  const CounterDisplay({super.key, required this.count});
  @override
  Widget build(BuildContext context) => Text('Счёт: $count');
}

class AddButton extends StatelessWidget {
  final VoidCallback onPressed;
  const AddButton({super.key, required this.onPressed});
  @override
  Widget build(BuildContext context) =>
      ElevatedButton(onPressed: onPressed, child: const Text('+'));
}

Как работает поток данных под капотом

Во Flutter данные текут вниз (от родителя к детям через параметры), а события — вверх (от детей к родителю через колбэки). Дочерний виджет не меняет состояние сам; он лишь вызывает переданную функцию onPressed, а родитель решает, что делать, и вызывает setState. Это делает поток предсказуемым: единственный источник правды — родитель.

        CounterPage (хранит count = 0)
        |  данные вниз          ^  события вверх
        |  (count: 0)           |  (onPressed колбэк)
        v                       |
  CounterDisplay            AddButton
   показывает 0          нажатие -> вызывает onPressed()
                                 -> родитель: setState count++
                                 -> новые данные текут вниз снова
# Модель lifting state up: родитель — источник правды
class CounterPage:
    def __init__(self):
        self.count = 0

    def display(self):                 # данные вниз
        print(f'Счёт: {self.count}')

    def on_add_pressed(self):          # событие вверх -> меняет родитель
        self.count += 1
        self.display()                 # перерисовка после изменения

page = CounterPage()
page.display()          # Счёт: 0
page.on_add_pressed()   # ребёнок "нажал" -> родитель обновил -> Счёт: 1
page.on_add_pressed()   # Счёт: 2

Частые ошибки

  • Дублировать состояние в нескольких виджетах — они рассинхронизируются. Источник правды должен быть один.
  • Менять данные прямо в дочернем виджете вместо вызова колбэка — нарушает однонаправленный поток.
  • Поднимать состояние слишком высоко — перестраивается лишний поддеревья. Поднимайте до ближайшего общего предка, не выше.

Best practices

  • Храните общие данные у ближайшего общего родителя — не выше, чем нужно.
  • Передавайте данные вниз параметрами, события — вверх колбэками (VoidCallback, ValueChanged).
  • Когда дерево становится глубоким и колбэков много — пора смотреть на стейт-менеджеры (следующий урок).

За колбэками во Flutter стоят удобные типы: VoidCallback для действий без аргументов, ValueChanged<T> для передачи изменённого значения наверх (например, новый текст из поля ввода). Используя эти стандартные типы вместо самодельных, вы делаете интерфейс своих виджетов узнаваемым: любой Flutter-разработчик с ходу поймёт, что onChanged: ValueChanged<String> сообщает родителю новое строковое значение.

Итог: поднятие состояния — фундаментальный паттерн Flutter: данные вниз, события вверх, один источник правды. Он отлично работает для небольших экранов, но в крупных приложениях колбэки множатся — тогда на сцену выходят инструменты управления состоянием.

Проверьте себя
1. Куда помещают состояние, общее для нескольких виджетов?
AВ каждый виджет отдельно
BВ ближайшего общего родителя (lifting state up)
CВ глобальную переменную
DВ метод build ребёнка
2. Как дочерний виджет сообщает родителю о действии пользователя?
AМеняет данные родителя напрямую
BВызывает переданный ему колбэк (например, onPressed)
CСоздаёт новый родитель
DЧерез глобальную переменную