Creating a StatefulWidget that passes data down using a Builder pattern is a common and effective way to manage local state without passing values to widget contructors. Thos way you provide values to children without manually passing parameters through every constructor (prop-drilling).
The “Provider-lite” Pattern
In Flutter, this is typically achieved by creating a custom widget that contains a builder function as a property. This function takes the current state/value and returns a Widget.
Step-by-Step Implementation
Here is how you build a counter widget that passes its value down to whatever UI you define in the builder.
1. Define the Widget
The widget needs a builder property. This is a function that usually looks like: Widget Function(BuildContext context, T value).
class MyDataRoot extends StatefulWidget {
// The builder allows the parent to define the UI while receiving the value
final Widget Function(BuildContext context, int value, VoidCallback increment) builder;
const MyDataRoot({super.key, required this.builder});
@override
State<MyDataRoot> createState() => _MyDataRootState();
}
2. Define the State
The state holds the actual logic and data. Inside the build method, you simply call the widget.builder function.
class _MyDataRootState extends State<MyDataRoot> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
// We pass the state and the logic into the builder
return widget.builder(context, _counter, _incrementCounter);
}
}
3. Use the Widget
Now, anywhere in your app, you can wrap a section of your UI with MyDataRoot. The children inside the builder now have access to the value without it being a global variable.
@override
Widget build(BuildContext context) {
return MyDataRoot(
builder: (context, value, increment) {
return Column(
children: [
Text('The current value is: $value'),
ElevatedButton(
onPressed: increment,
child: const Text('Add 1'),
),
],
);
},
);
}
Why use this instead of a constructor?
| Feature | Builder Pattern | Standard Constructor |
|---|---|---|
| Decoupling | The logic-heavy widget doesn’t need to know what the UI looks like. | The parent must know exactly which children to instantiate. |
| Reusability | You can wrap any UI in MyDataRoot to give it “counter powers.” | You have to rewrite the logic for every new UI screen. |
| Rebuilds | Only the code inside the builder is swapped/updated. | The entire parent widget usually rebuilds. |
When to move to InheritedWidget?
If you find yourself nesting multiple builders or needing to access that value deep down (10+ layers) in the widget tree, you should switch from this Builder pattern to an InheritedWidget (or a package like Provider or Riverpod).
