Flutter Intermediate Lesson 41 of 135 3 min read
ListenableBuilder and AnimatedBuilder in Flutter: Rebuild Only What Changes
Use ListenableBuilder with ChangeNotifier and AnimatedBuilder with animations to rebuild only part of the screen. Easy runnable Flutter examples.
On this page
ListenableBuilder and AnimatedBuilder do the same job: they listen to something that changes and rebuild only their own small part of the screen. The rest of the page is not rebuilt.
- Use
ListenableBuilderfor your own models (ChangeNotifier,ValueNotifier). - Use
AnimatedBuilderfor animations (AnimationController,Animation).
Under the hood they are almost the same widget. The two names exist so your code reads clearly.
ListenableBuilder with a ChangeNotifier
import 'package:flutter/material.dart';
class CartModel extends ChangeNotifier {
final List<String> _items = [];
List<String> get items => List.unmodifiable(_items);
int get count => _items.length;
void add(String item) {
_items.add(item);
notifyListeners(); // tell every listener to rebuild
}
void clear() {
_items.clear();
notifyListeners();
}
}
final cart = CartModel();
void main() => runApp(const MaterialApp(home: ShopPage()));
class ShopPage extends StatelessWidget {
const ShopPage({super.key});
static const products = ['Apple', 'Banana', 'Mango', 'Orange'];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Shop'),
actions: [
// Only this badge rebuilds when the cart changes.
ListenableBuilder(
listenable: cart,
builder: (context, child) => Padding(
padding: const EdgeInsets.all(12),
child: Badge(
label: Text('${cart.count}'),
isLabelVisible: cart.count > 0,
child: const Icon(Icons.shopping_cart),
),
),
),
],
),
body: ListView(
children: [
for (final p in products)
ListTile(
title: Text(p),
trailing: IconButton(
icon: const Icon(Icons.add_shopping_cart),
onPressed: () => cart.add(p),
),
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: cart.clear,
icon: const Icon(Icons.delete),
label: const Text('Empty cart'),
),
);
}
}
Tap the cart buttons. The page is a StatelessWidget, yet the badge updates, because ListenableBuilder listens to cart and rebuilds only the badge.
The child parameter: build once, reuse
If part of the builder never changes, build it once and pass it as child. It is handed back to the builder every time, without being rebuilt.
ListenableBuilder(
listenable: cart,
child: const Icon(Icons.shopping_cart, size: 32), // built once
builder: (context, child) {
return Badge(label: Text('${cart.count}'), child: child);
},
)
This matters most for expensive widgets inside animations.
Listening to several things at once
Listenable.merge combines several listenables into one:
ListenableBuilder(
listenable: Listenable.merge([cart, settings]),
builder: (context, child) => Text('${cart.count} items in ${settings.currency}'),
)
AnimatedBuilder
An AnimationController is a Listenable that changes 60 or more times a second. AnimatedBuilder rebuilds just the animated part on every frame.
import 'dart:math' as math;
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: SpinPage()));
class SpinPage extends StatefulWidget {
const SpinPage({super.key});
@override
State<SpinPage> createState() => _SpinPageState();
}
class _SpinPageState extends State<SpinPage> with SingleTickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AnimatedBuilder')),
body: Center(
child: AnimatedBuilder(
animation: _controller,
child: const FlutterLogo(size: 120), // built once
builder: (context, child) => Transform.rotate(
angle: _controller.value * 2 * math.pi,
child: child,
),
),
),
);
}
}
The logo is built once and passed as child; only the Transform is rebuilt each frame. More in Explicit animations.
ListenableBuilder vs ValueListenableBuilder
ValueListenableBuilder | ListenableBuilder | |
|---|---|---|
| Listens to | A ValueListenable<T> (for example ValueNotifier) | Any Listenable |
| Builder gets | The current value | Nothing: read the model yourself |
| Best for | One value | Models with several fields |
Common mistakes
- Forgetting
notifyListeners(). The data changes but nothing rebuilds. - Creating the model inside
build. A new model is made on every rebuild and the data is lost. Create it once: at the top level, ininitState, or in a provider. - Not disposing. Call
dispose()on notifiers and controllers you create inside aState.
Try it yourself
Add a total price to CartModel (give each product a price in a Map) and show it at the bottom of the screen in a second ListenableBuilder. Then use Listenable.merge to show the count and total in one place.