31
votes

I am trying to create a dropdown button in Flutter. I am getting a List from my database then I pass the list to my dropdownButton everything works the data is shown as intended but when I choose an element from it I get this error:

There should be exactly one item with [DropdownButton]'s value: Instance of 'Tag'. 
Either zero or 2 or more [DropdownMenuItem]s were detected with the same value
'package:flutter/src/material/dropdown.dart':
Failed assertion: line 805 pos 15: 'items == null || items.isEmpty || value == null ||
          items.where((DropdownMenuItem<T> item) {
            return item.value == value;
          }).length == 1'

I tried setting DropdownButton value to null it works but then I can't see the chosen element.

Here is my code:

FutureBuilder<List<Tag>>(
    future: _tagDatabaseHelper.getTagList(),
    builder: (BuildContext context, AsyncSnapshot<List<Tag>> snapshot) {
      if (!snapshot.hasData) {
        return Center(
          child: CircularProgressIndicator(),
        );
      }
      return ListView(
        children: <Widget>[
          SizedBox(
            height: MediaQuery.of(context).size.height * 0.2,
          ),
          Container(
            margin: EdgeInsets.symmetric(
                horizontal: MediaQuery.of(context).size.width * 0.07),
            child: Theme(
              data: ThemeData(canvasColor: Color(0xFF525A71)),
              child: DropdownButton<Tag>(
                value: _selectedTag,
                isExpanded: true,
                icon: Icon(
                  Icons.arrow_drop_down,
                  size: 24,
                ),
                hint: Text(
                  "Select tags",
                  style: TextStyle(color: Color(0xFF9F9F9F)),
                ),
                onChanged: (value) {
                  setState(() {
                    _selectedTag = value;
                  });
                },
                items: snapshot.data.map((Tag tag) {
                  return DropdownMenuItem<Tag>(
                    value: tag,
                    child: Text(
                      tag.tagTitle,
                      style: TextStyle(color: Colors.white),
                    ),
                  );
                }).toList(),
                value: _selectedTag,
              ),
            ),
          ),

I used futureBuilder to get my List from database.

12
Have you tried hardcoding a value there? String value = "some val"; before your FutureBuilder and assigning that to _value = value; - van
@van Yes same problem. - Abdelbaki Boukerche

12 Answers

52
votes

Well, since no problem has an exact same solution. I was facing the same issue with my code. Here is How I fixed this.

CODE of my DropdownButton:

DropdownButton(
   items: _salutations
         .map((String item) =>
             DropdownMenuItem<String>(child: Text(item), value: item))
         .toList(),
    onChanged: (String value) {
       setState(() {
         print("previous ${this._salutation}");
         print("selected $value");
         this._salutation = value;
            });
          },
     value: _salutation,
),

The Error

In the code snippet below, I am setting the state for a selection value, which is of type String. Now problem with my code was the default initialization of this selection value. Initially, I was initializing the variable _salutation as:

String _salutation = ""; //Notice the empty String.

This was a mistake!

Initial selection should not be null or empty as the error message correctly mentioned.

'items == null || items.isEmpty || value == null ||

And hence the crash:

crash_message

Solution
Initialize the value object with some default value. Please note that the value should be the one of the values contained by your collection. If it is not, then expect a crash.

  String _salutation = "Mr."; //This is the selection value. It is also present in my array.
  final _salutations = ["Mr.", "Mrs.", "Master", "Mistress"];//This is the array for dropdown
16
votes

Might also get this error if trying to set value of dropdown with a class instance;

  var tag1 = Tag();
  var tag2 = Tag();
  print(tag1 == tag2); // prints false, dropwdown computes that value is not present among dropdown options

To solve this override operator ==:

class Tag{
 String name = "tag";

  @override
  bool operator ==(Object other) => other is Tag && other.name == name;

  @override
  int get hashCode => name.hashCode;
}

or use https://pub.dev/packages/equatable lib

class Tag extends Equatable{
 String name = "tag";

  @override
  List<Object> get props => [name];
}
6
votes

I had the same problem. The solution is simple: you have to be sure that the String that is your default dropdownvalue is contained in the list that you want to use in your dropdownmenu. If you wanted to, let’s say, use a list from an api, you should be sure to know at least one value of that list, so that you could assign it to the variable that is your default dropdownvalue.

Here I want display a list that I obtain from an api. In order to not obtain the error, I set my defaultdropdownvalue with the name ‘Encajes’ that is one of the existing categories that my list contains.

String dropdownValue = "Encajes";

    items: categoriesString
    .map<DropdownMenuItem<String>>((String value) {
  return DropdownMenuItem<String>(
    value: value,
    child: Text(value),
  );
}).toList(),
3
votes

Code of my dropdown

child: DropdownButton(
      items: _currencies.map((String value) {
        return DropdownMenuItem<String>(
          child: Text(value),
          value: value,
        );
      }).toList(),
      value: 'Rupees',
      onChanged: (String newValueSelected) {
        // Your code to execute, when a menu item is selected from 
        dropdown
      },
))
var _currencies = ['Rupee','Dollar','Pound'];

I faced same error because the value in the dropdown code block is not matching with any of the fields in _currencies

1
votes

So I found a solution.

I created empty List to hold my Tag objects.

List<Tag> _tagList = [];

Then, in my initState i assigned the list i get from database to the previous List

 @override
void initState() {
super.initState();
_tagDatabaseHelper.getTagList().then((foo) {
  setState(() {
    _tagList = foo;
  });
});

}

Finally My DropdownButton code :

DropdownButton<Tag>(
            isExpanded: true,
            icon: Icon(
              Icons.arrow_drop_down,
              size: 24,
            ),
            hint: Text(
              "Select tags",
              style: TextStyle(color: Color(0xFF9F9F9F)),
            ),
            items: _tagList.map((foo) {
              return DropdownMenuItem(
                value: foo,
                child: Text(foo.tagTitle),
              );
            }).toList(),
            onChanged: (value) {
              setState(() {
                _selectedTag = value;
              });
            },
            value: _selectedTag,
          ),
1
votes

I have had the same issue and surprisingly, there were duplicates in my list of items which were being fetched from a remote DB.

Each time I fetched the data from the server (when a new app user logged in), the data had no duplicates but the same data was being added to the list multiple times because I was logging in multiple users on the same device. Maybe your bug is something similar.

So, make sure you remove any duplicates in the snapshot.data before setting them as items of the DropDownButton.

0
votes

just make the tag class extend from Equatable and pass the attributes to the props.. this did the trick for me.

class Tag extends Equatable{
  String id;
  String name;

  Tag(this.id, this.name);

  @override
  List<Object> get props => [id,name];

}
0
votes

you can avoid the null value using a ternary operator:

  Container(
             child:
              new DropdownButton<String>(
              value: dropdownValue ?? "1",
              icon: const Icon(Icons.arrow_downward),
              iconSize: 24,
              elevation: 16,
              style: const TextStyle(color: Colors.black, fontSize: 18),
              underline: Container(height: 2, color: Colors.white24, ),
              items: <String>['1', '2', '3', '5'].map((String value) {
              return new DropdownMenuItem<String>(
              value: value,
              child: new Text(value),
              );}).toList(),
              onChanged: (value) {
                  setState(() { dropdownValue=value;});
              },
          )),
0
votes

I used a trick. The selected item make as first index item in the list .So when changing item at every time remove the item from list and reinsert the item as first item in the list . Please refer the below code. Here iam using Object as the drop down item and the widget i make it as extracted function. and also before calling the dropDownButton function make

//items list like below

 List<LeaveType> items = [
 (id=1,name="Sick"),
 (id=2,name="Paid")
 ]

selectedLeave = null;

Row leaveTypeDropDown(StateSetter setCustomState, List<LeaveType> items) {
    if(selectedLeave != null){
      items.remove(selectedLeave);
      items.insert(0, selectedLeave);
    }
    return Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children : [
              text("Select Leave Type",textSize: 15),
              Padding(padding: const EdgeInsets.all(5)),
              Expanded(
                child: Container(
                  padding: const EdgeInsets.only(left: 10.0, right: 10.0),
                  decoration: BoxDecoration(
                    border: Border.all(color: Colors.black,width: 1),
                    borderRadius: const BorderRadius.all(Radius.circular(10.0)),
                  ),
                  child: DropdownButtonHideUnderline(
                    child: DropdownButton<LeaveType>(
                      isExpanded: true,
                      //initial value 
                      value: selectedLeave != null ? items[0] : null,
                      icon: const Icon(Icons.arrow_downward),
                      iconSize: 24,
                      elevation: 16,
                      hint: text("Leave Type"),
                      style: const TextStyle(
                          color: Colors.black
                      ),
                      onChanged: (LeaveType  value) {
                        setCustomState(() {
                          selectedLeave = value;
                          items.remove(selectedLeave);
                          items.insert(0, selectedLeave);
                        });
                      },
                      items: items
                          .map((leave) {
                        return  new DropdownMenuItem<LeaveType>(
                          value: leave,
                          child: text(leave.name),
                        );
                      }).toList(),
                    ),
                  ),
                ),
              ),
            ]
        );
  }
0
votes

I changed as below and it got solved:

Initial Code:

List<GamesModel> users = <GamesModel>[
  new GamesModel(1,"Option1"),
  new GamesModel(2,"Option2"),

];
return users;

Changed Code:

List<GamesModel> users = <GamesModel>[
      const GamesModel(1,"Option1"),
      const GamesModel(2,"Option2"),
];
return users;

If anybody want i can put the whole code

0
votes
          DropdownButton<String>(
            iconEnabledColor: Colors.cyan.withOpacity(.6),
            isExpanded: true,
            itemHeight: 50,
            iconSize: 30,
            hint: Text("Choose Province"),
            items: _provinces
                .map((e) => DropdownMenuItem(
              child: Text(e),
              value: e,
            ))
                .toList(),
            value: _Province,
            onChanged: (String? value) async{
              final respnose=await FirebaseFirestore.instance.collection('city').where('provinceName',isEqualTo: value).get();
              _city=[];
              for(var item in respnose.docs){
                print(item.data());
                _city.add(item.data()['name']);
              }
              
              print(_Province);
              setState(() {
                _city=_city;
                _Province = value;
              });
            },
          ),
          SizedBox(height: 20,),

          DropdownButton<String>(
            iconEnabledColor: Colors.cyan.withOpacity(.6),
            isExpanded: true,
            itemHeight: 50,
            iconSize: 30,
            hint: Text("Choose City"),
            items:_city
                .map((e) => DropdownMenuItem(
              child: Text(e),
              value: e,
            ))
                .toList(),
            value: _City,
            onChanged: (String? value) async{
              setState(() {
                _town=[];
                _Town=null;
              });
              print(_town);
              final respnose=await FirebaseFirestore.instance.collection('town').where('cityName',isEqualTo: value).get();
              print(respnose.docs);


              for(var item in respnose.docs){
                print(item.data());
                _town.add(item.data()['name']);
              }
              print(_town);
              print(_City);
              setState(() {

                _City = value;
                _town=_town;
              });
            },
          ),
          SizedBox(height: 20,),

if(true) DropdownButton( iconEnabledColor: Colors.cyan.withOpacity(.6), isExpanded: true, itemHeight: 50, iconSize: 30, hint: Text("Choose Town"), items:_town .map((e) => DropdownMenuItem( child: Text(e), value: e, ) ) .toList(), value: _Town, onChanged: (String? value)async { print(_Town); setState(() { _Town = value; });

0
votes

This error also occurs if you forget to give dropdown menu items a value. ==== WORKS ====

<String>['A', 'B', 'C'].map<DropdownMenuItem<String>>((vehicle) {
        print("vehicle is $vehicle");
        print("vehicle is equal ${vehicle == x.value}");
        return DropdownMenuItem<String>(
          value: vehicle,
          child: Text(
            // vehicle.vehicleInfo!.vehicleType!,
            vehicle,
            style: TextStyle(
              color: Colors.grey[600],
            ),
          ),
        );
      }).toList(),

==== DOESNT WORK ====

<String>['A', 'B', 'C'].map<DropdownMenuItem<String>>((vehicle) {
        return DropdownMenuItem<String>(
          child: Text(
            vehicle,
            style: TextStyle(
              color: Colors.grey[600],
            ),
          ),
        );
      }).toList(),