flutterdartcross-platformhybrid-mobile-app

Keyboard covering searchable dropdown widget


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 :

enter image description here

Expected Behaviour : When i focus in input area it should visible.

Flutter version 3

enter image description here

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(() {});
  }


}


Solution

  • this worked for me: https://stackoverflow.com/a/73047628/12172300

    Just Add reverse: true on SingleChildScrollView.

    child: Center(
          child: SingleChildScrollView(
            reverse: true,
            child: Column(