Flutter Tutorial

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 ListenableBuilder for your own models (ChangeNotifier, ValueNotifier).
  • Use AnimatedBuilder for 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

ValueListenableBuilderListenableBuilder
Listens toA ValueListenable<T> (for example ValueNotifier)Any Listenable
Builder getsThe current valueNothing: read the model yourself
Best forOne valueModels 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, in initState, or in a provider.
  • Not disposing. Call dispose() on notifiers and controllers you create inside a State.

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.

Practise in the playground Updated