Flutter Hooks useState example

In the previous blog post, we described how to create a dialog for multiple category selection. To do this we had to create two stateful widgets. We also had to override the initState method of the dialog. In React this can be done more easily with Hooks. Luckily there are also Hooks in Flutter that can do the same. In this blog post, we will show you how to rewrite the dialog example with Hooks.

Configuring the project

Before we can start with coding, we are going to add a dependency to the project, namely Flutter Hooks. For this we have to update our pubspec.yaml with the following dependency:

dependencies:
  flutter_hooks: ^0.15.0

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

flutter pub get

That is it! We can now use Flutter Hooks in the project.

Make the application use useState

Before we start, let’s take a look at the previous Stateful Widget. Since we have StatefulWidget, we pass any variables to the StatefulWidget. The class that extends the State can access those values through the widget. We have a list variable that maintains the current selection and use the setState to update the state of the Widget and trigger a repaint.

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

  CategorySelector(this.categories);

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

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

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        RaisedButton(
          child: Text("Select Fruit"),
          onPressed: () async {
            List<String> categories = await showDialog(
              context: this.context,
              child: new Dialog(
                child: CategorySelectorDialog(
                    widget.categories, List.from(selectedCategories)),
              ),
            );
            setState(() {
              selectedCategories.clear();
              selectedCategories.addAll(categories);
            });
          },
        ),
        Container(
          color: Colors.green,
          height: 2,
        ),
        SelectedCategories(
          categories: selectedCategories,
        )
      ],
    );
  }
}

The first thing we do is to merge both classes and have that class extend the HookWidget. We move the current selection to the build method and here we are going to call the hook: useState(List<String>)(). This means that when the values changes, it will have the same effect as when that would be done in the setState method. So we can remove the setState method and set the value directly:

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

  CategorySelector(this.categories);
  @override
  Widget build(BuildContext context) {
    final selectedCategories = useState(List<String>());
    return Column(
      children: [
        RaisedButton(
          child: Text("Select Fruit"),
          onPressed: () async {
            List<String> selection = await showDialog(
              context: context,
              child: new Dialog(
                child: CategorySelectorDialog(
                    categories, List.from(selectedCategories.value)),
              ),
            );
            selectedCategories.value = selection;
          },
        ),
        Container(
          color: Colors.green,
          height: 2,
        ),
        SelectedCategories(
          categories: selectedCategories.value,
        )
      ],
    );
  }
}

Now let’s take a look at the dialog. The biggest difference was the initState method that was called. In the initState we would add the current selection to the selected values so that the user does not have to remember which categories he already picked. With the hooks, this is no longer needed, since we can pass the current selection as the initial value for the current selection.

class CategorySelectorDialog extends HookWidget {
  final List<String> categories;
  final List<String> currentSelection;

  CategorySelectorDialog(this.categories, this.currentSelection);

  @override
  Widget build(BuildContext context) {
    final selectedCategories = useState(currentSelection);
    return Column(
      children: [
        Flexible(
          child: ListView.builder(
              itemCount: categories.length,
              itemBuilder: (BuildContext context, int index) {
                return CheckboxListTile(
                  value: selectedCategories.value.contains(categories[index]),
                  onChanged: (bool selected) {
                    if (selected) {
                      selectedCategories.value += [categories[index]];
                    } else {
                      selectedCategories.value.remove(categories[index]);
                      selectedCategories.value =
                          List.from(selectedCategories.value);
                    }
                  },
                  title: Text(categories[index]),
                );
              }),
        ),
        RaisedButton(
          onPressed: () {
            Navigator.pop(context, currentSelection);
          },
          child: Text("Cancel"),
        ),
        RaisedButton(
          onPressed: () {
            Navigator.pop(context, selectedCategories.value);
          },
          child: Text("Done"),
        )
      ],
    );
  }
}

In rewriting this simple category selection we removed more than 10% of the code lines. This does not mean that fewer lines of code are always better, but the lines of code we removed did not add much value and even made it less obvious what was going on. Thanks for reading and if you have any questions. Feel free to ask them! The full code of this example can be found on Github.

Leave a Reply