i created searchable dropdown widget but i have problem with them. When i click in input area , it render again with list view part but keyboard covering it. I already tried SingleRowChild class it does not work our case .
Actual Behaviour :
Expected Behaviour : When i focus in input area it should visible.
Flutter version 3
import 'dart:ffi';
import 'package:checkout_flutter/lang/app_localizations.dart';
import 'package:checkout_flutter/style/color.dart';
import 'package:checkout_flutter/util.dart';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_svg/flutter_svg.dart';
typedef Validator(String value);
/// Simple dorpdown whith plain text as a dropdown items.
class MNSearchableDropdownField extends StatelessWidget {
final String? initialValue;
final List<String> options;
final InputDecoration? decoration;
final DropdownEditingController<String>? controller;
final void Function(String item)? onChanged;
final void Function(String?)? onSaved;
final Validator? validator;
final bool Function(String item, String str)? filterFn;
final Future<List<String>> Function(String str)? findFn;
final double? dropdownHeight;
final String? labelText;
String? errorText;
MNSearchableDropdownField(
{Key? key,
required this.options,
this.decoration,
this.onSaved,
this.controller,
this.onChanged,
this.validator,
this.errorText,
this.findFn,
this.filterFn,
this.dropdownHeight,
this.labelText,
this.initialValue})
: super(key: key);
@override
Widget build(BuildContext context) {
return DropdownFormField<String>(
key: key,
decoration: decoration,
onSaved: onSaved,
controller: controller,
onChanged: onChanged,
validator: validator,
dropdownHeight: dropdownHeight,
labelText: labelText,
initialValue: initialValue,
displayItemFn: (dynamic str) => Text(
str ?? '',
style: TextStyle(fontSize: 16),
),
findFn: findFn ?? (dynamic str) async => options,
filterFn: filterFn ?? (dynamic item, str) => item.toLowerCase().startsWith(str.toLowerCase()),
dropdownItemFn: (dynamic item, position, focused, selected, onTap) => ListTile(
title: Text(
item,
style: TextStyle(color: Colors.black),
),
tileColor: focused ? Color.fromARGB(20, 0, 0, 0) : Colors.transparent,
onTap: onTap,
),
);
}
}
class DropdownEditingController<T> extends ChangeNotifier {
T? _value;
DropdownEditingController({T? value}) : _value = value;
T? get value => _value;
set value(T? newValue) {
if (_value == newValue) return;
_value = newValue;
notifyListeners();
}
@override
String toString() => '${describeIdentity(this)}($value)';
}
/// Create a dropdown form field
class DropdownFormField<T> extends StatefulWidget {
final bool autoFocus;
/// It will trigger on user search
final bool Function(T item, String str)? filterFn;
/// Check item is selectd
final bool Function(T? item1, T? item2)? selectedFn;
/// Return list of items what need to list for dropdown.
/// The list may be offline, or remote data from server.
final Future<List<T>> Function(String str) findFn;
/// Build dropdown Items, it get called for all dropdown items
/// [item] = [dynamic value] List item to build dropdown Listtile
/// [lasSelectedItem] = [null | dynamic value] last selected item, it gives user chance to highlight selected item
/// [position] = [0,1,2...] Index of the list item
/// [focused] = [true | false] is the item if focused, it gives user chance to highlight focused item
/// [onTap] = [Function] *important! just assign this function to Listtile.onTap = onTap, incase you missed this,
/// the click event if the dropdown item will not work.
///
final ListTile Function(
T item,
int position,
bool focused,
bool selected,
Function() onTap,
) dropdownItemFn;
/// Build widget to display selected item inside Form Field
final Widget Function(T? item) displayItemFn;
final InputDecoration? decoration;
final Color? dropdownColor;
final DropdownEditingController<T>? controller;
final void Function(String item)? onChanged;
final void Function(T?)? onSaved;
final Validator? validator;
/// height of the dropdown overlay, Default: 240
final double? dropdownHeight;
final String? labelText;
/// Style the search box text
final TextStyle? searchTextStyle;
/// Message to disloay if the search dows not match with any item, Default : "No matching found!"
final String emptyText;
/// Give action text if you want handle the empty search.
final String emptyActionText;
/// this functon triggers on click of emptyAction button
final Future<void> Function()? onEmptyActionPressed;
final String? initialValue;
DropdownFormField(
{Key? key,
required this.dropdownItemFn,
required this.displayItemFn,
required this.findFn,
this.filterFn,
this.autoFocus = false,
this.controller,
this.validator,
this.decoration,
this.dropdownColor,
this.onChanged,
this.onSaved,
this.dropdownHeight,
this.searchTextStyle,
this.emptyText = "No matching found!",
this.emptyActionText = 'Create new',
this.onEmptyActionPressed,
this.selectedFn,
this.labelText,
this.initialValue})
: super(key: key);
@override
DropdownFormFieldState createState() => DropdownFormFieldState<T>(key);
}
class DropdownFormFieldState<T> extends State<DropdownFormField> with SingleTickerProviderStateMixin {
final FocusNode _widgetFocusNode = FocusNode();
final FocusNode _searchFocusNode = FocusNode();
final LayerLink _layerLink = LayerLink();
final ValueNotifier<List<T>> _listItemsValueNotifier = ValueNotifier<List<T>>([]);
final TextEditingController _searchTextController = TextEditingController();
final DropdownEditingController<T>? _controller = DropdownEditingController<T>();
final Key? key;
final String? initialValue = "";
Validator? validator;
String? errorText;
int numberOfItems = 4;
bool get _isEmpty => _selectedItem == null;
bool _isFocused = false;
bool _isListEnable = false;
OverlayEntry? _overlayEntry;
OverlayEntry? _overlayBackdropEntry;
List<T>? _options;
int _listItemFocusedPosition = 0;
T? _selectedItem;
Widget? _displayItem;
Timer? _debounce;
String? _lastSearchString;
DropdownEditingController<dynamic>? get _effectiveController => widget.controller ?? _controller;
DropdownFormFieldState(this.key) : super() {}
@override
void initState() {
super.initState();
if (widget.autoFocus) _widgetFocusNode.requestFocus();
_selectedItem = widget.initialValue != null ? widget.initialValue : _effectiveController!.value;
if (_selectedItem == "") _selectedItem = null;
_searchFocusNode.addListener(() {
if (!_searchFocusNode.hasFocus && _overlayEntry != null) {
_removeOverlay();
}
});
}
@override
void dispose() {
super.dispose();
_debounce?.cancel();
_searchTextController.dispose();
}
String? validate(String? value) {
//DISUCSS APPROPRIATE NULL-CHECK?
if (_selectedItem == null) {
errorText = AppLocalizations.current!.requiredFieldMessage;
}
return errorText;
}
@override
Widget build(BuildContext context) {
_displayItem = widget.displayItemFn(_selectedItem);
return Semantics(
label: Util.keyToString(key!),
child: CompositedTransformTarget(
link: this._layerLink,
child: Column(children: [
GestureDetector(
onTap: () {
if (_selectedItem == null)
_selectedItem = widget.initialValue != null ? widget.initialValue : _effectiveController!.value;
//_widgetFocusNode.requestFocus();
setState(() {
_isFocused = true;
_isListEnable = true;
});
_searchFocusNode.requestFocus();
_search("");
},
child: Focus(
autofocus: widget.autoFocus,
focusNode: _widgetFocusNode,
onFocusChange: (focused) {
setState(() {
_isFocused = focused;
});
if (!_isFocused && (_effectiveController!.value == null || _effectiveController!.value != "")) {
setState(() {
errorText = validate(_effectiveController!.value);
});
} else {
setState(() {
errorText = null;
});
}
},
child: FormField(
validator: validate,
onSaved: (str) {
if (widget.onSaved != null) {
widget.onSaved!(_effectiveController!.value);
}
},
builder: (state) {
return Container(
child: InputDecorator(
key: Key('searchable-dropdown-input'),
decoration: InputDecoration(
border: OutlineInputBorder(borderSide: BorderSide(color: Colors.black, width: 2)),
focusedBorder: _isListEnable
? OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
borderRadius:
BorderRadius.only(topLeft: Radius.circular(5), topRight: Radius.circular(5)))
: OutlineInputBorder(
borderSide: BorderSide(
color: errorText == null ? Colors.black : CustomColors.errorRed, width: 2)),
disabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.black, width: 2)),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: errorText == null ? CustomColors.radioGray : CustomColors.errorRed, width: 2),
),
errorBorder:
OutlineInputBorder(borderSide: BorderSide(color: CustomColors.errorRed, width: 2)),
focusedErrorBorder:
OutlineInputBorder(borderSide: BorderSide(color: CustomColors.errorRed, width: 2)),
isDense: true,
labelStyle: TextStyle(
color: _isStateValidate(),
fontSize: 16,
fontWeight: _isFocused ? FontWeight.w700 : FontWeight.w400,
fontFamily: "HankenSans-Regular"),
errorText: errorText,
errorStyle: TextStyle(
fontSize: 12,
height: 1.5,
color: CustomColors.errorRed,
fontWeight: FontWeight.w700,
fontFamily: "HankenSans-Medium"),
suffixIcon: _isListEnable && _isFocused
? SvgPicture.asset('assets/up_arrow.svg',
key: Key('total-expand-icon'),
color: Colors.black,
fit: BoxFit.scaleDown,
package: 'checkout_flutter')
: SvgPicture.asset('assets/down_arrow.svg',
key: Key('total-expan-icon'),
color: Colors.grey,
fit: BoxFit.scaleDown,
package: 'checkout_flutter'),
labelText: widget.labelText,
hintText: widget.labelText,
hintStyle: TextStyle(
color: CustomColors.primaryBlack, fontSize: 16, fontFamily: "HankenSans-Regular"),
),
isEmpty: _isEmpty,
isFocused: _isFocused,
child: this._isListEnable && this._isFocused
? EditableText(
key: Key('searchable-dropdown-edit-text'),
style: TextStyle(color: Colors.black, fontSize: 16, fontFamily: "HankenSans-Medium"),
controller: _searchTextController,
cursorColor: Colors.black,
focusNode: _searchFocusNode,
backgroundCursorColor: Colors.transparent,
onChanged: (str) {
_onTextChanged(str);
},
onSubmitted: (str) {
//_searchTextController.value = TextEditingValue(text: "");
_toggleOverlay();
_setValue();
_removeOverlay();
_widgetFocusNode.nextFocus();
setState(() {
errorText = validate(str);
});
},
onEditingComplete: () {},
)
: _displayItem ?? Container(),
));
},
),
),
),
this._isListEnable && _isFocused ? _createListView() : Container(),
])));
}
Widget _createListView() {
return Material(
child: Container(
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: Colors.black,
width: 2,
),
left: BorderSide(
color: Colors.black,
width: 2,
),
top: BorderSide.none,
bottom: BorderSide(color: Colors.black, width: 2)),
),
child: SizedBox(
height: (45 * numberOfItems).toDouble(),
child: Container(
child: ValueListenableBuilder(
key: Key('searchable-dropdown-listview'),
valueListenable: _listItemsValueNotifier,
builder: (context, List<T> items, child) {
return _options != null && _options!.length > 0
? Padding(
padding: EdgeInsets.fromLTRB(0, 0, 5, 0),
child: Scrollbar(
key: Key('searchable-dropdown-listview-scroll'),
//isAlwaysShown: true,
radius: Radius.circular(50),
child: Padding(
padding: EdgeInsets.fromLTRB(10, 0, 5, 0),
child: ListView.builder(
shrinkWrap: false,
itemCount: _options!.length,
itemBuilder: (context, position) {
return Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(width: 1, color: CustomColors.divider))),
child: TextButton(
clipBehavior: Clip.none,
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
primary: Colors.black,
textStyle: const TextStyle(
fontSize: 14, fontFamily: "HankenSans-Regular")),
onPressed: () async {
_listItemFocusedPosition = position;
_searchTextController.value = TextEditingValue(text: "");
_setValue();
_isListEnable = false;
},
child: Align(
alignment: Alignment.topLeft,
child: Text(
_options![position].toString(),
))));
}))))
: Container(
padding: EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
widget.emptyText,
style: TextStyle(color: Colors.black45),
),
if (widget.onEmptyActionPressed != null)
TextButton(
onPressed: () async {
await widget.onEmptyActionPressed!();
_search(_searchTextController.value.text);
},
child: Text(widget.emptyActionText),
),
],
),
);
}))),
));
}
_removeOverlay() {
if (_overlayEntry != null) {
_overlayBackdropEntry!.remove();
_overlayEntry!.remove();
_overlayEntry = null;
//_searchTextController.value = TextEditingValue.empty;
setState(() {});
}
}
_isStateValidate() {
if (errorText != null) {
return CustomColors.errorRed;
} else if (_isFocused) {
return Colors.black;
} else {
return CustomColors.primaryGray;
}
}
_toggleOverlay() {
_isListEnable = !_isListEnable;
setState(() {});
}
_onTextChanged(String? str) {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 100), () {
if (_lastSearchString != str) {
_lastSearchString = str;
_search(str ?? "");
}
});
errorText = null;
if (validator != null) errorText = validate(str);
}
_search(String str) async {
List<T> items = await widget.findFn(str) as List<T>;
if (str.isNotEmpty && widget.filterFn != null) {
items = items.where((item) => widget.filterFn!(item, str)).toList();
}
if (items.length == 0) {
return;
}
setState(() {
numberOfItems = items.length >= 4 ? 4 : items.length % 4;
});
_options = items;
_listItemsValueNotifier.value = items;
}
_setValue() {
var item = _options![_listItemFocusedPosition];
_selectedItem = item;
_effectiveController!.value = _selectedItem;
if (widget.onChanged != null) {
widget.onChanged!(_selectedItem as String);
}
_searchTextController.value = TextEditingValue(text: _selectedItem.toString());
FocusScope.of(context).unfocus();
setState(() {});
}
}
this worked for me: https://stackoverflow.com/a/73047628/12172300
Just Add
reverse: true
on SingleChildScrollView.child: Center( child: SingleChildScrollView( reverse: true, child: Column(