Search code examples
flutterlistviewlisttile

Flutter change state of an item in a ListView


I'm trying to use a ListTile widget inside a ListView that will change depending on some information received elsewhere in the app. In order to practice, I tried to make a little Dartpad example.

The problem is as follows: I have been able to change the booleans and data behind the items, but they don't update within the ListView.builder.

I have ListTile widgets inside the ListView that I want to have four different states as follows: idle, wait, requested, and speaking.

The different states look like this, as an example: enter image description here

I am trying to change the individual state of one of these items, but I haven't been able to get them to properly update within the ListView.

The ListTile code looks like this. Most of this code is responsible for just handling what the UI should look like for each state:

class UserItem extends StatefulWidget {
  String name;
  UserTileState userTileState;
  UserItem(this.name, this.userTileState);

  @override
  UserItemState createState() => UserItemState();
}

class UserItemState extends State<UserItem> {
  String _getCorrectTextState(UserTileState userTileState) {
    switch (userTileState) {
      case UserTileState.speaking:
        return "Currently Speaking";
      case UserTileState.requested:
        return "Speak Request";
      case UserTileState.wait:
        return "Wait - Someone Speaking";
      case UserTileState.idle:
        return "Idle";
    }
  }

  Widget _getCorrectTrailingWidget(UserTileState userTileState) {
    switch (userTileState) {
      case UserTileState.speaking:
        return const CircleAvatar(
            backgroundColor: Colors.green,
            foregroundColor: Colors.white,
            child: Icon(Icons.volume_up));
      case UserTileState.requested:
        return CircleAvatar(
          backgroundColor: Colors.green.shade100,
          foregroundColor: Colors.grey[700],
          child: const Icon(Icons.volume_up),
        );
      case UserTileState.wait:
        return CircleAvatar(
          backgroundColor: Colors.green.shade100,
          foregroundColor: Colors.grey[700],
          child: const Icon(Icons.volume_off),
        );
      case UserTileState.idle:
        return CircleAvatar(
          backgroundColor: Colors.green.shade100,
          foregroundColor: Colors.grey[700],
          child: const Icon(Icons.volume_off),
        );
    }
  }

  void kickUser(String name) {
    print("Kick $name");
  }

  @override
  Widget build(BuildContext context) {
    return ListTile(
        onLongPress: () {
          kickUser(widget.name);
        },
        title: Text(widget.name,
            style: TextStyle(
                fontWeight: widget.userTileState == UserTileState.speaking ||
                        widget.userTileState == UserTileState.requested
                    ? FontWeight.bold
                    : FontWeight.normal)),
        subtitle: Text(_getCorrectTextState(widget.userTileState),
            style: const TextStyle(fontStyle: FontStyle.italic)),
        trailing: _getCorrectTrailingWidget(widget.userTileState));
  }
}

enum UserTileState { speaking, requested, idle, wait }

To try and trigger a change in one of these UserTile items, I wrote a function as follows. This one should make an idle tile become a requested tile.

void userRequest(String name) {
    // send a speak request to a tile
    int index = users.indexWhere((element) => element.name == name);
    users[index].userTileState = UserTileState.requested;
  }

I will then run that userRequest inside my main build function inside a button, as follows:

class MyApp extends StatefulWidget {
  @override
  MyAppState createState() => MyAppState();
}

class MyAppState extends State<MyApp> {
  
  void userRequest(String name) {
    // send a speak request to a tile
    int index = users.indexWhere((element) => element.name == name);
    users[index].userTileState = UserTileState.requested;
  }

  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Column(children: [
      Expanded(
          flex: 8,
          child: ListView.separated(
              separatorBuilder: (context, index) {
                return const Divider();
              },
              itemCount: users.length,
              itemBuilder: (context, index) {
                return users[index];
              })),
      Expanded(
          flex: 2,
          child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                
                TextButton(
                    onPressed: () {
                      setState(() {
                        userRequest("Test1");
                      });
                    },
                    child: const Text("Send Request")),
                
              ]))
    ]));
  }
}

When I tap the button to set the value within the first UserTile, nothing happens.

I don't know where to put setState, and the state of the object within the ListView isn't being updated. What is the most simple solution to this problem? Provider? I've used Provider in this same situation and can't get it to work. Is there another simpler solution to this?

How can I change update the state of a specific element within a ListView?


Solution

  • The code that I posted has had mixed results for some. My main project actually is actually a bit more complex and involves Provider, but I tried my best to strip the code down to its simplest parts when I posted this question. I'm wondering if something else is getting in the way of this working. (maybe it's because I threw this example together using dartpad?)

    In the mean time, I've discovered that adding or removing an element to the list seems to change the entire state of the list. Using this technique, instead of trying to change an individual value, I get its index, remove it, and re-add it at that index and it works like a charm. So instead of this:

    void userRequest(String name) {
        // send a speak request to a tile
        int index = users.indexWhere((element) => element.name == name);
        users[index].userTileState = UserTileState.requested;
      }
    
    

    followed by setState(() => userRequest("name");, the following seems to work instead:

    void userRequest(String name) {
        // send a speak request to a tile
        int index = users.indexWhere((element) => element.name == name);
        users.removeAt(index);
        users.insert(index, UserItem(name, UserTileStates.requested));
      }