Keep state in the common ancestor and turn your child widgets into controlled widgets that report changes back through callbacks.
Open this lesson in KodokonA classic situation: a quantity selector and a total displayed elsewhere need to share the same data. The golden rule: state lives in the nearest common ancestor of the widgets that depend on it. The parent owns the data, passes it down as a parameter, and receives changes through a callback that flows back up. This is "lifting state up".
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} €'),
],
),
);
}
}The child becomes a controlled widget: it's a StatelessWidget, stores nothing, displays the value it receives, and signals each intent through onChanged. ValueChanged<int> is simply an alias for void Function(int). Notice the minus button disabled at quantity 1: passing null to onPressed is enough, and this logic follows directly from the value received.
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),
),
],
);
}
}You use this contract all the time without realizing it: Checkbox, Slider, Switch are all controlled widgets (value + onChanged). When the tree deepens and threading callbacks through five levels becomes tedious, it will be time to discover state management solutions - that's the subject of the next module.