Flutter GetBuilder Dependent DropDownButton - 即使值已被重置,也应该只有一项具有 [DropdownButton] 的值

Posted

技术标签:

【中文标题】Flutter GetBuilder Dependent DropDownButton - 即使值已被重置,也应该只有一项具有 [DropdownButton] 的值【英文标题】:Flutter GetBuilder Dependent DropDownButton - There should be exactly one item with [DropdownButton]'s value even when value has been reset 【发布时间】:2021-12-03 04:05:08 【问题描述】:

我有两个 DropDownButtons,一个显示类别(水果和蔬菜),第二个根据所选类别显示产品(Apple、letucce 等),所以它取决于第一个。

当我选择类别时,产品列表会更新以显示相应的项目。如果我然后选择一个产品,它会被选中,但如果我再次更改类别There should be exactly one item with [DropdownButton]'s value: (previous product value) 错误会显示在产品下拉列表中。

我正在尝试使用 Getx GetBuilder 小部件仅在必要时更新屏幕。

这是我的项目中复制错误的简化代码:

Main

@override
Widget build(BuildContext context) 
  return GetMaterialApp(
    initialRoute: 'form',
    initialBinding: GeneralBinding(),
    routes: 'form' : (context) => FormScreen(),
  );

Bindings

class GeneralBinding extends Bindings 
  @override
  void dependencies() 
    Get.lazyPut<FormController>(() => FormController(), fenix: true);
  

Controller

class FormController extends GetxController 
  final DataAsset dataAsset = DataAsset();

  List<Category> categoriesList = <Category>[];
  List<Product> productsList = <Product>[];

  @override
  void onInit() 
    super.onInit();
    updateCategories();
  

  Future<void> updateCategories() async 
    List<Category> newData = await fillCategories();
    categoriesList.assignAll(newData);
    update();
  
  Future<void> updateProducts(int category) async 
    List<Product> newData = await fillProducts(category);
    newData = newData.where((e) => e.category == category).toList();
    productsList.assignAll(newData);
    update();
  

  Future<List<Category>> fillCategories() async 
    return dataAsset.categories;
    //Other case, bring from Database, thats why I need it like Future
  

  Future<List<Product>> fillProducts(int category) async 
    return dataAsset.products.where((element) => element.category == category ).toList();
    //Other case, bring from Database, thats why I need it like Future
  

Models

class Category 
    Category(required this.id, required this.desc);
    int id;
    String desc;

    factory Category.fromMap(Map<String, dynamic> json) => Category(
      id: json["id"],
      desc: json["desc"],
    );

class Product 
    Product(required this.category,required this.id,required this.desc);
    int category;
    int id;
    String desc;

    factory Product.fromMap(Map<String, dynamic> json) => Product(
      category: json["category"],
      id: json["id"],
      desc: json["desc"],
    );


class DataAsset 
  List<Map<String, dynamic>> jsonCategories = ['id': 1, 'desc': 'Fruits', 'id': 2, 'desc': 'Vegetables'];
  List<Map<String, dynamic>> jsonProducts = ['category': 1, 'id': 1, 'desc': 'Apple', 'category': 1, 'id': 2, 'desc': 'Grape', 'category': 1, 'id': 3, 'desc': 'Orange', 'category': 2, 'id': 4, 'desc': 'Lettuce', 'category': 2, 'id': 5, 'desc': 'Broccoli'];

  List<Category> get categories 
    return List<Category>.from(jsonCategories.map((e) => Category.fromMap(e)));
  
  List<Product> get products 
    return List<Product>.from(jsonProducts.map((e) => Product.fromMap(e)));
  

Screen

class FormScreen extends StatefulWidget 
  const FormScreen(Key? key) : super(key: key);

  @override
  _FormScreenState createState() => _FormScreenState();


class _FormScreenState extends State<FormScreen> 
  final _formKey = GlobalKey<FormState>();
  int? category;
  int? product;

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(),
      body: SingleChildScrollView(
        child: Form(key: _formKey,
          child: Column(
            children: [
              buildSelectCategory(),
              buildSelectProduct(),
              buildSubmit()
            ],
          ),
        ),
      ),
    );
  

  Widget buildSelectCategory() 
    return GetBuilder<FormController>(builder: (_formController) 
      print('Rebuilding categories');
      List<Category> categories = _formController.categoriesList;

      return DropdownButtonFormField<int>(
        value: (category == null)? null : category,
        isExpanded: true,
        decoration: const InputDecoration(labelText: 'Category'),
        items: categories.map((option) 
          return DropdownMenuItem(
            value: (option.id),
            child: (Text(option.desc)),
          );
        ).toList(),
        onChanged: (value) 
          category = value;
          product = null; //reset value of product variable
          _formController.updateProducts(category!);
        ,
        validator: (value) => (value == null)? 'Mandatory field' : null,
      );
    );
  

  Widget buildSelectProduct() 
    return GetBuilder<FormController>(builder: (_formController) 
      print('Rebuilding products');
      List<Product> products = _formController.productsList;

      return DropdownButtonFormField<int>(
        value: (product == null)? null : product,
        isExpanded: true,
        decoration: const InputDecoration(labelText: 'Product'),
        items: products.map((option) 
          return DropdownMenuItem(
            value: (option.id),
            child: Text(option.desc),
          );
        ).toList(),
        onChanged: (value) async 
          product = value;
        ,
        validator: (value) => (value == null)? 'Mandatory field' : null,
      );
    );
  

  Widget buildSubmit() 
    return ElevatedButton(
      child: const Text('SUBMIT'),
      onPressed: () 
        if (!_formKey.currentState!.validate() ) return;
        //Save object whit category and product value
      
    );
  

希望有人可以帮忙,我卡住了

我尝试过的事情:

为每个 getbuilder(id: 'category') 和 getbuilder(id: 'product') 添加 id 以仅使用 update(['product']) 更新产品列表; 将类别和产品变量从屏幕移动到控制器

没有成功

Flutter 2.5.2 稳定通道,获取:^4.3.8 包,(未添加其他依赖)

【问题讨论】:

【参考方案1】:

经过几次尝试和错误后,我得到了它。我从中学到了很多东西:

您可以在每个字段中使用 UniqueKey,因此每次通知更新时都会重新构建它,如下所示:

return DropdownButtonFormField<int>(
  key: UniqueKey(),                                         // <-- Here
  value: (formControllerGral.product == null)? null : formControllerGral.product,
  isExpanded: true,
  decoration: const InputDecoration(labelText: 'Product'),
  items: newItems,
  onChanged: (value) 
    formControllerGral.product = value;
  ,
  validator: (value) => (value == null)? 'Mandatory field' : null,
);

它会起作用,但只是因为你明确告诉小部件它在每次重建时都不相同。

我不喜欢那个解决方案,所以我一开始一直在尝试使用普通的 DropdownButton,并注意到在调用 onChanged 函数时 Dropdown 的值没有显示,所以我添加了更新小部件本身的函数,但没有刷新选项列表(如 setState),这就成功了:

对于第二个也是最后一个解决方案:

为每个 getbuilder(id: 'category') 和 getbuilder(id: 'product') 添加 id 以仅使用 update(['product']) 更新产品列表; 将类别和产品变量从屏幕移动到控制器

查看

Widget buildSelectCategory() 
  return GetBuilder<FormController>(id: 'category', builder: (_) 
    List<Category> categories = formControllerGral.categoriesList;

    return DropdownButtonFormField<int>(
      value: (formControllerGral.category == null)? null : formControllerGral.category,
      isExpanded: true,
      decoration: const InputDecoration(labelText: 'Category'),
      items: categories.map((option) 
        return DropdownMenuItem(
          value: (option.id),
          child: (Text(option.desc)),
        );
      ).toList(),
      onChanged: (value) 
        formControllerGral.category = value;
        formControllerGral.product = null; //reset value of product variable
        formControllerGral.updateCategories(fillData: false); //Like a setState for this widget itself
        formControllerGral.updateProducts(value!);
      ,
      validator: (value) => (value == null)? 'Mandatory field' : null,
    );
  );


Widget buildSelectProduct() 
  return GetBuilder<FormController>(
    id: 'product', 
    builder: (_) 
      List<Product> products = formControllerGral.productsList;

      List<DropdownMenuItem<int>>? newItems = products.map((option) 
        return DropdownMenuItem(
          value: (option.id),
          child: Text(option.desc),
        );
      ).toList();

      return DropdownButtonFormField<int>(
        value: (formControllerGral.product == null)? null : formControllerGral.product,
        isExpanded: true,
        decoration: const InputDecoration(labelText: 'Product'),
        items: newItems,
        onChanged: (value) 
          formControllerGral.product = value;
          formControllerGral.updateProducts(formControllerGral.category!, fillData: false);  //Like a setState for this widget itself
        ,
        validator: (value) => (value == null)? 'Mandatory field' : null,
      );
    ,
  );

控制器

Future<void> updateCategories(bool fillData = true) async 
  if (fillData) 
    List<Category> newData = await fillCategories();
    categoriesList.assignAll(newData);
  
  update(['category']);

Future<void> updateProducts(int category, bool fillData = true) async 
  if (fillData) 
    List<Product> newData = await fillProducts(category);
    newData = newData.where((e) => e.category == category).toList();
    productsList.assignAll(newData);
  
  update(['product']);

这可能是我之前应该注意到的一个基本问题,但我现在不会忘记它。

【讨论】:

以上是关于Flutter GetBuilder Dependent DropDownButton - 即使值已被重置,也应该只有一项具有 [DropdownButton] 的值的主要内容,如果未能解决你的问题,请参考以下文章

make dep: 'depend' 无事可做

roscore 启动出现问题

sh DependênciasglobaisNODE

如何在 emacs org-mode 中配置depend.el?

Makefile depend规则

Makefile depend规则