In Flutter, when we want to rebuild the UI inside a Stateful widget after updating a variable, we use setState(). setState() re-runs the build method and the widget is rebuilt with the updated variable value. For a simple UI, setState() may be enough as a state management solution but when the widget tree is large and complex, rebuilding the whole widget tree may prove to be costly. This impacts the app performance.
This is where ValueNotifier comes in. A ValueNotifier is a ChangeNotifier that holds a single value. It is also inbuilt into Flutter. ValueNotifier, with ValueListenableBuilder will only rebuild a specific part of the widget tree instead of rebuilding the whole widget tree.
Example of ValueNotifier
Let us first create a Stateful widget and in initState, initialize a ValueNotifier that holds an int value and intialize the value to 0. Incase we do not specify type, it can hold variable of any type. We also need to dispose the ValueNotifier to free up the memory allocated for it.
class ValueNotifierDemo extends StatefulWidget {
const ValueNotifierDemo({Key? key}) : super(key: key);
@override
State<ValueNotifierDemo> createState() => _ValueNotifierDemoState();
}
class _ValueNotifierDemoState extends State<ValueNotifierDemo> {
late final ValueNotifier<int> _currentValue;
@override
void initState() {
super.initState();
_currentValue = ValueNotifier(0);
}
@override
void dispose() {
_currentValue.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container();
}
}
Now, let us create a ValueListenableBuilder which listens to the changes in ValueNotifier value and rebuilds the UI.
ValueListenableBuilder(
valueListenable: _currentValue,
child: const Text(
'Value: ',
style: TextStyle(
fontSize: 30,
),
),
builder: (context, int value, child) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
child!,
Text(
'$value',
style: TextStyle(
fontSize: 30,
),
),
],
);
},
),
ValueListenableBuilder takes three parameters, i) a valueListenable which takes the ValueNotifier object which it listens to, ii) a builder which builds the UI and iii) a child widget which takes a widget that does not depend on the value of valueListenable. The first two parameters are required but the child parameter is optional. However, using the child parameter could improve the app performance as it does not need to be rebuilt when valueListenable updates.
The builder also takes three parameters, i) a build context, ii) value, whose value the ValueListenableBuilder depends upon and iii) the child widget which we passed as parameter in ValueListenableBuilder. When we update the valueListenable value, only this builder method is re-run whereas using setState(), Flutter re-runs the build method.
Also notice that we pass a Text widget to child parameter which does not get rebuilt.
Finally, we define two buttons to manipulate the int value. One to decrease the value by 1 and another to increase the value by 1.
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
MaterialButton(
onPressed: () => _currentValue.value--,
color: Colors.red,
elevation: 0,
child: Icon(
Icons.remove,
size: 35.0,
color: Colors.white,
),
padding: EdgeInsets.all(15.0),
shape: CircleBorder(),
),
MaterialButton(
onPressed: () => _currentValue.value++,
color: Colors.green,
elevation: 0,
child: Icon(
Icons.add,
size: 35.0,
color: Colors.white,
),
padding: EdgeInsets.all(15.0),
shape: CircleBorder(),
)
],
),
Full Code
import 'package:flutter/material.dart';
class ValueNotifierDemo extends StatefulWidget {
const ValueNotifierDemo({Key? key}) : super(key: key);
@override
State<ValueNotifierDemo> createState() => _ValueNotifierDemoState();
}
class _ValueNotifierDemoState extends State<ValueNotifierDemo> {
late final ValueNotifier<int> _currentValue;
@override
void initState() {
super.initState();
_currentValue = ValueNotifier(0);
}
@override
void dispose() {
_currentValue.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Counter'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
child: ValueListenableBuilder(
valueListenable: _currentValue,
child: const Text(
'Value: ',
style: TextStyle(
fontSize: 30,
),
),
builder: (context, int value, child) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
child!,
Text(
'$value',
style: TextStyle(
fontSize: 30,
),
),
],
);
},
),
),
SizedBox(height: 50),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
MaterialButton(
onPressed: () => _currentValue.value--,
color: Colors.red,
elevation: 0,
child: Icon(
Icons.remove,
size: 35.0,
color: Colors.white,
),
padding: EdgeInsets.all(15.0),
shape: CircleBorder(),
),
MaterialButton(
onPressed: () => _currentValue.value++,
color: Colors.green,
elevation: 0,
child: Icon(
Icons.add,
size: 35.0,
color: Colors.white,
),
padding: EdgeInsets.all(15.0),
shape: CircleBorder(),
)
],
),
],
),
);
}
}
Result
Finally, we built a simple counter app to learn to use ValueNotifier. We can see the value increasing and decreasing on respective button click.
I hope this article helped to give you a proper introduction of ValueNotifier and when to use it.