I am trying to build a searchable dropdown which will load value from service on every button click. So, for that I have encapsulated DropDownButton and TextField in Stack Widget.
On keypress we get response from api, so far so good. But after getting data from api dropdown was not opening. After digging a bit I came to know it was not opening because we need to manually tap it to open, but since its in stack and second children is TextField I can't tap it.
But opening DropDownButton button programmatically is not possible. So I tried second solution from https://stackoverflow.com/a/59499191/10423593 but it didn't work.
Below is my code without solution from stackoverflow.
import 'package:flutter/material.dart';
import 'package:giphy/services/gifs_service.dart';
import 'package:giphy/shared/autocomplete.dart';
class TestDropDown extends StatefulWidget {
// const TestDropDown({Key? key}) : super(key: key);
@override
_TestDropDownState createState() => _TestDropDownState();
}
class _TestDropDownState extends State<TestDropDown> {
final GifyService _service = GifyService();
final TextEditingController _gifSearchController = TextEditingController();
List<SearchData> _dropDownResult = <SearchData>[];
GlobalKey key = GlobalKey();
// T? _findChildWidgetFromKey<T extends Widget>(
// BuildContext? context, T childWidget) {
// T? detector;
// context!.visitChildElements((element) {
// if (element.widget == childWidget) {
// detector = element.widget as T;
// }
// });
// return detector;
// }
Widget _buildDropDown(List<SearchData> searchData) => DropdownButton<String>(
isExpanded: true,
key: key,
onChanged: (String? value) {},
items: searchData
.map(
(e) => DropdownMenuItem<String>(
child: Text(e.name ?? ''),
value: e.name ?? '',
),
)
.toList(),
);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
_buildDropDown(_dropDownResult),
Container(child: () {
if (_dropDownResult.length > 0) {
_buildDropDown(_dropDownResult);
}
}()),
TextField(
controller: _gifSearchController,
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(width: 0.5),
borderRadius: BorderRadius.circular(21),
),
),
onChanged: (String value) async {
AutoComplete result = await _service.getSearchKeywords(value);
setState(() {
_dropDownResult = result.data;
});
},
),
],
),
);
}
}
After trying some packages. I wasn't able to do flutter autocomplete based on api calls.
So decided to try some other approach and used Overlay in flutter
import 'dart:async';
import 'package:flutter/material.dart';
class TestDropDown extends StatefulWidget {
@override
_TestDropDownState createState() => _TestDropDownState();
}
class _TestDropDownState extends State<TestDropDown> {
late OverlayEntry _overlayEntry;
Timer? _debounce;
_showOverlay(BuildContext context) {
OverlayState? state = Overlay.of(context);
final RenderBox? box = key.currentContext!.findRenderObject() as RenderBox;
Size size = box!.size;
Offset position = box.localToGlobal(Offset.zero);
_overlayEntry = OverlayEntry(
builder: (context) => Positioned(
top: size.height + position.dy,
left: position.dx,
child: Card(
child: Container(
height: 200,
width: size.width,
decoration: BoxDecoration(
border: Border.all(),
),
child: Column(
children: [
Container(
width: size.width,
child: IconButton(
onPressed: () {
if (_overlayEntry.mounted) {
_overlayEntry.remove();
}
},
icon: Icon(Icons.close),
alignment: Alignment.topRight,
),
),
Expanded(
child: ListView(
children: []..addAll(List.generate(
200, (index) => Text(index.toString()))),
),
),
],
)),
),
),
);
state!.insert(_overlayEntry);
}
final key = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(50),
child: Column(
children: [
TextField(
key: key,
onChanged: (String searchText) {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 500), () {
print(searchText);
});
},
),
ElevatedButton(
onPressed: () {
_showOverlay(context);
},
child: Text('Press Me'),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
if (_overlayEntry.mounted) {
_overlayEntry.remove();
}
},
),
);
}
}