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.