user3249281
user3249281

Reputation: 535

Flutter - How to update state (or value?) of a Future/List used to build ListView (via FutureBuilder)

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.

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!

Upvotes: 5

Views: 19364

Answers (2)

Jaime
Jaime

Reputation: 894

You may have to use ListviewBuilder instead of FutureBuilder. I had a similar problem where I had to load data from firestore, manipulate it, and only then send it down to the ListView, so I couldn't use FutureBuilder. I essentially looped the QuerySnapShot, made the changes where appropriate in each doc, then added it to a List object (List chatUsersList = List();):

String seenText = "";
chatSnapShot.forEach((doc) {
      if (doc.seen) {
        seenText = "not seen yet";
      } else {
        seenText = "seen " + doc.seenDate;
      }
      ...
      chatUsersList.add(Users(id, avatar, ..., seenText));
    }

Then in the ListViewBuilder:

ListView.builder(
  itemBuilder: (context, index) {       
    return
   UserTile(uid, chatUsersList[index].peerId,
      chatUsersList[index].avatar,..., chatUsersList[index].seenText);
  },
  itemCount: chatUsersList.length, ),

And then in UserTile:

class UserTile extends StatelessWidget {
  final String uid;
  final String avatar;
  ...
  final String seenText;

  ContactTile(this.uid, this.avatar, ..., this.seenText);
  @override
  Widget build(BuildContext context) {

    var clr = Colors.blueGrey;
    if (seenText == "not seen yet") clr = Colors.red;
    ...

    return
    ListTile(
      isThreeLine: true,
      leading:
      Container(

        width: 60.0,

        height: 60.0,
        decoration: new BoxDecoration(
          shape: BoxShape.circle,
          image: new DecorationImage(
            fit: BoxFit.cover,
            image: new CachedNetworkImageProvider(avatar),
          ),
        ),
      ),
      title: Row(
        mainAxisSize: MainAxisSize.max,
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: < Widget > [
          Expanded(
            child: Text(
              name, style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold, ),
              overflow: TextOverflow.ellipsis,
              maxLines: 1
            )
          ),
          Text(
            timestamp, style: TextStyle(fontSize: 14.0, ),
            overflow: TextOverflow.ellipsis,
            maxLines: 1
          ),
        ],
      ),
      subtitle: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: < Widget > [
          Text(sub, style: TextStyle(color: Colors.black87, fontSize: 14.0, ),
            overflow: TextOverflow.ellipsis, ),
          Text(**seenText**, style: TextStyle(color: **clr**, fontSize: 12.0, fontStyle: FontStyle.italic), ),
        ],
      ),
      trailing: Icon(Icons.keyboard_arrow_right),
      onTap: () {
     ...

      });

  }

Upvotes: 0

spenster
spenster

Reputation: 538

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.

Upvotes: 2

Related Questions