12

This is a simplified version of the scenario:

class ParentWdiegt extends StatelessWidget{
//
//
floatinActionButton: FloatingActionButtonWidget(onPressed:()=>CustomWidgetState.someMethod(someValue))
//
//somewhere in the ParentWidget tree
child: CustomWidget() //is stateful
}

CustomWidgetState

class CustomWidgetState extends State<CustomWidget>{
//trigger this function when FAB is pressed in parent widget
someMethod(SomeValue) {//}
}

Is there any way that I can expose someMethod in the state object to be triggered when FAB is pressed without using InheritedWidget?

Shady Aziza
  • 50,824
  • 20
  • 115
  • 113
  • Try this https://stackoverflow.com/questions/50639599/flutter-how-to-pass-variable-from-one-dart-class-to-another-dart-class/50642280#50642280 – Vinoth Kumar Jun 07 '18 at 06:04
  • Does it really makes sense to call something on the state ? – Rémi Rousselet Jun 07 '18 at 08:18
  • @RémiRousselet I have a `CustomWidget` that is `Stateful` and I want to keep all state manipulation logic inside the `CustomWidgetState`. So one case of invoking the state manipulation logic inside `CustomWidgetState` is done by pressing some buttons from other widgets like `ParentWidget` in my question. The reason I am doing it this way is because I do not want to build ANY state manipulation logic outside the `State` object. If you have a better way of achieving this I would appreciate the help. – Shady Aziza Jun 07 '18 at 08:47
  • 1
    Would https://stackoverflow.com/questions/50430273/how-to-set-state-from-another-widget/50430389#50430389 make sense ? The valueListener could be a clickevent. And the `CustomWidgetState` subscribe to that clickEvent to do stuff – Rémi Rousselet Jun 07 '18 at 08:52
  • Looks interesting, but as far I understand, this will only work with fields but can not trigger methods, am I correct? – Shady Aziza Jun 07 '18 at 09:43
  • No no, you can call methods with listenables too. Just don't use `AnimatedWidget` as you don't need it. Something like `listenable.addListener(someMethod)` – Rémi Rousselet Jun 07 '18 at 09:45
  • Can you show me an example by using it with methods please? I am not really sure how to transform your answer to be used with a method – Shady Aziza Jun 07 '18 at 09:49
  • Done. I hope it's clear enough ! – Rémi Rousselet Jun 07 '18 at 10:33

2 Answers2

39

While GlobalKey allows for an easy access to any widget's state ; avoid it. Widgets should not interact with other widgets directly. This is one of the core principle of Flutter.

Flutter uses reactive programming instead. Where widgets communicate with each others by submitting events. Not by directly editing the desired widget.

The obvious benefit is that widgets stays independant. And potentially dozens of widgets can communicate with each others using the same principle.

I already made an example here on how to make two different widgets share a common editable value.

If you want to call methods instead, this uses the same principle : A Listenable or Stream shared between widgets. But without using AnimatedWidget or StreamBuilder for the listening. Instead we'll do the listening manually (which requires slighly more boilerplate) to trigger a custom function.

Here's an example using Stream.

import 'dart:async';
import 'package:flutter/material.dart';

class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  final changeNotifier = new StreamController.broadcast();

  @override
  void dispose() {
    changeNotifier.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return new Column(
      children: <Widget>[
        new AnotherWidget(
          shouldTriggerChange: changeNotifier.stream,
        ),
        new RaisedButton(
          child: new Text("data"),
          onPressed: () => changeNotifier.sink.add(null),
        )
      ],
    );
  }
}

class AnotherWidget extends StatefulWidget {
  final Stream shouldTriggerChange;

  AnotherWidget({@required this.shouldTriggerChange});

  @override
  _AnotherWidgetState createState() => _AnotherWidgetState();
}

class _AnotherWidgetState extends State<AnotherWidget> {
  StreamSubscription streamSubscription;

  @override
  initState() {
    super.initState();
    streamSubscription = widget.shouldTriggerChange.listen((_) => someMethod());
  }

  @override
  didUpdateWidget(AnotherWidget old) {
    super.didUpdateWidget(old);
    // in case the stream instance changed, subscribe to the new one
    if (widget.shouldTriggerChange != old.shouldTriggerChange) {
      streamSubscription.cancel();
      streamSubscription = widget.shouldTriggerChange.listen((_) => someMethod());
    }
  }

  @override
  dispose() {
    super.dispose();
    streamSubscription.cancel();
  }

  void someMethod() {
    print('Hello World');
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

In this example, someMethod of AnotherWidget will be called whenever a click on the RaisedButton instantiated by _ParentWidgetState is performed.

Benno Richters
  • 15,378
  • 14
  • 42
  • 45
Rémi Rousselet
  • 256,336
  • 79
  • 519
  • 432
  • this doesn't work for me, really no idea, I basically copied and pasted. – Kaki Master Of Time Jan 19 '19 at 15:39
  • How would this work if a stateful widget initiates an async action in a singleton non-widget class (e.g. retrieving some data from an API) and needs to be informed when that action has finished and what the result type (success, error) is (i.e. get an object of a class which extends Notification) ? My intent is that the initiating button has to be disabled until the response notification has arrived. – valley Jan 24 '19 at 16:26
  • 1
    This is a perfect explanation, thanks! Came in handy for a complex interaction I'm building. – zeh Mar 12 '19 at 22:05
  • This works only when two `Widget` are directly interacted, when it comes to the situation that `First Widget` generates `Second Widget` that generates `Third Widget`,then, how the `First Widget` reacts when `Third Widget` state changed? – zionpi Nov 18 '19 at 02:17
4

You can use GlobalKey for that:

// some global place
final customWidgetKey = new GlobalKey<CustomWidgetState>();

...

  // import the file with "customWidgetKey"

  new CustomWidget(key: customWidetKey, ...)

...

  // import the file with "customWidgetKey"

  floatinActionButton: FloatingActionButtonWidget(
      onPressed: ()=>customWidgetKey.currentState.someMethod(someValue))
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • 1
    Usually bad idea. Prefer `context.ancestorStateOfType` – Rémi Rousselet Jun 07 '18 at 08:18
  • `ancestorStateOfType` seems fragile to me but thanks for pointing out, I wasn't aware of that method. – Günter Zöchbauer Jun 07 '18 at 08:19
  • That's how we get `NatigatorState` using `Navigator.of`. Anyway `GlobalKey` must be unique inside the view ; which is quite incovenient. Especially in that situation – Rémi Rousselet Jun 07 '18 at 08:22
  • `Navigator` is a more specific use case where some hierarchy can be assumed. I don't see hints in the question about specific use cases. Not sure what you mean with "Especially in that situation" - do you you mean your `Navigator` example or did you derive something from the question that I might have missed? – Günter Zöchbauer Jun 07 '18 at 08:25
  • 1
    I meant that potentially, more then one widget want to react to the button click. But with `GlobalKey` you'd need to know in advance how the whole widget tree works. I think the answer I added explain fairly well my line of thought on the topic. – Rémi Rousselet Jun 07 '18 at 10:43
  • 2
    I see your point, but I don't fully agree. Not every widget someone wants to communicate to is an ancestor. Actually I'd do it without Flutter anyway by using something like an event bus (redux, ...) instead. – Günter Zöchbauer Jun 07 '18 at 11:31