Search code examples
flutterdropdownselected

Error when selecting a dropdown item in Flutter


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) enter image description here


Solution

  • I think I've figured out your issue. It stems from how you're using FutureBuilder.

    This is what I think is going on:

    1. Your app is runs. It does the call to getFeedCategories()
    2. The future completes, and the list is built, etc with the following items:

      obj 123 ==> FeedCategory(Prueba), obj 345 ==> FeedCategory(Categories 2)

    3. You select the item from the dropdown, setState() is called

      Your _feedCategory is now equal to obj 123 ==> FeedCategory(Prueba)

    4. Widget is rebuilt. It does another call to getFeedCategories()

    5. Future completes, list is built etc with the following items

      obj 567 ==> FeedCategory(Prueba), obj 789 ==> FeedCategory(Categories 2)

    6. The Dropdown tests 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.