Flutter - Using ListenableBuilder Widget Examples

This tutorial shows you how to use the ListenableBuilder widget in Flutter.

Flutter layout consists of widgets in a tree structure. In certain conditions, the layout needs to be updated. Ideally, the application should only rebuild the widgets that need to be updated in order to improve the performance. Therefore, you should properly write the code to avoid unnecessary widget rebuilds. In this tutorial, I am going to explain about a widget that can be useful if you want to handle

Using ListenableBuilder

Below is the constructor of ListenableBuilder.

  const ListenableBuilder({
    Key? key,
    required Listenable listenable,
    required Widget build(BuildContext context) builder,
    Widget? child,
  })

There are two required arguments, listenable and builder. For the listenable argument, you have to pass a value whose type is Listenable. The ListenableBuilder builder listens to the passed listenable. When the listenable object changes, it will notify its listeners. The ListenableBuilder will trigger the function passed as the builder argument when it's notified by its listenable

Listenable is an abstract class or interface that maintains a list of listeners. The listeners are the objects to be notified when the object that implements Listenable changes. There are several ways to create the Listenable object. Below are the examples.

Using ChangeNotifier

The first way is by creating a class that uses the ChangeNotifier mixin. ChangeNotifier is a class that implements Listenable. It can be extended or mixed. When there is an event that triggers to update a widget, you need to call the notifyListeners function.

  class MyCounter with ChangeNotifier {
  
    int _count = 0;
    int get count => _count;
  
    void increment() {
      _count++;
      notifyListeners();
    }
  }

Then, create an instance of the class above and pass it as the listenable argument of the ListenableBuilder.

  class MyContent extends StatefulWidget {
  
    const MyContent({super.key});
  
    @override
    State<MyContent> createState() => _MyContentState();
  }
  
  class _MyContentState extends State<MyContent> {
  
    final MyCounter _counter = MyCounter();
  
    @override
    Widget build(BuildContext context) {
      return SizedBox(
        width: double.infinity,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ListenableBuilder(
              listenable: _counter,
              builder: (BuildContext context, Widget? child) {
                print('builder');
                return Column(
                  children: [
                    Text('${_counter.count}'),
                  ],
                );
              },
            ),
            OutlinedButton(
              onPressed: () {
                _counter.increment();
              },
              child: const Text('Increment'),
            ),
          ],
        ),
      );
    }
  }

In the example above, there is a button that triggers the increment method of the MyCounter instance when pressed. Since the increment method calls notifyListeners, the ListenableBuilder will be notified every time the button is pressed. When it happens, the function passed as the builder argument will be triggered. It only rebuilds the widget returned by the builder argument. It will not rebuild the entire _MyContentState widget.

Using ValueNotifier

Another way to define a Listenable is by using a ValueNotifier, which is a ChangeNotifier that holds a single value. With this way, you just need to create an instance of ValueNotifier, no need to create a custom ChangeNotifier class.

Below is the adjusted _MyContentState class that uses a ValueNotifier.

  class _MyContentState extends State<MyContent> {
  
    final ValueNotifier<int> _counterValueNotifier = ValueNotifier<int>(0);
  
    @override
    Widget build(BuildContext context) {
      print('_MyContentState - build');
      return SizedBox(
        width: double.infinity,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ListenableBuilder(
              listenable: _counterValueNotifier,
              builder: (BuildContext context, Widget? child) {
                print('builder');
                return Column(
                  children: [
                    Text('${_counterValueNotifier.value}'),
                  ],
                );
              },
            ),
            OutlinedButton(
              onPressed: () {
                _counterValueNotifier.value++;
              },
              child: const Text('Increment'),
            ),
          ],
        ),
      );
    }
  }

Performance Optimization by Passing child Argument

Sometimes, the builder widget may return a widget subtree that doesn't depend on the listenable. If we can avoid rebuilding the subtree, it would improve the performance of the application. The solution is by passing a widget as the child argument.

The widget passed as the child argument will be passed as the second argument of the function passed as the builder argument. Therefore, it's possible to reuse the widget instead of building a new one.

For example, we want to add the following widget as a subtree returned by the builder function.

  class CounterTitle extends StatelessWidget {
  
    CounterTitle({super.key});
  
    @override
    Widget build(BuildContext context) {
      print('CounterTitle - build');
      return const Text('COUNTER');
    }
  }

Below is the example with the child argument passed.

  class _MyContentState extends State<MyContent> {
  
    final ValueNotifier<int> _counterValueNotifier = ValueNotifier<int>(0);
  
    @override
    Widget build(BuildContext context) {
      print('_MyContentState - build');
      return SizedBox(
        width: double.infinity,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ListenableBuilder(
              listenable: _counterValueNotifier,
              builder: (BuildContext context, Widget? child) {
                print('builder');
                return Column(
                  children: [
                    child!,
                    Text('${_counterValueNotifier.value}'),
                  ],
                );
              },
              child: CounterTitle(),
            ),
            OutlinedButton(
              onPressed: () {
                _counterValueNotifier.value++;
              },
              child: const Text('Increment'),
            ),
          ],
        ),
      );
    }
  }

Summary

In this tutorial, we have learned how to use the ListenableBuilder widget to control which widgets should be rebuilt. Basically, it needs a Listenable, which is an object that can notify when it's updated. When notified, the builder function will be invoked and hence rebuild the widget subtree. If the builder function contains a subtree that doesn't depend on the Listenable, you can pass it as the child argument which will make it passed as the second argument of the builder function.