NOTE:
I ended up restructuring things instead, eliminating the sibling-communication path.
ParentWidget
|-ChildWidgetA
|-ChildWidgetB
Because I needed two different versions of ChildWidgetA (one static, one with editing fields), and because I needed bi-directional communication (sibling B sends command to A, A returns final status back to B)…
It all just got two complicated. Made the "sibling" node a child instead, making communication easier.
Lesson: if you need sibling-to-sibling communication, try hard to find a way to make them parent/child instead. Sibling-to-sibling widget communication is possible but makes code very hard to follow due to very indirect code flow.
——— Original Post ———–
Situation: Parent widget, with two children.
As:
ParentWidget
|-ChildWidgetA
|-ChildWidgetB
ChildWidgetA contains a data entry form.
ChildWidgetB contains buttons (save, cancel, export, etc)
Need action (button click) in ChildWidgetB to trigger response (read and save of text data) in ChildWidgetA.
Changing parent/child relationships like this:
ParentWidget
|-ChildWidgetA
|-ChildWidgetB
… would work (so child could use callback provided by parent) but is not a possibility in my actual situation, unless there’s no other possible solution.
I’d also rather not pull knowledge of the data entry form up into the (unrelated) ParentWidget by, for example, creating TextEditingController
objects there for to help the ControlsWidget read text values for save.
Question: What possibilities exist to get this to work if I alter/rearrange the code? (Perhaps streams, event listeners, state magic…?)
Here’s some annotated example code that demonstrates the structural issue:
class ParentWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
DataEntryFormWidget(), // TextFields with values to read, save
ControlsWidgetWithSaveButton(), // Button I want to trigger the save
],
);
}
}
class DataEntryFormWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
TextFormField(), // assume I have a way to read values from these
TextFormField(),
// etc
]
);
}
void doSave() {
// read values from TextFormField()
// This is the thing I cannot do from ControlsWidgetWithSaveButton
// because that doesn't have access to TextFormField(s)...
// So can't simply move this code/function to controls widget.
//
// getDatabase.save(... the values ...)
// (all saved ok)
}
}
class ControlsWidgetWithSaveButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
ElevatedButton(
onPressed: () {
// THE PROBLEM:
// Here I want to invoke/trigger the doSave() function.
// Could do the DB write here if I had the data...
},
child: const Text('Save'),
),
// ... <assume some other buttons>
]
);
}
}
2
Answers
One way is using the provider package. Similarly you can do it with other state management packages but this is the easiest one.
First, we need to create a
ChangeNotifier
, thesave
method will be used to notify the listeners that save was requested.change your
ParentWidget
to register the providerYou need to convert your
DataEntryFormWidget
to aStatefulWidget
because we need to handle theinitState
anddispose
. In theinitState
we will add a listener that calls ourdoSave
method and insidedispose
we will remove the listener.Finally, in the
ControlsWidgetWithSaveButton
widget, inside theonPressed
method, we are calling thesave
to notify the listeners.You can use GlobalKey and then call methods in other
components.
GlobalKey<ComponentsBState> componentsBStateGlobalKey = GlobalKey()
.Attention should be paid to deleting ‘_’ when declaring, Make it a state class that can be accessed externally.
Like this:
class ComponentsBState extends State<ComponentsB> {}