Kodokon kodokon.com

共享状态:状态提升与回调

把状态保存在共同的祖先中,并把你的子 widget 变成受控 widget,让它们通过回调把变化上报回来。

8 分钟 · 3 题

在 Kodokon 中打开本课

一个经典的场景:一个数量选择器和显示在别处的总价需要共享同一份数据。黄金法则:状态存放在依赖它的那些 widget 最近的共同祖先中。父组件拥有数据,把它作为参数往下传,并通过一个向上回流的回调来接收变化。这就是 "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} €'),
        ],
      ),
    );
  }
}
父组件拥有状态,并把值和回调分发出去。

子组件成了一个受控 widget:它是一个 StatelessWidget,不保存任何东西,显示它收到的值,并通过 onChanged 发出每一个意图。ValueChanged<int> 只不过是 void Function(int) 的别名而已。注意在数量为 1 时被禁用的减号按钮:只要给 onPressed 传入 null 就够了,而这个逻辑直接由收到的值推导而来。

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),
        ),
      ],
    );
  }
}
子组件显示它收到的值,并把意图上报回去。

你其实一直在不知不觉地使用这个约定:CheckboxSliderSwitch 都是受控 widget(value + onChanged)。当树变得越来越深,把回调层层穿过五层变得令人厌烦时,就该去认识状态管理方案了 - 那正是下一单元的主题。

知识检测

确认你已牢记本课的重点内容。

  1. 应该把两个兄弟 widget 共享的状态放在哪里?
    • 分别放在这两个 widget 各自内部
    • 放在最近的共同祖先中
    • 放在一个全局变量里
    • 放在 MaterialApp 里
  2. 子 widget 是如何请求修改状态的?
    • 它直接在父组件上调用 setState
    • 它修改自己作为参数收到的属性
    • 它调用父组件提供的回调
  3. 什么是受控 widget?
    • 一个接收自己的值、通过回调上报变化、且没有自身状态的 widget
    • 一个必须使用 setState 的 widget
    • 一个被单元测试覆盖的 widget
    • 一个用 const 声明的 widget