skip to Main Content

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


  1. 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, the save method will be used to notify the listeners that save was requested.

    class FormController extends ChangeNotifier {
      void save() {
        notifyListeners();
      }
    }
    

    change your ParentWidget to register the provider

    class ParentWidget extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return ChangeNotifierProvider(
          create: (context) => FormController(),
          child: Column(
            children: [
              DataEntryFormWidget(), // TextFields with values to read, save
              ControlsWidgetWithSaveButton(), // Button I want to trigger the save
            ],
          ),
        );
      }
    }
    

    You need to convert your DataEntryFormWidget to a StatefulWidget because we need to handle the initState and dispose. In the initState we will add a listener that calls our doSave method and inside dispose we will remove the listener.

    class DataEntryFormWidget extends StatefulWidget {
      @override
      State<DataEntryFormWidget> createState() => _DataEntryFormWidgetState();
    }
    
    class _DataEntryFormWidgetState extends State<DataEntryFormWidget> {
      @override
      void initState() {
        super.initState();
        Provider.of<FormController>(context, listen: false).addListener(doSave);
      }
    
      @override
      void dispose() {
        Provider.of<FormController>(context, listen: false).removeListener(doSave);
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Column(children: [
          TextFormField(),
          TextFormField(),
        ]);
      }
    
      void doSave() {
        // save
      }
    }
    

    Finally, in the ControlsWidgetWithSaveButton widget, inside the onPressed method, we are calling the save to notify the listeners.

    class ControlsWidgetWithSaveButton extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Row(children: [
          ElevatedButton(
            onPressed: () {
              Provider.of<FormController>(context, listen: false).save();
            },
            child: const Text('Save'),
          ),
        ]);
      }
    }
    
    Login or Signup to reply.
  2. 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> {}

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search