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')
),
],
);
}
}
Widget
inside your business logicYou 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')
),
],
);
}
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.
build
method that has side effectsDon'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.