I’m a bit stuck trying to implement futurebuilder in my main app (I have successfully used it elsewhere and I am struggling to get my head around what I am doing differently).
I have recreated the problem I have below from a bigger more complex app to try and pin down the bit I am stuck on… apologies if there is still some superfluous stuff in there but as I am not sure which bit is causing me the problem I left it in.
In summary I am loading some xml data which I need to display certain things from in the initial load.
If I load the data into tmp values and then set the state properties in setState it works but I have to set a lot of arbitrary values to the state properties which seems wrong.
This is the version of code that works (well it runs at least… the fact it is picking up the hard coded values in the real thing is less of an issue because I can set them to what I want and subsequent actions make everything correct).
import 'package:flutter/material.dart';
import 'load_xml.dart';
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final String xmlAssetPath = 'data.xml';
DataTree dataTree = DataTree(categories: []);
Category selectedCategory = Category(name: 'not_set', groupNames: []);
Group selectedGroup = Group(name: "not_set", items: []);
String groupName ="Citrus";
Item selectedItem = Item(name: "Orange");
int selectedItemIndex = 0;
void _loadData() async {
DataTree tmpDataTree = await loadXml(xmlAssetPath);
debugPrint ("Loading data tree...");
for (var category in tmpDataTree.categories) {
debugPrint ("Loading category ${category.name}...");
if ((category.isDefault ?? false) && (category.defaultGroupName != null)) {
debugPrint ("Setting groupName to ${category.defaultGroupName}...");
groupName = category.defaultGroupName!;
}
}
debugPrint ("Loading selected group $groupName...");
Group tmpSelectedGroup = await loadGroup(xmlAssetPath, groupName);
for (var item in tmpSelectedGroup.items) {
debugPrint ("Loading item ${item.name}...");
if (item.name == tmpSelectedGroup.defaultItemName) {
selectedItem = item;
selectedItemIndex = tmpSelectedGroup.items.indexOf(selectedItem);
}
}
setState(() {
dataTree = tmpDataTree;
selectedGroup = tmpSelectedGroup;
});
}
@override
void initState() {
super.initState();
_loadData();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: selectedCategory.name,
home: ListView.builder(
itemBuilder: (context, index) => Card(
key: ValueKey(selectedGroup.items[index].name),
child: ListTile(
title: Text(selectedGroup.items[index].name),
subtitle: Text((selectedGroup.items[index].name==selectedItem.name) ? "Selected" : "Not selected"),
),
),
itemCount: selectedGroup.items.length,
),
);
}
}
But ideally I want to avoid having the hard coded values so I tried to use FutureBuilder like this:
import 'package:flutter/material.dart';
import 'load_xml.dart';
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final String xmlAssetPath = 'data.xml';
Future<DataTree> dataTree;
Category selectedCategory = Category(name: 'not_set', groupNames: []);
Future<Group> selectedGroup;
// Group selectedGroup = Group(name: "not_set", items: []);
String groupName ="Citrus";
Item selectedItem = Item(name: "Orange");
int selectedItemIndex = 0;
void _loadData() async {
dataTree = await loadXml(xmlAssetPath);
debugPrint ("Loading data tree...");
for (var category in dataTree.categories) {
debugPrint ("Loading category ${category.name}...");
if ((category.isDefault ?? false) && (category.defaultGroupName != null)) {
debugPrint ("Setting groupName to ${category.defaultGroupName}...");
groupName = category.defaultGroupName!;
}
}
debugPrint ("Loading selected group $groupName...");
selectedGroup = await loadGroup(xmlAssetPath, groupName);
for (var item in selectedGroup.items) {
debugPrint ("Loading item ${item.name}...");
if (item.name == selectedGroup.defaultItemName) {
selectedItem = item;
selectedItemIndex = selectedGroup.items.indexOf(selectedItem);
}
}
}
@override
void initState() {
super.initState();
_loadData();
}
@override
Widget build(BuildContext context) {
return FutureBuilder (
future: Future.wait([dataTree,selectedGroup]),
builder: (BuildContext context, AsyncSnapshot<List<dynamic>> snapshot) {
if (snapshot.hasData) {
return MaterialApp (
title: snapshot.data![1].name,
home: ListView.builder(
itemBuilder: (context, index) => Card(
key: ValueKey(snapshot.data![1].items[index].name),
child: ListTile(
title: Text(snapshot.data![1].items[index].name),
subtitle: Text((snapshot.data![1].items[index].name==selectedItem.name) ? "Selected" : "Not selected"),
),
),
itemCount: snapshot.data![1].items.length,
)
);
} else {
return Container();
}
}
);
}
}
But of course I get
lib/my_app_await_not_working.dart:23:16: Error: A value of type 'DataTree' can't be assigned to a variable of type 'Future<DataTree>'.
- 'DataTree' is from 'package:test_init/load_xml.dart' ('lib/load_xml.dart').
- 'Future' is from 'dart:async'.
dataTree = await loadXml(xmlAssetPath);
...
which I figured out from here is because I am using await it stops it being a future.
However if I do this..
import 'package:flutter/material.dart';
import 'load_xml.dart';
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final String xmlAssetPath = 'data.xml';
Future<DataTree> dataTree;
Category selectedCategory = Category(name: 'not_set', groupNames: []);
Future<Group> selectedGroup;
// Group selectedGroup = Group(name: "not_set", items: []);
String groupName ="Citrus";
Item selectedItem = Item(name: "Orange");
int selectedItemIndex = 0;
void _loadData() {
dataTree = loadXml(xmlAssetPath);
debugPrint ("Loading data tree...");
for (var category in dataTree.categories) {
debugPrint ("Loading category ${category.name}...");
if ((category.isDefault ?? false) && (category.defaultGroupName != null)) {
debugPrint ("Setting groupName to ${category.defaultGroupName}...");
groupName = category.defaultGroupName!;
}
}
debugPrint ("Loading selected group $groupName...");
selectedGroup = loadGroup(xmlAssetPath, groupName);
for (var item in selectedGroup.items) {
debugPrint ("Loading item ${item.name}...");
if (item.name == selectedGroup.defaultItemName) {
selectedItem = item;
selectedItemIndex = selectedGroup.items.indexOf(selectedItem);
}
}
}
@override
void initState() {
super.initState();
_loadData();
}
@override
Widget build(BuildContext context) {
return FutureBuilder (
future: Future.wait([dataTree,selectedGroup]),
builder: (BuildContext context, AsyncSnapshot<List<dynamic>> snapshot) {
if (snapshot.hasData) {
return MaterialApp (
title: snapshot.data![1].name,
home: ListView.builder(
itemBuilder: (context, index) => Card(
key: ValueKey(snapshot.data![1].items[index].name),
child: ListTile(
title: Text(snapshot.data![1].items[index].name),
subtitle: Text((snapshot.data![1].items[index].name==selectedItem.name) ? "Selected" : "Not selected"),
),
),
itemCount: snapshot.data![1].items.length,
)
);
} else {
return Container();
}
}
);
}
}
I then get things like
ib/my_app_fb_not_working.dart:14:20: Error: Field 'dataTree' should be initialized because its type 'Future<DataTree>' doesn't allow null.
- 'Future' is from 'dart:async'.
- 'DataTree' is from 'package:test_init/load_xml.dart' ('lib/load_xml.dart').
Future<DataTree> dataTree;
^^^^^^^^
In both cases I also get errors like.
lib/my_app_fb_not_working.dart:25:35: Error: The getter 'categories' isn't defined for the class 'Future<DataTree>'.
- 'Future' is from 'dart:async'.
- 'DataTree' is from 'package:test_init/load_xml.dart' ('lib/load_xml.dart').
Try correcting the name to the name of an existing getter, or defining a getter or field named 'categories'.
for (var category in dataTree.categories) {
^^^^^^^^^^
I’ve tried a bunch of different things to declare them but can’t figure out how to get the data in via a future.
For ref if it helps to replicate here are the other modules i am testing with (don’t worry too much about the inefficient/buggy xml loading I can sort that out later.. I think it works well enough from a pov of testing this)….
main.dart
import 'package:flutter/material.dart';
import 'my_app.dart';
void main() {
runApp(const MyApp());
}
load_xml.dart
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:xml/xml.dart' as xml;
import 'dart:async';
class DataTree {
List<Category> categories = [];
DataTree({required this.categories});
}
class Category {
final List<String> groupNames;
final String name;
final bool? isDefault;
final String? defaultGroupName;
const Category({required this.groupNames, required this.name, this.isDefault, this.defaultGroupName});
}
class Group {
final List<Item> items;
final String name;
final bool? isDefault;
final String? defaultItemName;
const Group({required this.items, required this.name, this.isDefault, this.defaultItemName});
}
class Item {
final String name;
final bool? isDefault;
const Item({required this.name, this.isDefault});
}
Future<Group> loadGroup(String xmlAssetPath, String groupName) async {
debugPrint ("Inside load group for $groupName");
Group group;
List<Item> itemList = [];
String xmlString = await rootBundle.loadString(xmlAssetPath);
final document = xml.XmlDocument.parse(xmlString);
final groups = document.findAllElements('group')
.where((group) => group.getAttribute('name') == groupName)
.toList();
// do we need these?
String name='not set';
bool isGroupDefault = false;
bool isItemDefault = false;
String defaultItemName='not set';
for (var element in groups) {
String? name = element.getAttribute('name');
debugPrint("Checking group $name");
if (name == groupName) {
debugPrint("Matches $name");
name = element.getAttribute('name').toString();
isGroupDefault = (element.getAttribute('default') == 'true') ? true : false;
final items = element.findElements('item');
for (var item in items) {
String name = item.getAttribute('name').toString();
debugPrint("Parsing item $name");
isItemDefault = (item.getAttribute('default') == 'true') ? true : false;
if (isItemDefault == true) {
defaultItemName = name;
debugPrint ("Got default item $defaultItemName");
} // change this to id at some point
itemList.add ( Item (
name: name,
));
isItemDefault = false;
}
break;
}
}
group = Group (
items: itemList,
name: groupName,
isDefault: isGroupDefault,
defaultItemName: defaultItemName,
);
return group;
}
Future<DataTree> loadXml(String xmlAssetPath) async {
DataTree dataTree;
List<Category> categoryList = [];
String xmlString = await rootBundle.loadString(xmlAssetPath);
final document = xml.XmlDocument.parse(xmlString);
final categories = document.findAllElements('category');
for (var element in categories) {
List<String> groupNames = [];
String categoryName = element.getAttribute('name').toString();
final groups = element.findElements('group');
for (var element in groups) {
String groupName = element.getAttribute('name').toString();
groupNames.add(groupName);
}
categoryList.add(Category(
name: categoryName,
groupNames: groupNames,
));
}
dataTree = DataTree(categories: categoryList);
return dataTree;
}
assets/data.xml
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<data>
<category name="Fruit" default="true">
<group name="Citrus">
<item name="Orange"/>
<item name="Lemon" default="true"/>
<item name="Lime"/>
</group>
<group name="Berry" default="true">
<item name="Strawberry"/>
<item name="Raspberry" default="true"/>
<item name="Gooseberry"/>
</group>
<group name="Exotic">
<item name="Pineapple"/>
<item name="Papaya"/>
<item name="Mango"/>
</group>
</category>
<category name="Vegetable">
<group name="Root">
<item name="Carrot"/>
<item name="Potato"/>
<item name="Parsnip"/>
</group>
<group name="Brassica">
<item name="Cabbage"/>
<item name="Broccoli"/>
<item name="Kale"/>
</group>
</category>
</data>
2
Answers
So thanks to @pskink I have a working version... using a record makes it quite neat although I don't think that is the thing that actually fixed it - rather using a separate future returned from _loadData and then adding a load of ? and ! to override nulls - but I just don't have time to do all the differential diagnosis to see exactly what fixed what (feel free to add comments to make my code more robust).
As an aside I like the fact that retrieving elements from records makes my code look a bit more like Perl :)
Time to stop using FutureBuilder and start using Riverpod. Riverpod can nicely manage cached data from async operations, and aggregate those for overall results. I stopped using FutureBuilder as soon as I became comfortable with Riverpod providers.