7

I'm struggling to figure out how to use Riverpod for the following scenario.

I have a ListView where the children are Containers with a button inside.

When the button is pressed, I want to change the colour of that container. I want that colour to be stored in a piece of state using a Riverpod provider.

There is another button outside the list. When that is pressed, it should change the color of ALL the containers.

enter image description here

It feels like I need a Change/StateNotifierProvider for each container. Do I use families for this? How would I tie a particular piece of state to its associated container?

And how would the red button access all of the states to change the colour of all?

As a bonus, I would also like the red button to be notified when one of the green buttons changes the color of its container

Many thanks

DaveBound
  • 215
  • 4
  • 11

2 Answers2

8

You could use family, but in this case, since you have a non-fixed number of entries it would complicate things unnecessarily.

Here is a full runnable example written with hooks_riverpod. If you need me to translate to not use hooks I can do that too. Keep in mind this is intentionally simple and a bit naive, but should be adaptable to your situation.

First off, a model class. I would typically use freezed but that's out of scope for this question.

class Model {
  final int id;
  final Color color;

  Model(this.id, this.color);
}

Next, the StateNotifier:

class ContainerListState extends StateNotifier<List<Model>> {
  ContainerListState() : super(const []);

  static final provider = StateNotifierProvider<ContainerListState, List<Model>>((ref) {
    return ContainerListState();
  });

  void setAllColor(Color color) {
    state = state.map((model) => Model(model.id, color)).toList();
  }

  void setModelColor(Model model, Color color) {
    final id = model.id;
    state = state.map((model) {
      return model.id == id ? Model(id, color) : model;
    }).toList();
  }

  void addItem() {
    // TODO: Replace state.length with your unique ID
    state = [...state, Model(state.length, Colors.lightBlue)];
  }
}

Lastly, the UI components (hooks):

class MyHomePage extends HookWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final modelList = useProvider(ContainerListState.provider);
    return Scaffold(
      appBar: AppBar(
        title: Text('ListView of Containers'),
        actions: [
          IconButton(
            icon: Icon(Icons.add),
            onPressed: () {
              context.read(ContainerListState.provider.notifier).addItem();
            },
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: modelList.length,
        itemBuilder: (_, index) {
          return ContainerWithButton(model: modelList[index]);
        },
      ),
      floatingActionButton: RedButton(),
    );
  }
}

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

  final Model model;

  @override
  Widget build(BuildContext context) {
    return ListTile(
      tileColor: model.color,
      trailing: ElevatedButton(
        style: ElevatedButton.styleFrom(primary: Colors.lightGreen),
        onPressed: () {
          context.read(ContainerListState.provider.notifier).setModelColor(model, Colors.purple);
        },
        child: Text('Button'),
      ),
    );
  }
}

class RedButton extends HookWidget {
  const RedButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // Bonus: Red button will be notified on changes
    final state = useProvider(ContainerListState.provider);

    return FloatingActionButton.extended(
      onPressed: () {
        context.read(ContainerListState.provider.notifier).setAllColor(Colors.orange);
      },
      backgroundColor: Colors.red,
      label: Text('Set all color'),
    );
  }
}

Non-hooks:

class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final modelList = watch(ContainerListState.provider);
    return Scaffold(
      appBar: AppBar(
        title: Text('ListView of Containers'),
        actions: [
          IconButton(
            icon: Icon(Icons.add),
            onPressed: () {
              context.read(ContainerListState.provider.notifier).addItem();
            },
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: modelList.length,
        itemBuilder: (_, index) {
          return ContainerWithButton(model: modelList[index]);
        },
      ),
      floatingActionButton: RedButton(),
    );
  }
}

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

  final Model model;

  @override
  Widget build(BuildContext context) {
    return ListTile(
      tileColor: model.color,
      trailing: ElevatedButton(
        style: ElevatedButton.styleFrom(primary: Colors.lightGreen),
        onPressed: () {
          context.read(ContainerListState.provider.notifier).setModelColor(model, Colors.purple);
        },
        child: Text('Button'),
      ),
    );
  }
}

class RedButton extends ConsumerWidget {
  const RedButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    // Bonus: Red button will be notified on changes
    final state = watch(ContainerListState.provider);

    return FloatingActionButton.extended(
      onPressed: () {
        context.read(ContainerListState.provider.notifier).setAllColor(Colors.orange);
      },
      backgroundColor: Colors.red,
      label: Text('Set all color'),
    );
  }
}

I would recommend throwing this in a new Flutter app to test it out.

Alex Hartford
  • 5,110
  • 2
  • 19
  • 36
  • 1
    Thanks a million Alex, that's a great education for me. I'm still trying to understand the finer details esp as I haven't used hooks_riverpod before (so if you have time to show a non-hooks version that would also be super helpful). I see you are managing the state of the list as a whole (ContainerListState) as opposed to having an individual piece of state for each Container. Presumably, this means that when the button is tapped on ONE container, the entire LIST state is rebuilt. Intuitively, this feels inefficient (might it cause unnecessary widget rebuilds?) or am I worrying about nothing? – DaveBound Jul 15 '21 at 10:33
  • Reason for using `state = [...state, xxxx]` in addItem(): https://stackoverflow.com/a/65380308/10927806 – DaveBound Jul 15 '21 at 11:04
  • @DaveBound You are right, this would cause the entire list to rebuild. I wouldn't say this is the most efficient way, but it should be the simplest way. I would stray away from using family for this because you would have to track every family you have created. – Alex Hartford Jul 15 '21 at 13:28
  • Before I add the non-hooks version, can I ask if you are using `riverpod 0.14` or `1.0`? – Alex Hartford Jul 15 '21 at 13:28
  • flutter_riverpod: ^0.12.1 – DaveBound Jul 15 '21 at 14:34
  • Updated the answer with the non-hooks version of the UI components. – Alex Hartford Jul 15 '21 at 14:55
1

Alex's non-hooks version is a bit different with riverpod ^1.0.0

I changed one small thing independent of version: I moved the provider out of the class to global scope, both approaches work, the official docs show this version below.

class ContainerListState extends StateNotifier<List<Model>> {
  ContainerListState() : super(const []);
  // No static provider declaration in here
...
}
// Provider moved out here
final containerListProvider = StateNotifierProvider<ContainerListState, List<Model>>((ref) {
  return ContainerListState();
});

ProviderScope is necessary for the app to still be able to access the provider.

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

Changes from 0.12 to 1.0 regarding this question:

  1. No more context.read() for StatelessWidget -> Use ConsumerWidget
  2. WidgetRef ref and ref.watch() instead of ScopedReader watch

Non-hooks:

With riverpod ^1.0.4

class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final modelList = ref.watch(containerListProvider);
    return Scaffold(
      appBar: AppBar(
        title: Text('ListView of Containers'),
        actions: [
          IconButton(
            icon: Icon(Icons.add),
            onPressed: () {
              ref.read(containerListProvider.notifier).addItem();
            },
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: modelList.length,
        itemBuilder: (_, index) {
          return ContainerWithButton(model: modelList[index]);
        },
      ),
      floatingActionButton: RedButton(),
    );
  }
}

class ContainerWithButton extends ConsumerWidget {
  const ContainerWithButton({
    Key? key,
    required this.model,
  }) : super(key: key);

  final Model model;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ListTile(
      tileColor: model.color,
      trailing: ElevatedButton(
        style: ElevatedButton.styleFrom(primary: Colors.lightGreen),
        onPressed: () {
          ref
              .read(containerListProvider.notifier)
              .setModelColor(model, Colors.purple);
        },
        child: Text('Button'),
      ),
    );
  }
}

class RedButton extends ConsumerWidget {
  const RedButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // Bonus: Red button will be notified on changes
    final state = ref.watch(containerListProvider);

    return FloatingActionButton.extended(
      onPressed: () {
        ref.read(containerListProvider.notifier).setAllColor(Colors.orange);
      },
      backgroundColor: Colors.red,
      label: Text('Set all color'),
    );
  }
}