icemanbsi / searchable_dropdown

MIT License
107 stars 162 forks source link

How to reset the dropdown list ? #119

Open Suresh-Alangat opened 3 years ago

Suresh-Alangat commented 3 years ago

I have got two objects, one for handling selected item value & the other is a list of items. During the initial screen loading time, both of these objects are null and the dropdown looks disabled . When I bind data to the list and perform selections, it is working fine. Issue comes when I try to reset the drop down when I have performed a save action and need to get all field values cleared. I tried to 1) set null to both objects 2) set null to selected item value alone 3) set -1 to selected item value. Nothing actually did work.

SearchableDropdown.single(
            value: selectedValue,
            items: (itemsList != null && itemsList.isNotEmpty)
                ? itemsList.map((Item item) {
              return DropdownMenuItem(
                  child: Text(item.text),
                  value: item.value);
            }).toList()
                : [],
            hint: "Select Item",
            onChanged: (value) {
              setState(() {
                selectedValue = value;
              });
            },
            isExpanded: true,
          ),

What makes me mad is, during the initial loading time both objects are null , it is working fine and during reset process, it shows the following error,

RangeError (index): Invalid value: Only valid value is 0: 13

JHeisecke commented 3 years ago

I'm having the same problem, is pretty dissapointing that I can't update the value for the SearchableDropdown. It works for me when I add an auxiliary variable _city that holds the id for the value of the SearchableDropdown, in the best case scenario I should be able to set user.userData.city as my value.

  int _city; 
 Consumer<City>(
    builder: (ctx, cityData, child) =>
        SearchableDropdown(
      items: cityData.cities
          .map(
            (city) => DropdownMenuItem(
              value: city.id,
              child: Text(city.name),
            ),
          )
          .toList(),
      value: _city,
      onChanged: !_status
          ? (newValue) => setState(
                () {
                  _city = newValue;
                  user.userData.city = _city;
                },
              )
          : null,
      hint: const Text("Ciudad"),
    ),
  );

That's how I made it work, but the searchableDropdown, searches by value and not child, my values are numbers and the child is the description, there's no point of searching cities by their id (1, 2, 3...) I added searchFn, to search by child data, and got the same error again. RangeError (index): Invalid value: Only valid value is 0: 2

this is my code with the searchFn added, I don't understand how that function broke it

Consumer<City>(
  builder: (ctx, cityData, child) =>
      SearchableDropdown(
    items: cityData.cities
        .map(
          (city) => DropdownMenuItem(
            value: city.id,
            child: Text(city.name),
          ),
        )
        .toList(),
    value: _city,
    onChanged: !_status
        ? (newValue) => setState(
              () {
                _city = newValue;
                user.userData.city = _city;
              },
            )
        : null,
    hint: const Text("Ciudad"),
    searchFn: (String keyword, items) {
      List<int> ret = [];
      if (items != null && keyword.isNotEmpty) {
        keyword.split(" ").forEach((k) {
          int i = 0;
          items.forEach((item) {
            if (k.isNotEmpty &&
                (item.child.data
                    .toString()
                    .toLowerCase()
                    .contains(k.toLowerCase()))) {
              ret.add(i);
            }
            i++;
          });
        });
      }
      if (keyword.isEmpty) {
        ret = Iterable<int>.generate(items.length)
            .toList();
      }
      return (ret);
    },
  ),
);
lcuis commented 3 years ago

Hello @Suresh-Alangat and @JHeisecke ,

Here is some similar code based on the "Single dialog object" example which works fine at least with the search_choices plugin:

SearchChoices.single(
        value: selectedNumber,
        items: numberItemsEmpty ?? [],
        label: numberItemsEmpty == null
            ? ElevatedButton(
                onPressed: () {
                  setState(() {
                    numberItemsEmpty = ExampleNumber.list.map((exNum) {
                      return (DropdownMenuItem(
                          child: Text(exNum.numberString), value: exNum));
                    }).toList();
                  });
                },
                child: Text("Generate items list"))
            : ElevatedButton(
                onPressed: () {
                  setState(() {
                    selectedNumber = null;
                    numberItemsEmpty = null;
                  });
                },
                child: Text("Reset items list")),
        hint: (numberItemsEmpty?.isEmpty ?? true) ? "No items" : "Select Item",
        onChanged: (value) {
          setState(() {
            selectedNumber = value;
          });
        },
        isExpanded: true,
      )

As you can see, there is a buton to generate the list and another one to clear it. I get no error using both of them.

Can you please let me know how I should adapt this example to reproduce your issue?

Should I try to reset the list of items from the dialog? This could maybe explain the error you are getting.

JHeisecke commented 3 years ago

Hi @lcuis thanks for the response. I'm trying to understand your comment but I can't seem to, sorry, I'll try to explain what I'm doing so you can reproduce my error.

My code with minor tweaks

Consumer<City>(
  builder: (ctx, cityData, child) =>
      SearchableDropdown.single(
    items: cityData.cities
        .map(
          (city) => DropdownMenuItem(
            value: city.id,
            child: Text(city.name),
          ),
        )
        .toList(),
    value: user.userData.city,
    onChanged: !_status
        ? (newValue) => setState(
              () {
                user.userData.city= newValue;
              },
            )
        : null,
    hint: const Text("Ciudad"),
    searchFn: (String keyword, items) {
      List<int> ret = [];
      if (items != null && keyword.isNotEmpty) {
        keyword.split(" ").forEach((k) {
          int i = 0;
          items.forEach((item) {
            if (k.isNotEmpty &&
                (item.child.data
                    .toString()
                    .toLowerCase()
                    .contains(k.toLowerCase()))) {
              ret.add(i);
            }
            i++;
          });
        });
      }
      if (keyword.isEmpty) {
        ret = Iterable<int>.generate(items.length)
            .toList();
      }
      return (ret);
    },
    isExpanded: true,
  ),
);

I get my list from a petition I do to my backend and map it to a List of objects, for example: [{id: 0, name: Asuncion}, {id: 1, name: San Lorenzo}, {id:2, name: "Lambare"}] (in my code that would be the value of cityData.cities) like so. For the purpose of this issue, let's assume cityData.cities is never null or empty.

Images for reference:

imagen imagen

user.userData.city holds the id of the city picked its value is changed on the onChanged function. Also user.userData.city is nevel null, it always has a value. user.userData.city is the value I want to send to my backend.

When I pick a city other than "Asuncion" (id 0) and save it. I get the error RangeError (index): Invalid value: Only valid value is 0: X X being the id of the city picked (always other than Asuncion). Another error is, when I pick Asuncion as my city and save it, I can't change it back to another city, when debugging newValue is receiving always value 0 (which is Asuncion) and not 1 or 2 when they're picked.

GIF reproducing error

I already had this working on a DropdownButtonFormField but I need a searchBar because in the future I should have more tan 200 cities in that dropdown. This is how it works on that widget

Consumer<City>(
  builder: (ctx, cityData, child) =>
      DropdownButtonFormField(
    items: cityData.cities
        .map(
          (city) => DropdownMenuItem(
            value: city.id,
            child: Text(city.name),
          ),
        )
        .toList(),
    value: user.userData.city,
    onChanged: !_status
        ? (newValue) => setState(
              () {
                user.userData.city = newValue;
              },
            )
        : null,
    hint: const Text("Ciudad"),
    isExpanded: true,
  ),
);

i hope this helps understanding

stack trace


════════ Exception caught by widgets library ═══════════════════════════════════
The following RangeError was thrown building SearchableDropdown<int>(dirty, dependencies: [_InheritedTheme, _LocalizationsScope-[GlobalKey#f4db9], Directionality], state: _SearchableDropdownState<int>#8dd72):
RangeError (index): Invalid value: Only valid value is 0: 2

The relevant error-causing widget was
SearchableDropdown<int>
lib\…\profile_detail\profile_details.dart:181
When the exception was thrown, this was the stack
#0      List.[] (dart:core-patch/growable_array.dart:177:60)
#1      _SearchableDropdownState.build.<anonymous closure> package:searchable_dropdown/searchable_dropdown.dart:520
#2      List.forEach (dart:core-patch/growable_array.dart:313:8)
#3      _SearchableDropdownState.build
package:searchable_dropdown/searchable_dropdown.dart:517
#4 StatefulElement.build
package:flutter/…/widgets/framework.dart:4744
lcuis commented 3 years ago

Hello @JHeisecke ,

Thanks for your explanations.

Here is an adaptation of your example for a minimalist but complete app:

import 'package:flutter/material.dart';
import 'package:searchable_dropdown/searchable_dropdown.dart';

class City {
  String name;
  int id;
  City(
    this.name,
    this.id,
  );
}

class CityData {
  List<City> cities = [
    City("Arrecife", 0),
    City("Puerto del Carmen", 1),
    City("Tias", 2),
    City("Costa Teguise", 3),
    City("Haria", 4),
  ];
  CityData();
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  CityData cityData = CityData();

  List<DropdownMenuItem<int>> cities;

  MyHomePage({
    Key key,
  }) : super(key: key) {
    cities = cityData.cities
        .map(
          (city) => DropdownMenuItem(
            value: city.id,
            child: Text(city.name),
          ),
        )
        .toList();
  }

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int selectedCityIndex;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            SearchableDropdown.single(
              items: widget.cities,
              value: selectedCityIndex,
              onChanged: (newValue) => setState(
                () {
                  selectedCityIndex = newValue;
                  print("selected city id: ${widget.cities[selectedCityIndex].value}");
                },
              ),
              hint: const Text("Ciudad"),
              searchFn: (String keyword, items) {
                List<int> ret = [];
                if (items != null && keyword.isNotEmpty) {
                  keyword.split(" ").forEach((k) {
                    int i = 0;
                    items.forEach((item) {
                      if (k.isNotEmpty &&
                          (item.child.data
                              .toString()
                              .toLowerCase()
                              .contains(k.toLowerCase()))) {
                        ret.add(i);
                      }
                      i++;
                    });
                  });
                }
                if (keyword.isEmpty) {
                  ret = Iterable<int>.generate(items.length).toList();
                }
                return (ret);
              },
              isExpanded: true,
            ),
          ],
        ),
      ),
    );
  }
}

I made several adaptations which may explain why this is working without errors for me.

Does this example run with errors for you?