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

Time:11-04

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"}
  ];
}
  • Related