flutterdart

Is this approach using ValueNotifier and FutureBuilder correct?


I'm reading a JSON file to generate the layout based on the data. Basically, it returns either a column or a row. I'm using FutureBuilder to handle the data loading.

I’m also using a ValueNotifier to detect when readyToInstall changes and trigger setState().

What do you think of this approach? Would you consider it a proper way to handle this situation?

One thing I noticed: when I click the "Update" button, print("ok"); gets called twice. Is that expected behavior?

The code works fine, but my goal is to learn the correct way to implement this.

class InitInterface extends StatefulWidget {
  const InitInterface({super.key});

  @override
  State<InitInterface> createState() => _InitInterfaceState();
}

class _InitInterfaceState extends State<InitInterface> {
  //used in FutureBuilder to know when we are ready to start install the layout.
  late Future<Widget> readyToInstall;
  late Stream<Widget> readyToInstall0;

  @override
  void initState() {
    //will read json file and get data to install the layout
    readyToInstall = loadLayout();
    readyToInstall.then((value) {
      print('teste');
      panelData.addListener(() {
        //panelData is objhect from a class that extend ValueNotifier
        readyToInstall = updateLayout();
        setState(() {});
      });
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        FutureBuilder<Widget>(
          future: readyToInstall,
          builder: (BuildContext context, AsyncSnapshot<Widget> snapshot) {
            if (snapshot.hasData) {
              print('ok'); //<--called two times after update
              return snapshot.data!;
            } else if (snapshot.hasError) {
              return Text('Error: ${snapshot.error}');
            } else {
              return const Loading();
            }
          },
        ),
        ElevatedButton(
            onPressed: () {
              readyToInstall = updateLayout();
              setState(() {});
            },
            child: Text('Update')
        ),
      ],
    );
  }
}

Solution

  • Do not use Widget inside your business logic

    You should avoid using Widget outside of your ui code. InitInterfaceState should receive the data directly and decide the layout based on the data:

    class _InitInterfaceState extends State<InitInterface> {
      late Future<Data> data;
      late Stream<Data> streamOfData;
      ...
    

    Then define a method which actually decides whether a Column or Row should be used inside the widget tree:

    Widget getLayout(Data? data) {
      if (data == null) {
      // Throw exception if the data is null
        throw Exception(...);
      }
      if (data.needsColumn) {
        return Column(...);
      } else {
        return Row(...);
      }
    }
    

    And then you can call this function inside your FutureBuilder:

    @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            FutureBuilder<Widget>(
              // Use data instead of widget here
              future: data,
              builder: (BuildContext context, AsyncSnapshot<Widget> snapshot) {
                if (snapshot.hasData) {
                  return getLayout(snapshot.data);
                } else if (snapshot.hasError) {
                  return Text('Error: ${snapshot.error}');
                } else {
                  return const Loading();
                }
              },
            ),
            ElevatedButton(
                onPressed: () {
                  readyToInstall = updateLayout();
                  setState(() {});
                },
                child: Text('Update')
            ),
          ],
        );
      }
    

    Avoid using setState inside initState

    The reason why here it's not a problem is that setState is being called inside a callback which is fine. But still it's not a good practice, so you should consider moving it to e.g. getLayout function or define a new one and call it.

    I'm not sure what you need the ValueNotifier exactly for, so I can't help you out there.

    Do not write ui logic inside the build method that has side effects

    Don't worry, you didn't do that. The reason why it's bad is that build method can be called several times in sequence. That's why your print("ok") is being called multiple times.