DaveBound
DaveBound

Reputation: 225

How best to use Riverpod to manage items in a list

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

Upvotes: 8

Views: 9117

Answers (2)

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'),
    );
  }
}

Upvotes: 3

Alex Hartford
Alex Hartford

Reputation: 6000

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.

Upvotes: 9

Related Questions