I'm using the latest version of Flutter and I have a problem that I don't understand. Here is the situation: I build a dropdown button according to the value of a variable. That variable might change via setState() when changing the value of the dropdown.
Code example:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const DynamicWidget(),
);
}
}
class DynamicWidget extends StatefulWidget {
const DynamicWidget({Key? key}) : super(key: key);
@override
_DynamicWidgetState createState() => _DynamicWidgetState();
}
class _DynamicWidgetState extends State<DynamicWidget> {
List<Map<String, String>> data = [];
@override
Widget build(BuildContext context) {
// after a condition here, data can become this:
// **NOTE**: data will never have the same length
data = [
{"name": 'c', "unit": "cm"}
];
return Scaffold(
appBar: AppBar(
elevation: 0,
title: const Text('An example'),
),
body: Column(
children: [
// a few things there
// ...
// here, that text will never change despite "setState()"
Text(data.toString()),
ListView.builder(
itemCount: data.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
itemBuilder: (_, int i) {
return Row(
children: [
Text("Unit for ${data[i]['name']} :"),
const SizedBox(width: 10),
DropdownButton(
elevation: 0,
value: data[i]['unit'],
onChanged: (String? newUnit) {
setState(() {
// doesn't change anything
data[i]['unit'] = newUnit!;
// however the new value is printed correctly
print("data = $data");
});
},
underline: Container(height: 2, color: Colors.cyan),
items: ["km", "hm", "dam", "m", "dm", "cm", "mm"]
.map<DropdownMenuItem<String>>(
(String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
},
).toList(),
),
],
);
},
),
],
),
);
}
}
Basically I have some data that represent letters of a formula and their given unit (and that formula will change over time according to user's choices). I want to have a dropdown that lists the variables and allows the user to change the unit of these variables. I'm blocked because setState()
doesn't do anything, it doesn't refresh the page, therefore the initial value given to the DropdownButton
stays the same.
CodePudding user response:
The setState
method will rebuild everything inside the build Widget, so if you're initializing your values there, it will always stay in the initial item.
Here's an example of how to build a dropdown menu, either with predefined or asynchronous data:
class Example extends StatefulWidget {
const Example({
Key? key,
}) : super(key: key);
@override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
final _stageController = TextEditingController();
final _formKey = GlobalKey<FormState>();
final _scaffoldKey = GlobalKey<ScaffoldState>();
var _stages = List<Stages>.empty(growable: true);
var _dropdownMenuItems = List<DropdownMenuItem<Stages>>.empty(growable: true);
// you can define your data here
Stages _selectedStage = Stages(
id: 20000,
name: "Escolha",
description: "Selecione",
registration: DateTime.now().toIso8601String(),
);
@override
void initState() {
// if you need to load async data
loadStages();
super.initState();
}
void loadStages() async {
_stages.add(
Stages(
id: 1,
name: "System",
description: "description",
registration: DateTime.now().toIso8601String(),
),
);
try {
// here load your data
//StagesBloc stagesBloc = context.read<StagesBloc>();
List<Stages> tempStages = await stagesBloc.getStages();
setState(() {
_stages = tempStages;
});
} catch (e) {
print(e);
}
_dropdownMenuItems = buildDropdownMenuItems(_stages);
_selectedStage = _dropdownMenuItems[0].value!;
}
List<DropdownMenuItem<Stages>> buildDropdownMenuItems(List allStages) {
List<DropdownMenuItem<Stages>> items = [];
for (Stages stage in allStages) {
items.add(
DropdownMenuItem(
value: stage,
child: Text(stage.name.toString()),
),
);
}
return items;
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: _buildAppBar(),
body: _body(),
);
}
AppBar _buildAppBar() {
return AppBar(
elevation: 0,
backgroundColor: Color(0xFF002d59),
centerTitle: true,
automaticallyImplyLeading: false,
title: Text(
"SOME TEXT",
overflow: TextOverflow.ellipsis,
style: TextStyle(
letterSpacing: 4,
fontWeight: FontWeight.bold,
),
),
);
}
Container _body() {
return Container(
padding: EdgeInsets.only(top: 30),
child: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if(_stages.isNotEmpty) _selectETAPA(),
],
),
),
),
);
}
Container _selectETAPA() {
return Container(
height: 100,
margin: EdgeInsets.only(
top: 25,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"SOME TEXT HERE",
style: TextStyle(
fontWeight: FontWeight.w400,
color: Colors.black,
fontSize: 15,
letterSpacing: 2,
),
overflow: TextOverflow.ellipsis,
),
DropdownButton(
isExpanded: true,
value: _selectedStage,
items: _dropdownMenuItems,
onChanged: (val) => setState(
() {
_selectedStage = val as Stages;
_stageController.text = _selectedStage.name.toString();
},
),
),
],
),
);
}
}
CodePudding user response:
This statement:
data = [
{"name": 'c', "unit": "cm"}
];
is executed every time the widget is rebuilt. So your setState
works but whatever you set in setState
is overidden by this code.
Declare data
as late final
so you don't accidentally reinitialize it as you did previously. And initialize data
once in initState
:
late final List<Map<String, String>> data;
...
@override
void initState() {
super.initState();
data = [
{"name": 'c', "unit": "cm"}
];
}