Flutter provider not working with dynamically built ExpansionPanelRadio

29 Views Asked by At

I am building an app where a ExpansionPanelList is being built dynamically from a custom class. The list is beneath a header with some details about the content of the list, including the total number of items.

When items are deleted from the list using a trailing button on each radio panel, the items are removed from the list but the total shown in the header does not change (unless the user changes pages and back again, forcing a rebuild). The provider seems to work just fine when encapsulating statically built widgets, but this dynamic one gives it problems. I originally used stateless widgets, but doing this meant a slider on each panel didn't update, nor did the list rebuild , until some action was taken to force it.

I changed to stateless which fixed that issue, but the header not refreshing remains.

Edit: here is a little summary:

Consumer+builder    
  ->Tab
    -> Header,
    -> Builder[ExpandingPanels…]
  ->Drawer

I have the consumer right at the root, have tried it all the way down the tree to no effect. Child property is set to ‘_’ so that it doesnt ignore children.

On both the header and each expanding panel I’ve added a delete button to remove an item from the list which generates the expandingpanels.

On the drawer I’ve added a button to add an item to the list.

When the delete (on header and panel) or add (on drawer) is pressed, here are the results:

  • Stateless/Stateful Header -> list rebuilds, header updates (i.e. all working as expected)
  • Stateful List -> List rebuilds, header doesnt update
  • Stateful Drawer-> Neither list nor header updates

So it looks like siblings are talking to each othe

This is the header code, you can see the 2 consumers (which don't update unless moving back and forward to this page.

class BagHeaderBar extends StatelessWidget {
  const BagHeaderBar({
    Key? key,
    required this.bagIndex,
  }) : super(key: key);

  final int bagIndex;

  @override
  Widget build(BuildContext context) {
    return Card(
        child: Padding(
          child: IntrinsicHeight(
            child: Row(
              children: <Widget>[
                Expanded(
                    flex: 8,
                    child: Align(
                      alignment: Alignment.centerLeft,
                      child: Consumer<PlayerCharacter>(   // <--------- this doesn't seem to 'consume'
                          builder: (context, player, _) {
                        return Text(
                          player.bag[bagIndex].bagNotes,
                        );
                      }),
                    )),
                Expanded(
                  flex: 2,
                  child: Align(
                    alignment: Alignment.centerRight,
                    child: Consumer<PlayerCharacter>(     // <--------- this doesn't seem to 'consume'
                        builder: (context, player, _) {
                      return Text(
                        '${player.bag[bagIndex].capacityUsed} / ${player.bag[bagIndex].capacityMax}',
                        style: const TextStyle(fontSize: 18),
                      );
                    }),
                  ),
                ),
              ],
            ),
          ),
        ));
  }
}

And here is the code which is dynamically built, and which has the delete buttons. They are meant to remove the item from the list and adjust the number in the header. The item gets removed (because its now stateful. It didn't refresh when stateless, but the data was removed from the object.


class ExpansionInventoryBag extends StatefulWidget {
  const ExpansionInventoryBag({
    Key? key,
    required this.bagIndex,
  }) : super(key: key);

  final int bagIndex;

  @override
  State<ExpansionInventoryBag> createState() => _ExpansionInventoryBagState();
}
//---------------------------------------------
class _ExpansionInventoryBagState extends State<ExpansionInventoryBag> {
  @override
  Widget build(BuildContext context) {

    final player = context.read<PlayerCharacter>();

    return SingleChildScrollView(
        child: ExpansionPanelList.radio(
          children: player.bag[widget.bagIndex].contents!.map<ExpansionPanelRadio>((BagSlot bagSlot) {
            return ExpansionPanelRadio(
                canTapOnHeader: true,
                value: bagSlot.slotId,
                headerBuilder: (BuildContext context, bool isExpanded) {
                  return ListTile(
                    title: Text(
                        '${bagSlot.getBaseInventoryItem().name} ${(bagSlot.quantity > 1) ? "(${bagSlot.quantity})" : ""}'),
                    trailing: bagSlot.getBaseInventoryItem().equippable
                        ? Switch(
                            value: bagSlot.equipped,
                            onChanged: (bool value) {
                              final player = context.read<PlayerCharacter>();
                              setState(() { 
                                player.bag[widget.bagIndex]
                                    .setEquipStateById(bagSlot.slotId, value);
                              });
                            },
                          )
                        : null,
                  );
                },
                body: ListTile(
                  title: Text(bagSlot.getBaseInventoryItem().shortDescription),
                  subtitle: Text(bagSlot.slotId),
                  trailing: const Icon(Icons.delete),
                  onTap: () {
                    setState(() {
                      player.bag[widget.bagIndex].removeItemById(bagSlot.slotId);
                    });
                    //final player = context.read<PlayerCharacter>();
                  },
                ));
          }).toList(),
        ),
      );

  }
}


Ive tried generating ChangeNotiferProviders dynamically, multi providers for BagSlot (the elements which make up the panels). Stateless, and Stateful widgets. Callbacks.

0

There are 0 best solutions below