Flutter Provider Example Category Selector

In the latest blog posts, I have been writing about Flutter Hooks to simplify state management in Flutter. We described how to simplify the modal dialog selector of categories with Flutter Hooks. However, Flutter Hooks is not the only solution. Another option is to use a provider which is used in the simple app state management page of Flutter. We will rewrite the modal category dialog selector as an example, except that this time we will use the provider.

A look at the past

Before we start with rewriting the example, let’s take a quick look at the code that we are going to rewrite. There are three widgets:

  • Display of a list shows all selected items
  • Filtering of a list shows all items with a checkbox
  • Parent widget, managing the state of the two other Widgets

The SelectedCategories Widget displays the currently selected items. Those items are passed to the Widget as a List of Strings. For displaying the items, the ListView is used.

class SelectedCategories extends StatelessWidget {
  final List<String> categories;

  const SelectedCategories({Key key, this.categories}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Flexible(
      child: ListView.builder(
          itemCount: categories.length,
          itemBuilder: (BuildContext context, int index) {
            return Padding(
              padding: const EdgeInsets.all(8.0),
              child: Text(categories[index]),
            );
          }),
    );
  }
}

The CategoryFilter Widget displays all items with a checkbox. Those items are passed to the Widget as a List of Strings. For displaying the items, the ListView is used and a checkbox is added. The actions are passed to the parent Widget with a Callback.

class CategoryFilter extends StatelessWidget {
  final List<String> categories;
  final List<String> selected;
  final Function(String, bool) callback;
  const CategoryFilter({Key key, this.categories, this.selected, this.callback})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Flexible(
      child: ListView.builder(
          itemCount: categories.length,
          itemBuilder: (BuildContext context, int index) {
            return CheckboxListTile(
              value: selected.contains(categories[index]),
              onChanged: (bool selected) {
                callback(categories[index], selected);
              },
              title: Text(categories[index]),
            );
          }),
    );
  }
}

The CategorySelector Widget is the parent Widget. All actions are handled and through the setState function, both the Filter and the Overview are updated.

class CategorySelector extends StatefulWidget {
  final List<String> categories;

  CategorySelector(this.categories);

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

class _CategorySelectorState extends State<CategorySelector> {
  final selectedCategories = List<String>();

  handleChange(String name, bool selected) {
    setState(() {
      if (selected) {
        selectedCategories.add(name);
      } else {
        selectedCategories.remove(name);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        CategoryFilter(
          categories: widget.categories,
          selected: selectedCategories,
          callback: handleChange,
        ),
        Container(
          color: Colors.green,
          height: 2,
        ),
        SelectedCategories(
          categories: selectedCategories,
        )
      ],
    );
  }
}

Adding dependencies

Before we can start with coding, we are going to add a dependency to the project, namely provider. Their package page contains a lot of useful information and references to more use cases and examples

dependencies:
  provider: ^4.3.2+3

Do not forget to install the dependency, running the following command:

flutter pub get

That is it! We can now start with rewriting the example.

Rewriting the Example with Provider

With the provider there are two concepts that are important:

  • ChangeNotifier
  • Consumer

The goal of the change notifier is to notify listeners of changes. Consumers are listeners of the change notifiers and do something with the changes when notified. So in our example, we have a list of items. The SelectedCategories Widget is interested in the current state list of items. The CategoryFilter Widget is interested in updating and the current state of the list. So both the SelectedCategories and the CategoryFilter Widget will be consumers. We will create a new class that extends the ChangeNotifier so that both Widget can listen to updates of the list.

class CategoryList extends ChangeNotifier {
  CategoryList(List<String> categories) {
    categories.forEach((value) {
      _categories.putIfAbsent(value, () => false);
    });
  }

  final Map<String, bool> _categories = Map();

  List<String> get categories => _categories.keys.toList();

  List<String> get selectedCategories =>
      _categories.entries.where((e) => e.value).map((e) => e.key).toList();

  void toggle(String item) {
    _categories[item] = !_categories[item];
    notifyListeners();
  }
}

The CategoryList extends the ChangeNotifier. This means that it should tell other Widgets when something has changed. As you can see at the toggle, the notifyListeners is called which will trigger an update for each Consumer. The class is created with a list of categories. When the user presses the checkbox, the state of the category is changed. Let’s take a look at how to listen to those changes.

class SelectedCategories extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<CategoryList>(
      builder: (context, categoryList, child) {
        return Flexible(
          child: ListView.builder(
              itemCount: categoryList.selectedCategories.length,
              itemBuilder: (BuildContext context, int index) {
                return Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Text(categoryList.selectedCategories[index]),
                );
              }),
        );
      },
    );
  }
}

The build function is now consuming the CategoryList, so it will be updated when something changes. In the builder function, we can now access the CategoryList. Here we are interested in the selectedCategories, which we can access. The CategoryFilter is done in almost the same way. Except, when pressing the toggle we have to retrieve the CategoryList as we are not building anymore.

class CategoryFilter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<CategoryList>(builder: (context, categoryList, child) {
      return Flexible(
        child: ListView.builder(
            itemCount: categoryList.categories.length,
            itemBuilder: (BuildContext context, int index) {
              return CheckboxListTile(
                value: categoryList.selectedCategories
                    .contains(categoryList.categories[index]),
                onChanged: (bool selected) {
                  var categoriesModel = context.read<CategoryList>();
                  categoriesModel.toggle(categoryList.categories[index]);
                },
                title: Text(categoryList.categories[index]),
              );
            }),
      );
    });
  }
}

The current CategoryList can be retrieved with a simple context read. After that, we can call the toggle function on the CategoryList. There is one more thing we need to do, which is to provide the CategoryList. This can be done by wrapping the Widget with a ChangeNotifierProvider.

class MultipleCategorySelection extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.green,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Scaffold(
        appBar: AppBar(title: Text("Interactive categories")),
        body: ChangeNotifierProvider(
            create: (context) => CategoryList(
                ["Banana", "Pear", "Apple", "Strawberry", "Pineapple"]),
            child: Column(
              children: [
                CategoryFilter(),
                Container(
                  color: Colors.green,
                  height: 2,
                ),
                SelectedCategories()
              ],
            )),
      ),
    );
  }
}

We removed the original parent Widget of both classes, as it no longer needed. With the provider, we extracted the State Management to one class. This way we can keep all actions that update the state in one place and are the Widget only in control of displaying information. The full code can be found on Github. If you have any questions, feel free to ask them in the comments!

Leave a Reply