Im pasting relevant code below, but you may be able to answer it based upon my pseudo-explanation.
Im using a FutureBuilder to build a listview.
Future<List<Location>> _listFuture
variable (which is the future for the FutureBuilder). The problem is that the ListView/Contrainer/ListTile is driven by a
Future<List<Location>>
. I can pass the "tapped" index to my ontap handler, but I dont believe I can have my _changeBackground() just update the backgroundColor value for the selected index and call setState() because you cannot directly access/update a future like that (I get the error ERROR: The operator '[]' isn't defined for the class 'Future<List<Location>>'.
)
Im not sure Im taking the right approach. In this case, I guess I could always theoretically separate the "background" color tracking into a new separate List (outside of the future) and track/reference it that way using aligned indexes from onTap().
However, I am not sure that would always work. In the future, I might need to actually change the values/state of what was returned in the future. For instance, think of if I wanted to be able to click on a list item and update the "companyName". In this case, I would be changing a value stored in the future directly. I guess I could technically send the new name to the server and completely refresh the list that way, but that seems inefficient (what if they decide to "cancel" and not save changes?).
Any help is appreciated. thanks!
this class actually holds the relevant data for the list
// Location
class Location {
// members
String locationID;
String locationName;
String companyName;
String backgroundColor = 'fc7303';
// constructor?
Location({this.locationID, this.locationName, this.companyName});
// factory?
factory Location.fromJson(Map<String, dynamic> json) {
return Location(
locationID: json['locationID'],
locationName: json['locationName'],
companyName: json['companyName'],
);
}
}
this class is the parent json response that has "result" (success/error) messages. it instantiates the class above as a list to track the actual company/location records
//jsonResponse
class jsonResponse{
String result;
String resultMsg;
List<Location> locations;
jsonResponse({this.result, this.resultMsg, this.locations});
factory jsonResponse.fromJson(Map<String, dynamic> parsedJson){
var list = parsedJson['resultSet'] as List;
List<Location> locationList = list.map((i) => Location.fromJson(i)).toList();
return jsonResponse(
result: parsedJson['result'],
resultMsg: parsedJson['resultMsg'],
locations: locationList
);
}
} // jsonResponse
here is the state and stateful widgets that use the classes above to parse API data and create ListView
class locationsApiState extends State<locationsApiWidget> {
// list to track AJAX results
Future<List<Location>> _listFuture;
// init - set initial values
@override
void initState() {
super.initState();
// initial load
_listFuture = updateAndGetList();
}
Future<List<Location>> updateAndGetList() async {
var response = await http.get("http://XXX.XXX.XXX.XXX/api/listCompanies.php");
if (response.statusCode == 200) {
var r1 = json.decode(response.body);
jsonResponse r = new jsonResponse.fromJson(r1);
return r.locations;
} else {
throw Exception('Failed to load internet');
}
}
_changeBackground(int index){
print("in changebackground(): ${index}"); // this works!
_listFuture[index].backgroundColor = '34bdeb'; // ERROR: The operator '[]' isn't defined for the class 'Future<List<Location>>'.
}
// build() method
@override
Widget build(BuildContext context) {
return new FutureBuilder<List<Location>>(
future: _listFuture,
builder: (context, snapshot){
if (snapshot.connectionState == ConnectionState.waiting) {
return new Center(
child: new CircularProgressIndicator(),
);
} else if (snapshot.hasError) {
return new Text('Error: ${snapshot.error}');
} else {
final items = snapshot.data;
return new Scrollbar(
child: new RefreshIndicator(
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
//Even if zero elements to update scroll
itemCount: items.length,
itemBuilder: (context, index) {
return
Container(
color: HexColor(items[index].backgroundColor),
child:
ListTile(
title: Text(items[index].companyName),
onTap: () {
print("Item at $index is ${items[index].companyName}");
_changeBackground(index);
} // onTap
)
);
},
),
onRefresh: () {
// implement later
return;
} // refreshList,
),
);
}// else
} // builder
); // FutureBuilder
} // build
} // locationsApiState class
class locationsApiWidget extends StatefulWidget {
@override
locationsApiState createState() => locationsApiState();
}
helper class (taken from somewhere on stackoverflow) for converting HEX to integer colors
class HexColor extends Color {
static int _getColorFromHex(String hexColor) {
hexColor = hexColor.toUpperCase().replaceAll("#", "");
if (hexColor.length == 6) {
hexColor = "FF" + hexColor;
}
return int.parse(hexColor, radix: 16);
}
HexColor(final String hexColor) : super(_getColorFromHex(hexColor));
}
Thanks!
What I would recommend is removing the background color from your location class, and instead storing in your State which locations are selected. That way, your location list doesn't need to change when items are selected. I would also create a StatelessWidget for your location item, which would set the background color, depending on whether it's selected or not. So:
// for the LocationItem widget callback
typedef void tapLocation(int index);
class locationsApiState extends State<locationsApiWidget> {
// list to track AJAX results
Future<List<Location>> _listFuture;
final var selectedLocationIndices = Set<int>();
// init - set initial values
@override
void initState() {
super.initState();
// initial load
_listFuture = updateAndGetList();
}
Future<List<Location>> updateAndGetList() async {
var response = await http.get("http://XXX.XXX.XXX.XXX/api/listCompanies.php");
if (response.statusCode == 200) {
var r1 = json.decode(response.body);
jsonResponse r = new jsonResponse.fromJson(r1);
return r.locations;
} else {
throw Exception('Failed to load internet');
}
}
void _toggleLocation(int index) {
if (selectedLocationIndices.contains(index))
selectedLocationIndices.remove(index);
else
selectedLocationIndices.add(index);
}
// build() method
@override
Widget build(BuildContext context) {
return new FutureBuilder<List<Location>>(
future: _listFuture,
builder: (context, snapshot){
if (snapshot.connectionState == ConnectionState.waiting) {
return new Center(
child: new CircularProgressIndicator(),
);
} else if (snapshot.hasError) {
return new Text('Error: ${snapshot.error}');
} else {
final items = snapshot.data;
return new Scrollbar(
child: new RefreshIndicator(
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
//Even if zero elements to update scroll
itemCount: items.length,
itemBuilder: (context, index) {
return LocationItem(
isSelected: selectedLocationIndices.contains(index),
onTap: () => setState({
_toggleLocation(index);
})
);
},
),
onRefresh: () {
// implement later
return;
} // refreshList,
),
);
}// else
} // builder
); // FutureBuilder
} // build
} // locationsApiState class
class locationsApiWidget extends StatefulWidget {
@override
locationsApiState createState() => locationsApiState();
}
And the Item list entry:
class LocationItem extends StatelessWidget {
final bool isSelected;
final Function tapLocation;
const LocationItem({@required this.isSelected, @required this.tapLocation, Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
color: isSelected ? HexColor('34bdeb') : HexColor('fc7303'),
child: ListTile(
title: Text(items[index].companyName),
onTap: () => tapLocation() // onTap
)
);
}
}
Forgive me, I can't compile it so I hope it's correct. But I think you get the idea: have the Stateful widget keep track of the selected locations separately, and let the location decide how to render itself when it's rebuilt.