簡體   English   中英

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

我有兩個 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
      }
    );
  }
}

希望有人能幫忙,拜托,我卡住了

我嘗試過的事情:

  • 將 id 添加到每個 getbuilder(id: 'category') 和 getbuilder(id: 'product') 以僅使用 update(['product']) 更新產品列表;
  • 將類別和產品變量從屏幕移動到控制器

沒有成功

Flutter 2.5.2 穩定通道,獲取:^4.3.8 包,(未添加其他依賴)

經過幾次嘗試和錯誤,我讓它工作了。 我從中學到了很多東西:

您可以在每個字段中使用 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),這就成功了:

對於第二個也是最后一個解決方案:

  • 將 id 添加到每個 getbuilder(id: 'category') 和 getbuilder(id: 'product') 以僅使用 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']);
}

可能這是我以前應該注意的一個基本問題,但我現在不會忘記它。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM