I have a problem with a Flutter dropdown. When I select one of the items it throws an error:
Another exception was thrown: 'package:flutter/src/material/dropdown.dart': Failed assertion: line 481 pos 15: 'value == null || items.where((DropdownMenuItem item) => item.value == value).length == 1': is not true.
I was searching and people tell that this error is produced because the selected element doesn't belong to the original list but after some debug I can see it does. I can't find the source of this error, so I would appreciate any help.
Here is my code
FeedCategory model
import 'package:meta/meta.dart';
class FeedCategory {
static final dbId = "id";
static final dbName = "name";
int id;
String name;
FeedCategory({this.id, @required this.name});
FeedCategory.fromMap(Map<String, dynamic> map)
: this(
id: map[dbId],
name: map[dbName],
);
Map<String, dynamic> toMap() {
return {
dbId: id,
dbName: name,
};
}
@override
String toString() {
return 'FeedCategory{id: $id, name: $name}';
}
}
Widget
import 'package:app2date/repository/repository.dart';
import 'package:app2date/model/FeedSource.dart';
import 'package:app2date/model/FeedCategory.dart';
import 'package:app2date/util/ui.dart';
import 'package:flutter/material.dart';
class ManageFeedSource extends StatefulWidget {
ManageFeedSource({Key key, this.feedSource}) : super(key: key);
final FeedSource feedSource;
@override
_ManageFeedSource createState() => new _ManageFeedSource();
}
class _ManageFeedSource extends State<ManageFeedSource> {
FeedCategory _feedCategory;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('New Feed'),
),
body: new FutureBuilder(
future: Repository.get().getFeedCategories(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
List<FeedCategory> categoriesList = snapshot.data;
if (categoriesList != null) {
return new DropdownButton<FeedCategory>(
hint: Text('Choose category...'),
value: _feedCategory,
items: categoriesList.map((FeedCategory category) {
return DropdownMenuItem<FeedCategory>(
value: category,
child: Text(category.name),
);
}).toList(),
onChanged: (FeedCategory category) {
print('Selected: $category');
setState(() {
_feedCategory = category;
});
},
);
} else {
return Container(
decoration: new BoxDecoration(color: Colors.white),
);
}
},
),
);
}
@override
void initState() {
super.initState();
}
}
Repository getFeedCategories method
Future<List<FeedCategory>> getFeedCategories() async {
return await database.getFeedCategories();
}
Database getFeedCategories method
Future<List<FeedCategory>> getFeedCategories() async {
var dbClient = await db;
var query = "SELECT * FROM $feedCategoryTableName;";
var result = await dbClient.rawQuery(query);
List<FeedCategory> feedCategories = [];
for (Map<String, dynamic> item in result) {
feedCategories.add(new FeedCategory.fromMap(item));
}
return feedCategories;
}
The categoriesList content and the selected category (debugger)
I think I've figured out your issue. It stems from how you're using FutureBuilder.
This is what I think is going on:
The future completes, and the list is built, etc with the following items:
obj 123 ==> FeedCategory(Prueba)
, obj 345 ==> FeedCategory(Categories 2)
You select the item from the dropdown, setState() is called
Your _feedCategory is now equal to obj 123 ==> FeedCategory(Prueba)
Widget is rebuilt. It does another call to getFeedCategories()
Future completes, list is built etc with the following items
obj 567 ==> FeedCategory(Prueba)
, obj 789 ==> FeedCategory(Categories 2)
items.where((DropdownMenuItem item) => item.value == value).length == 1
, but length == 0 because obj 123 ==> FeedCategory(Prueba)
is not found.There are a couple of solutions to your problem. One would be to add an equals operator to your FeedCategory
class that compares the category and/or id.
Another would be to separate the Future used in the futurebuilder from the part that changes - you could do this either by keeping the future as a member variable (maybe instantiate it in initState?), or by making the inner part of the builder into it's own Stateful widget (in which case you could probably make ManageFeedSource a StatelessWidget). I'd recommend the last option.
Let me know if that doesn't solve your issue, but I'm pretty sure that's the reason it's doing what it's doing.