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,一个用于显示类别(水果和蔬菜),第二个用于根据 selected 类别显示产品(Apple、letucce 等),因此它取决于第一个。
当我 select 类别时,产品列表会更新以显示相应的项目。如果我然后 select 一个产品,它是 selected,但如果我再次更改类别 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']);[=50 更新产品列表=]
- 将类别和产品变量从屏幕移动到控制器
没有成功
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),这就成功了:
对于第二个也是最后一个解决方案:
- 为每个 getbuilder(id: 'category') 和 getbuilder(id: 'product') 添加 id 以仅使用 update(['product']);[=32 更新产品列表=]
- 将类别和产品变量从屏幕移动到控制器
查看
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']);
}
可能这是我之前应该注意的基本问题,但我现在不会忘记它。
我有两个 DropDownButtons,一个用于显示类别(水果和蔬菜),第二个用于根据 selected 类别显示产品(Apple、letucce 等),因此它取决于第一个。
当我 select 类别时,产品列表会更新以显示相应的项目。如果我然后 select 一个产品,它是 selected,但如果我再次更改类别 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']);[=50 更新产品列表=]
- 将类别和产品变量从屏幕移动到控制器
没有成功
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),这就成功了:
对于第二个也是最后一个解决方案:
- 为每个 getbuilder(id: 'category') 和 getbuilder(id: 'product') 添加 id 以仅使用 update(['product']);[=32 更新产品列表=]
- 将类别和产品变量从屏幕移动到控制器
查看
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']);
}
可能这是我之前应该注意的基本问题,但我现在不会忘记它。