skip to Main Content

I want to create a google type search bar.
Every time user types in something – app will make a API request to backend and show results in a dropdown below the search bar just like google does.
But I have other content under my search bar which gets pushed down everytime dropdown opens up.
If I use Positioned() widget then the dropdown list only occupied the predefined space provided in parent stack widget.

Code I tried:

import 'package:flutter/material.dart';

class X1 extends StatefulWidget {
  const X1({super.key});

  @override
  State<X1> createState() => _X1State();
}

class _X1State extends State<X1> {
  bool visible = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Column(
      children: [
        Stack(
          children: [
            TextButton(
              onPressed: () {
                setState(() {
                  visible = !visible;
                });
              },
              child: Text('Button'),
            ),
            Positioned.fromRelativeRect(
              rect: RelativeRect.fromLTRB(0, 20, 0, 0),
              child: Visibility(
                child: Container(width: 50, height: 50, color: Colors.pink),
                visible: visible,
              ),
            ),
          ],
        ),
        Container(width: 50, height: 50, color: Colors.green),
      ],
    ));
  }
}

OnClick - dropdown only occupies same height as button. I would like it to overlay on green container below
Without click

2

Answers


  1. Chosen as BEST ANSWER
    • Ajax search dropdown that is using flutters inbuilt Autocomplete Widget
    • Dropdown wont push content down as it opens up.
    • Very usefull in forms/popups where other fields exists along with search box.
    • We pass in searchUrl which is used to search the typed value in db by making GET request.
    class AjaxSearchDropdownFloating extends StatefulWidget {
      final double width;
      final double height;
      final String searchUrl;
      final Map<String, String>? searchUrlParams;
      final Function(Map<String, dynamic>) onSelected;
      final String? initialValue;
      final String label;
    
      // search url response is going to be a list of dict. dict[displayKey] will be shown in dropdown
      final String displayKey;
    
      const AjaxSearchDropdownFloating(
          {super.key,
          required this.width,
          this.height = 300,
          required this.searchUrl, // basic url without keyword search. eg: /xyz/recomm/
          this.searchUrlParams,
          required this.onSelected,
          required this.displayKey,
          this.initialValue,
          this.label = 'Search'});
    
      @override
      State<AjaxSearchDropdownFloating> createState() => _AjaxSearchDropdownFloatingState();
    }
    
    class _AjaxSearchDropdownFloatingState extends State<AjaxSearchDropdownFloating> {
      @override
      Widget build(BuildContext context) {
        final double mediumFont = 20;
        return SizedBox(
          width: widget.width,
          child: Autocomplete<Map<String, dynamic>>(
            initialValue: TextEditingValue(text: widget.initialValue ?? ''),
            fieldViewBuilder: (
              BuildContext context,
              TextEditingController textEditingController,
              FocusNode focusNode,
              VoidCallback onFieldSubmitted,
            ) {
              return TextField(
                controller: textEditingController,
                focusNode: focusNode,
                decoration: InputDecoration(
                  label: Text(
                    widget.label,
                    style: const TextStyle(color: AppColors.appGrey),
                  ),
                ),
              );
            },
            displayStringForOption: (Map<String, dynamic> value) {
              return value[widget.displayKey];
            },
    
            //Listview widget on how the dropdown will look like
            optionsViewBuilder: (
              BuildContext context,
              AutocompleteOnSelected<Map<String, dynamic>> onSelected,
              Iterable<Map<String, dynamic>> options,
            ) {
              return Align(
                alignment: Alignment.topLeft,
                child: Material(
                  elevation: 4,
                  child: SizedBox(
                    width: widget.width,
                    height: widget.height, // Set the desired width
                    child: ListView.separated(
                      itemCount: options.length,
                      itemBuilder: (context, index) {
                        return Container(
                          height: mediumFont * 3,
                          decoration: const BoxDecoration(
                            borderRadius: BorderRadius.zero, // Set the border radius
                          ),
                          child: TextButton(
                            style: TextButton.styleFrom(
                              shape: const BeveledRectangleBorder(
                                borderRadius: BorderRadius.zero,
                              ),
                            ),
                            onPressed: () {
                              onSelected(options.elementAt(index));
                            },
                            child: Align(
                              alignment: Alignment.centerLeft,
                              child: Text(
                                options.elementAt(index)[widget.displayKey],
                              ),
                            ),
                          ),
                        );
                      },
                      separatorBuilder: (context, index) {
                        return const Divider();
                      },
                    ),
                  ),
                ),
              );
            },
            // Return options Map<String,dynamic> to show in dropdown list
            optionsBuilder: (TextEditingValue textEditingValue) async {
              List<Map<String, dynamic>> searchList = []; // Map<String, dynamic>
    
              // TODO write your custom get request here
              http.Response? resp = await BaseApi().getHttp(
                api: widget.searchUrl,
                authenticatedReq: true,
                queryParams: {'search': textEditingValue.text, ...?widget.searchUrlParams},
              );
    
              if (resp.statusCode == 200) {
                final respBody = jsonDecode(resp.body); //list
    
                // if resp is paginated then get results key
                if (respBody is Map && respBody.containsKey('results')) {
                  searchList = respBody['results'];
                } else if (respBody is List<dynamic>) {
                  // convert list<dynamic> to list<map<dynamic,dynamic>>
                  for (Map<String, dynamic> dict in respBody) {
                    searchList.add(dict);
                  }
                }
              }
    
              return searchList;
            },
            onSelected: (Map<String, dynamic> selection) {
              widget.onSelected(selection);
            },
          ),
        );
      }
    }
    

  2. Try the Autocomplete widget. The following code should give you something like this

    enter image description here

    The data class

    class Airline {
      final String name;
      final String country;
      final String founded;
      Airline({
        required this.name,
        required this.country,
        required this.founded,
      });
    
      factory Airline.fromMap(Map<String, dynamic> map) {
        return Airline(
          name: map['name'] as String,
          country: map['country'] as String,
          founded: map['founded'] as String,
        );
      }
    }
    

    The api call function

     Future<List<Airline>> apiSearch(String query) async {
        String baseUrl = 'https://freetestapi.com/api/v1/airlines?search=';
        final response = await http.get(Uri.parse(baseUrl + query));
        final List<dynamic> list = jsonDecode(response.body);
        return list.map((e) => Airline.fromMap(e)).toList();
      }
    

    The Autocomplete widget. A debouncing method before calling the api here, might be ideal to avoid calling on each keystroke.

    Autocomplete<String>(
      optionsBuilder: (textEditingValue) async {
        if (textEditingValue.text == '') {
          return const Iterable.empty();
        }
        // Important: This runs on each keystroke, maybe debounce
        final result = await apiSearch(textEditingValue.text);
        return result.map((e) => e.name);
      },
      onSelected: (selection) {
        debugPrint('You just selected $selection');
      },
    )
    

    For more examples and how the results are customizable, see this answer

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search