setState() doesn't refresh my widget tree when using dynamic data to build the page

Question

Asked by ThomasG2201 on November 03, 2021 (source).

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.

Answer

Question answered by Roslan A (source).

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"}
  ];
}
FLUTTER
SHARE: