Kodokon kodokon.com

Общее состояние: подъём состояния и колбэки

Храни состояние в общем предке и превращай дочерние виджеты в контролируемые виджеты, которые сообщают об изменениях через колбэки.

8 мин · 3 вопросов

Открыть этот урок в Kodokon

Классическая ситуация: выбор количества и итоговая сумма, показанная в другом месте, должны использовать одни и те же данные. Золотое правило: состояние живёт в ближайшем общем предке виджетов, которые от него зависят. Родитель владеет данными, передаёт их вниз параметром и получает изменения через колбэк, который поднимается обратно наверх. Это и есть «подъём состояния» (lifting state up).

DART
import 'package:flutter/material.dart';

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

  @override
  State<CartScreen> createState() => _CartScreenState();
}

class _CartScreenState extends State<CartScreen> {
  int _quantity = 1;

  void _updateQuantity(int value) {
    setState(() => _quantity = value);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Cart')),
      body: Column(
        children: [
          QuantitySelector(
            quantity: _quantity,
            onChanged: _updateQuantity,
          ),
          Text('Total: ${_quantity * 20} €'),
        ],
      ),
    );
  }
}
Родитель владеет состоянием и раздаёт значение и колбэк.

Дочерний виджет становится контролируемым виджетом: это StatelessWidget, он ничего не хранит, показывает полученное значение и сообщает о каждом намерении через onChanged. ValueChanged<int> - это просто псевдоним для void Function(int). Обрати внимание на кнопку «минус», отключённую при количестве 1: достаточно передать null в onPressed, и эта логика напрямую следует из полученного значения.

DART
import 'package:flutter/material.dart';

class QuantitySelector extends StatelessWidget {
  const QuantitySelector({
    super.key,
    required this.quantity,
    required this.onChanged,
  });

  final int quantity;
  final ValueChanged<int> onChanged;

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        IconButton(
          icon: const Icon(Icons.remove),
          onPressed: quantity > 1
              ? () => onChanged(quantity - 1)
              : null,
        ),
        Text('$quantity'),
        IconButton(
          icon: const Icon(Icons.add),
          onPressed: () => onChanged(quantity + 1),
        ),
      ],
    );
  }
}
Дочерний виджет показывает полученное значение и передаёт намерения наверх.

Ты пользуешься этим контрактом постоянно, сам того не замечая: Checkbox, Slider, Switch - все они контролируемые виджеты (value + onChanged). Когда дерево станет глубже и протаскивать колбэки через пять уровней надоест, придёт время познакомиться с решениями для управления состоянием - это тема следующего модуля.

Проверка знаний

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

  1. Где размещать состояние, общее для двух соседних виджетов?
    • В каждом из двух виджетов
    • В ближайшем общем предке
    • В глобальной переменной
    • В MaterialApp
  2. Как дочерний виджет запрашивает изменение состояния?
    • Он вызывает setState напрямую у родителя
    • Он изменяет свойство, полученное параметром
    • Он вызывает колбэк, переданный родителем
  3. Что такое контролируемый виджет?
    • Виджет, который получает своё значение и сообщает об изменениях через колбэк, без собственного состояния
    • Виджет, который обязан использовать setState
    • Виджет, покрытый модульными тестами
    • Виджет, объявленный как const