Riverpod StateProvider doesn't work on list

Viewed 246

I'm trying to create a Checkbox List, selectedProvider contains a boolean list with length choices.lenght. Let's say choices.length is 4, the initial list should be [false, false, false, false], let's say the user click on the last one, the list should turned into [false, false, false, true], but the bullet box doesn't update. Am I doing something wrong?

Widget getCheckboxList(WidgetRef ref, int start, int end) {
  List<Widget> content = [];

  final selectedProvider = StateProvider<List<bool>>(
      (ref) => List.filled(choices.length, false));

  for (int i = start; i <= end; i++) {
    content.add(
      Theme(
        data: ThemeData(
          splashColor: Colors.transparent,
          highlightColor: Colors.transparent,
        ),
        child: CheckboxListTile(
          controlAffinity: ListTileControlAffinity.leading,
          contentPadding: const EdgeInsets.symmetric(horizontal: 4),
          visualDensity: const VisualDensity(horizontal: -2, vertical: -2),
          title: Row(
            children: [
              const SizedBox(width: 5),
              Text(choices[i][0]),
              const Spacer(),
              Text(choices[i][1] == 0 ? '免費' : '+ HK\$ ${choices[i][1]}'),
              const SizedBox(width: 16),
            ],
          ),
          activeColor: primaryColor,
          value:
              ref.watch(selectedProvider.select((selected) => selected[i])),
          onChanged: (bool? picked) {
            ref.read(selectedProvider.notifier).state[i] = picked!;
          },
        ),
      ),
    );
  }
  return Column(children: [...content]);
}
2 Answers

The reason for this is that you've put the provider definition inside the getCheckboxList method, which I would presume (without seeing the rest of the code) that it is used inside another widget's build method. That will cause the method to be recalled (and re-executed) each time the widget rebuilds.

If you move the selectedProvider definition to outside of any class (as is required), it should work for you.

In general, you should avoid using methods for building widgets. These are highly inefficient, and do not get "cached" by the platform when the app runs. Instead, you can (and should) package such logic inside custom widgets.

For example, the above might be changed into this:

// content_widget.dart
final selectedProvider = StateProvider<List<bool>>(
  (ref) => List.filled(4, false),
);

class ContentWidget extends ConsumerWidget {
  ...
  @override
  Widget build(BuildContext build, WidgetRef ref) {
    final List<bool> selectedValues = ref.watch(selectedProvider);

    return Theme(
      data: ThemeData(),
      child: Column(
        ...
        children: List.generate(
          selectedValues.length,
          (int i) => CheckboxListTile(
            ...
            value: selectedValues[i],
            onChanged: (bool? selected) {
              ref.read(selectedProvider.notifier).state[i] = picked!;
            },
          ),
        ),
      ),
    );
  }
}

And if you want your provider to change its size based on an arbitrary choices object, you can simply create a choicesProvider and then your selectProvider can use it like this:

final selectedProvider = StateProvider<List<bool>>(
  (ref) => List.filled(ref.watch(choicesProvider).length, false),
);

Since the state is immutable, we need to do something like this:

onChanged: (_) {
  ref.read(selectedProvider.notifier).update(
        (state) => [
          for (var j = 0; j < state.length; j++)
            if (j == i) !state[j] else state[j],
        ],
      );
},

It will be more readable if you use StateNotifier and StateNotifierProvider instead. You can then define a toggle() function.

class Selected extends StateNotifier<List> {
  Selected(List initial) : super(initial);

  void toggle(index) {
    state = [
      for (var i = 0; i < state.length; i++)
        if (i == index) !state[i] else state[i],
    ];
  }
}

Then define the provider

final selectedProvider = StateNotifierProvider<Selected, List>(
    (ref) => Selected(List.filled(8, false)));

When you have to change the value,

onChanged: (_) {
  ref.read(selectedProvider.notifier).toggle(i);
},
Related