1

Framework: Flutter

Platform: Web

I have the following code which renders a TextField. A ListView is rendered below the TextField using an Overlay when the TextField has focus. The intent of this widget is to function as a drop down menu. However, the onTap() callback of the ListTile's inside the ListView do nothing when tapped!

import 'package:flutter/material.dart';

class CountriesField extends StatefulWidget {
  @override
  _CountriesFieldState createState() => _CountriesFieldState();
}

class _CountriesFieldState extends State<CountriesField> {
  final FocusNode _focusNode = FocusNode();

  OverlayEntry? _overlayEntry;

  List<String> countries = ['Lebanon', 'Syria', 'India'];

  @override
  void initState() {
    super.initState();
    _focusNode.addListener(() {
      if (_focusNode.hasFocus) {
        _overlayEntry = _createOverlayEntry();
        Overlay.of(context).insert(_overlayEntry!);
      } else {
        _overlayEntry!.remove();
      }
    });
  }

  OverlayEntry _createOverlayEntry() {
    RenderBox? renderBox = context.findRenderObject() as RenderBox?;
    var size = renderBox?.size;
    var offset = renderBox?.localToGlobal(Offset(0, size!.height + 5.0));

    return OverlayEntry(
      builder: (context) => Positioned(
        left: offset?.dx,
        top: offset?.dy,
        width: size?.width,
        child: Material(
          elevation: 4.0,
          child: SizedBox(
            width: 200,
            height: 300,
            child: ListView.builder(
              padding: EdgeInsets.zero,
              itemCount: countries.length,
              itemBuilder: (BuildContext context, index) {
                return ListTile(
                  title: Text(countries[index]),
                  onTap: () {
                    print(countries[index]);
                  },
                );
              },
            ),
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _focusNode.dispose();
    _overlayEntry?.remove();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      focusNode: _focusNode,
      decoration: InputDecoration(labelText: 'Country'),
    );
  }
}

P.S.: I am aware of the many drop down and searchable drop down packages available already, however, I need to make quite a specific drop down search widget for a project I'm doing and need to solve this problem.

Anorakk
  • 71
  • 1
  • 6

2 Answers2

0

To "draw" the drop down list,you need CompositedTransformTarget and CompositedTransformFollower

You also need layerLink to link them together,

So this is after adding them:


import 'package:flutter/material.dart';

class CountriesField extends StatefulWidget {
  @override
  _CountriesFieldState createState() => _CountriesFieldState();
}

class _CountriesFieldState extends State<CountriesField> {
  final FocusNode _focusNode = FocusNode();

  OverlayEntry? _overlayEntry;
  final GlobalKey _widgetKey = GlobalKey();
  final layerLink = LayerLink();
  List<String> countries = ['Lebanon', 'Syria', 'India'];

  @override
  void initState() {
    super.initState();
    _focusNode.addListener(() {
      if (_focusNode.hasFocus) {
        _overlayEntry = _createOverlayEntry();
        Overlay.of(context).insert(_overlayEntry!);
      } else {
        _overlayEntry!.remove();
      }
    });
  }

  OverlayEntry _createOverlayEntry() {
    RenderBox? renderBox = context.findRenderObject() as RenderBox?;
    var size = renderBox?.size;
    var offset = renderBox?.localToGlobal(Offset(0, size!.height + 5.0));

    return OverlayEntry(
      builder: (context) => CompositedTransformFollower(
        link: layerLink,
        showWhenUnlinked: false,
        followerAnchor:  Alignment.bottomCenter,
        targetAnchor: Alignment.bottomCenter,
        offset: offset ?? Offset(0, 0),
        child: Material(
          elevation: 4.0,
          child: Container(
            width: 200,
            height: 300,
            alignment: Alignment.center,
            child:  ListView.builder(
              padding: EdgeInsets.zero,
              itemCount: countries.length,
              itemBuilder: (BuildContext context, index) {
                return ListTile(
                  title: Text(countries[index]),
                  onTap: () {
                    print(countries[index]);
                  },
                );
              },
            ),
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _focusNode.dispose();
    _overlayEntry?.remove();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: CompositedTransformTarget(
          link: layerLink,
          child: TextFormField(
            key: _widgetKey,
            focusNode: _focusNode,
            decoration: InputDecoration(labelText: 'Country'),
          ),
        ));
  }
}

laila nabil
  • 135
  • 8
0

I had the same issue and it was because the focusNode was removing the overlay too early.

You can wrap the call to overlayEntry.remove() in a WidgetsBinding.instance.addPostFrameCallback

  @override
  void initState() {
    super.initState();
    _focusNode.addListener(() {
      if (_focusNode.hasFocus) {
        _overlayEntry = _createOverlayEntry();
        Overlay.of(context).insert(_overlayEntry!);
      } else {
        WidgetsBinding.instance.addPostFrameCallback((_) {
          // Maybe also check if overlayEntry.mounted is true.
          _overlayEntry?.remove();
        });
      }
    });
  }
Gpack
  • 1,878
  • 3
  • 18
  • 45