Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to open DropdownButton when other widget is tapped, in Flutter?

Tags:

flutter

dart

I need to have a DropdownButton's list of options open/show programmatically when some other widget is tapped. I know that this may not be UI-best-practice and all, but I need this behavior:

As an example, in a structure like the one below, I may need to have taping Text("every") to open the neighboring DropdownButton's dropdown list, behaviors similar to clicking a <select>'s label in HTML.

Row(children: [
  Padding(
    padding: const EdgeInsets.only(right: 16),
    child: Text('every'),
  ),
  Expanded(
    child: DropdownButton<String>(
      value: _data['every'],
      onChanged: (String val) => setState(() => _data['every'] = val),
      items: _every_options.map<DropdownMenuItem<String>>(
        (String value) {
          return DropdownMenuItem<String>(
            value: value,
            child: Text(value),
          );
        },
      ).toList(),
      isExpanded: true,
    ),
  ),
]);

NOTE: I am in need though of the general solution to this problem, not just how to make that Text behave somewhat "like a HTML label" in the tree below. It may need to be triggered to open by maybe a further away button etc.

like image 630
NeuronQ Avatar asked Aug 16 '19 18:08

NeuronQ


People also ask

How do I use DropdownButton in flutter?

DropDownButton: In Flutter, A DropDownButton is a material design button. The DropDownButton is a widget that we can use to select one unique value from a set of values. It lets the user select one value from a number of items. The default value shows the currently selected value.

How do I open dropdown dialog below DropdownButton in flutter?

Option 1: Set DropDown. dart selectedItemOffset to -40 in then DropDownItems will always opens below the DropdownButton .


2 Answers

The other answer is the best way to do this, but as requested by the OP in comments, here are two very "hacky" ways to achieve this, yet without implementing custom widgets.

1. Access DropdownButton widget tree directly using GlobalKey

If we look at the source code of DropdownButton, we can notice that it uses GestureDetector to handle taps. However, it's not a direct descendant of DropdownButton, and we cannot depend on tree structure of other widgets, so the only reasonably stable way to find the detector is to do the search recursively.

One example is worth a thousand explanations:

class DemoDropdown extends StatefulWidget {  
  @override
  InputDropdownState createState() => DemoDropdownState();
}

class DemoDropdownState<T> extends State<DemoDropdown> {
  /// This is the global key, which will be used to traverse [DropdownButton]s widget tree
  GlobalKey _dropdownButtonKey;

  void openDropdown() {
    GestureDetector detector;
    void searchForGestureDetector(BuildContext element) {
      element.visitChildElements((element) {
        if (element.widget != null && element.widget is GestureDetector) {
          detector = element.widget;
          return false;

        } else {
          searchForGestureDetector(element);
        }

        return true;
      });
    }

    searchForGestureDetector(_dropdownButtonKey.currentContext);
    assert(detector != null);

    detector.onTap();
  }

  @override
  Widget build(BuildContext context) {
    final dropdown = DropdownButton<int>(
      key: _dropdownButtonKey,
      items: [
        DropdownMenuItem(value: 1, child: Text('1')),
        DropdownMenuItem(value: 2, child: Text('2')),
        DropdownMenuItem(value: 3, child: Text('3')),
      ],
      onChanged: (int value) {},
    );

    return Column(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        Offstage(child: dropdown),
        FlatButton(onPressed: openDropdown, child: Text('CLICK ME')),
      ],
    );
  }
}

2. Use Actions.invoke

One of the recent features of Flutter is Actions (I'm not sure what it's meant for, I've only noticed it today after flutter upgrade), and DropdownButton uses it for reacting to different... well, actions.

So a little tiny bit less hacky way to trigger the button would be to find the context of Actions widget and invoke the necessary action.

There are two advantages of this approach: firstly, Actions widget is a bit higher in the tree, so traversing that tree wouldn't be as long as with GestureDetector, and secondly, Actions seems to be a more generic mechanism than gesture detection, so it's less likely to disappear from DropdownButton in the future.

// The rest of the code is the same
void openDropdown() {
  _dropdownButtonKey.currentContext.visitChildElements((element) {
    if (element.widget != null && element.widget is Semantics) {
      element.visitChildElements((element) {
        if (element.widget != null && element.widget is Actions) {
          element.visitChildElements((element) {
            Actions.invoke(element, Intent(ActivateAction.key));
            return false;
          });
        }
      });
    }
  });
}
like image 123
mrnateriver Avatar answered Oct 19 '22 21:10

mrnateriver


It's one (of many) designed API limitations...

The easiest approach to accomplish what you want, without modifying the SDK, copy dropdown.dart, and create your own version of it, let's say custom_dropdown.dart, and paste the code there ...

in line 546, rename the class to CustomDropdownButton, and in line 660 and 663 rename _DropdownButtonState to CustomDropdownButtonState, ( we need the state class to be exposed outside the file ).

Now you can do whatever you want with it, although you were interested in the _handleTap(), to open the overlay menu options.

Instead of making _handleTap() public, and refactor the code, add another method like:

(line 726)
void callTap() => _handleTap();

Now, change your code to use your DropdownButton instead of the Flutter's DropdownButton, the key is to "set the key" (Global one) :P

// some stateful widget implementation.

  Map<String, String> _data;
  List<String> _every_options;
  // we need the globalKey to access the State.
  final GlobalKey dropdownKey = GlobalKey();

  @override
  void initState() {
    _every_options = List.generate(10, (i) => "item $i");
    _data = {'every': _every_options.first};
    simulateClick();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Row(children: [
        Padding(
          padding: const EdgeInsets.only(right: 16),
          child: Text('every'),
        ),
        Expanded(
          child: CustomDropdownButton<String>(
            key: dropdownKey,
            value: _data['every'],
            onChanged: (String val) => setState(() => _data['every'] = val),
            items: _every_options
                .map((str) => DropdownMenuItem(
                      value: str,
                      child: Text(str),
                    ))
                .toList(),
            isExpanded: true,
          ),
        ),
      ]),
    );
  }

  void simulateClick() {
    Timer(Duration(seconds: 2), () {
      // here's the "magic" to retrieve the state... not very elegant, but works.
      CustomDropdownButtonState state = dropdownKey.currentState;
      state.callTap();
    });
  }
like image 40
roipeker Avatar answered Oct 19 '22 21:10

roipeker