Understanding Flutter Hooks useListenable and useListenableSelector
Tags: flutter, dart, programming, tutorial
Published: 2022-09-29 16:32:43


## Introduction

Have you ever wondered how you can update your UI when there are data changes without explicitly writing listeners for those data? In this article, you will learn what the `flutter_hooks` package is and how to get these desired results. We will explore the "useListenable" and "useListenableSelector" methods the package provides us.


[`flutter_hooks`](https://pub.dev/packages/flutter_hooks) is a flutter package that gives you access to pre-written hooks. Hooks are objects that help you manage the lifecycle of a widget as well as increase code reusability of widgets. An example is when we use a `TextEditingController` or an `AnimationController`, we would have to write the `initState` logic, call it in the required place in the build method and then dispose of it. `flutter_hooks` helps simplify that process. It gives us an easier way of managing all of that while ensuring there are no memory leaks by disposing it when no longer in use.

Flutter hooks, according to the documentation, are grouped into 2 types: 
1. Primitive hooks
2. Object-binding hooks

The Object-binding hooks are further divided into 4 types:
1. Dart:async related hooks
2. Animation related hooks
3. Listenable related hooks
4. Misc hooks

For the sake of this article, we will only describe Listenable related hooks.
**Listenable related hooks** are hooks that have functions relating to Listenable data. These functions can be either subscribing to or creating Listenables. The hooks under these are
1. useListenable
2. useListenableSelector
3. useValueNotifier
4. useValueListenable

## What are useListenable and useListenableSelector methods?
_useListenable_ and _useListenableSelector_ are two of the four listenable related hooks which are used to trigger rebuilds when the state of a listenable data is changed. The difference between the two is that while _useListenable_ triggers a rebuild every time the listener is called, _useListenableSelector_ allows you to specify the value of the Listenable that should trigger a rebuild. _useListenableSelector_ listens to the listenable data but gives you the ability to select when the rebuild should happen.

You have been seeing _Listenable_ a lot. You must be wondering what it is.

## What is Listenable 
If you have been into Flutter for a while, you might have noticed some widgets have the `addListener` and `removeListener` methods, those widgets are Listenables. Listenable is an interface that provides those two methods. There are classes that implement this Listenable class, such as `ChangeNotifier` and `ValueNotifier`. In summary, any class that extends or implements either the `ChangeNotifier` or `ValueNotifier` class are by inheritance Listenables. Example is `TextEditingController` which extends `ValueNotifier`.

I will be showing the implementation using a two screen layout made with stateless widgets so we don't have to call `setState`. The first screen will have a text field and a button which only shows when the user inputs **"Flutter Hooks"**. The second screen will have a single button that toggles the theme of the screen between light and dark modes. Now let's dive into the actual implementation.

## Flutter Implementation
First step is to create the flutter project. 
```
flutter create hooks_example
```

Next, we go to the `pubspec.yaml` file in the project root folder and add the `flutter_hooks` package to it. The dependencies section of our `pubspec.yaml` will then look like this

```
dependencies:
  cupertino_icons: ^1.0.2
  flutter:
    sdk: flutter
  flutter_hooks: ^0.18.5+1
```

The latest version of flutter_hooks at the time of creating this article was `0.18.5+1`. Next we create the two files in the lib folder `first_screen.dart` and `second_screen.dart`.
Starting with the first screen we add the following code to `first_screen.dart`. 

```
import 'package:flutter/material.dart';

import 'second_screen.dart';

class FirstScreen extends StatelessWidget {
  const FirstScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.blueGrey,
        title: const Text("Hooks - First Screen"),
      ),
      body: Column(
        children: [
          const Expanded(
            flex: 3,
            child: Center(
              child: Padding(
                padding: EdgeInsets.all(8.0),
                child: TextField(
                  decoration: InputDecoration(
                    labelText: "Enter Text...",
                    enabledBorder: OutlineInputBorder(),
                    focusedBorder: OutlineInputBorder(),
                  ),
                ),
              ),
            ),
          ),
          Expanded(
            flex: 2,
            child: Center(
              child: OutlinedButton(
                onPressed: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(builder: (_) => const SecondScreen()),
                  );
                },
                style: OutlinedButton.styleFrom(
                  padding:
                      const EdgeInsets.symmetric(horizontal: 25, vertical: 20),
                  backgroundColor: Colors.blueGrey,
                ),
                child: const Text(
                  "Proceed",
                  style: TextStyle(
                    color: Colors.white,
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

```

Note that there will be an error under the `SecondScreen()` class, this is because we have not created the second screen yet. Not to worry, we will get to that now. The code below is then added to `second_screen.dart`

```
import 'package:flutter/material.dart';

class SecondScreen extends StatelessWidget {
  const SecondScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        automaticallyImplyLeading: true,
        backgroundColor: Colors.blueGrey,
        title: const Text("Hooks - Second Screen"),
      ),
      body: Center(
        child: OutlinedButton(
          onPressed: () {},
          style: OutlinedButton.styleFrom(
            padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 20),
            backgroundColor: Colors.blueGrey,
          ),
          child: const Text(
            "Change Theme",
            style: TextStyle(
              color: Colors.white,
            ),
          ),
        ),
      ),
    );
  }
}

```

After this, the error in the first screen should be gone. Next we change our `main.dart` to look like this.

```
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Hooks Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const FirstScreen(),
    );
  }
}
```

Here, we clean up the default code the project comes with upon creation and change the value of the home property of `MaterialApp` to the `FirstScreen` widget. When we run the code, we will have both screens looking like:
![Image of the first screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ymes4mhe7p2sxq7esscf.png) ![Image of the second screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o03pykz712kz5dpq85pg.png)

Now that we have our base screens, let us proceed to add the logic to the first screen. We replace the different sections of the code to look like below.

```
// 1. 
class FirstScreen extends HookWidget {
  const FirstScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
// 2.  
   final controller = useTextEditingController();
// 3. 
    final buttonIsVisible = useListenableSelector(
        controller, () => controller.text == "Flutter Hooks");
    return Scaffold(

'''

'''
// 4. 
              child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: TextField(
                  controller: controller,
                  decoration: const InputDecoration(
                    labelText: "Enter Text...",
                    enabledBorder: OutlineInputBorder(),
                    focusedBorder: OutlineInputBorder(),
                  ),
                ),
              ),
'''

'''
// 5.    
         child: Visibility(
              visible: buttonIsVisible,
              child: Center(
                child: OutlinedButton(
                  onPressed: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(builder: (_) => const SecondScreen()),
                    );
                  },
                  style: OutlinedButton.styleFrom(
                    padding: const EdgeInsets.symmetric(
                        horizontal: 25, vertical: 20),
                    backgroundColor: Colors.blueGrey,
                  ),
                  child: const Text(
                    "Proceed",
                    style: TextStyle(
                      color: Colors.white,
                    ),
                  ),
                ),
              ),
            ),
'''
```

What happened here is that we 
1. changed StatelessWidget to HookWidget to be able to use the different hooks
2. created a text controller using one of the Misc hooks
3. created a listenableSelector to listen for changes to the text controller but only rebuild when the selector result changes from true to false and vise versa
4. added the controller to the text field
5. then made the button visibility dependent on the `TextEditingValue` of the text controller using the `Visibility` widget.


For the second widget we make similar adjustments to the existing code and the code should look like this.

```
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

ValueNotifier<bool> isDarkMode = ValueNotifier(true);

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

  @override
  Widget build(BuildContext context) {
    useListenable(isDarkMode);
    return Scaffold(
      backgroundColor: isDarkMode.value ? Colors.grey[900] : Colors.white,
      appBar: AppBar(
        automaticallyImplyLeading: true,
        backgroundColor: isDarkMode.value ? Colors.grey : Colors.blueGrey,
        title: const Text("Hooks - Second Screen"),
      ),
      body: Center(
        child: OutlinedButton(
          onPressed: () {
            isDarkMode.value = !isDarkMode.value;
          },
          style: OutlinedButton.styleFrom(
            padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 20),
            backgroundColor: isDarkMode.value ? Colors.grey : Colors.blueGrey,
          ),
          child: const Text(
            "Change Theme",
            style: TextStyle(
              color: Colors.white,
            ),
          ),
        ),
      ),
    );
  }
}

```

Here, we 
1. create a global simple ValueNotifier variable `isDarkMode` which holds a boolean value, it is made global so we can have a constant constructor for the `SecondScreen` class.
2. make use of a ternary operator to specify the theme colors everywhere we need the theme change to reflect. 
3. also added a function that toggles the `isDarkMode` value to the `onPressed` property of the `Change Theme` button.


Now that we are done with the "dirty work", our final result will be this 
![Final result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q1h9eab2510115urtzzc.gif)

We notice that the first screen is only rebuilt when the value of `buttonIsValid `changes while the second screen rebuilds every time the `isDarkMode` value changes.

You can check out the complete code [here](https://github.com/Lord-Chris/hooks_example)

## Conclusion
You have learnt a summary about `flutter_hooks`, what `Listenables` are, how `useListenable` and `useListenableSelector` can be used to customize and limit rebuilds of widgets, and when to use one over the other. Check out the official [flutter_hooks](https://pub.dev/packages/flutter_hooks) and [flutter](https://docs.flutter.dev/) documentation for further study.

If you have any questions, feel free to reach out to me on Twitter: [@lord_chris__](https://twitter.com/lord_chris__) or LinkedIn: [@lord-chris](https://linkedin.com/in/lord-chris).