skip to content

With the provider package, how do you rebuild only a basket badge when the item count changes, not on every Basket notification?

level: middleimportance: should knowfreq 44%

answer

  1. narrow the value, then the scope
  2. select on count, not the Basket
  3. the dependent is the context's owner
  4. Selector<Basket, int> with a builder
  5. DeepCollectionEquality unless shouldRebuild

basics

~20 s

Put the badge in its own widget that calls context.select((Basket b) => b.count), or wrap it in Selector<Basket, int>. Either re-runs the selector on each notification and rebuilds only the badge when the count differs.

solid answer

~40 s

Two things must shrink: the dependency and its scope. Select the derived value, `context.select((Basket b) => b.count)`, instead of watching the whole `Basket`, and make the badge its own widget so the dependent element is the badge rather than the AppBar or screen. Inline, `Selector<Basket, int>(selector: (_, b) => b.count, builder: (context, count, child) => Badge(...), child: const Icon(...))` does the same with its own context. Both compare the previous and new selected values with `DeepCollectionEquality`, so an `int` is compared by value. `Selector` also accepts `shouldRebuild(previous, next)` to override that comparison. Taps on the badge should use `context.read<Basket>()` inside the callback, which adds no dependency.

code

dart · 42 lines
dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// Basket is a ChangeNotifier exposing int count.

class BasketBadge extends StatelessWidget {
  const BasketBadge({super.key, required this.onOpen});

  final VoidCallback onOpen;

  @override
  Widget build(BuildContext context) {
    // Rebuilds only when count changes, not on price or coupon updates.
    final count = context.select((Basket b) => b.count);
    return IconButton(
      onPressed: onOpen,
      icon: Badge(
        label: Text('$count'),
        isLabelVisible: count > 0,
        child: const Icon(Icons.shopping_basket),
      ),
    );
  }
}

class ShopAppBar extends StatelessWidget implements PreferredSizeWidget {
  const ShopAppBar({super.key, required this.onOpenBasket});

  final VoidCallback onOpenBasket;

  @override
  Size get preferredSize => const Size.fromHeight(kToolbarHeight);

  @override
  Widget build(BuildContext context) {
    // No Basket dependency here: the AppBar never rebuilds for the basket.
    return AppBar(
      title: const Text('Shop'),
      actions: [BasketBadge(onOpen: onOpenBasket)],
    );
  }
}

go deeper

for a junior

Recall that select or Selector let a widget depend on one field, like the count, instead of the whole Basket.

for a middle

Explain both halves, the narrowed value and the narrowed scope, and how DeepCollectionEquality and shouldRebuild decide whether the builder runs.

for a senior

Show you can profile and prove it: the badge rebuilds on add and remove, not on price refreshes, and no ancestor watches Basket just to pass a count down.

for a principal

Weigh fine-grained selects against model design: splitting a busy notifier into smaller providers can remove the need for many selectors across a large app.

## The problem A shop screen's `AppBar` shows a basket icon with a count badge. The `Basket` is a `ChangeNotifier` exposed by `ChangeNotifierProvider`, and it notifies on every change: an item added, a quantity edited, a price refreshed, a coupon applied. If the screen's `build` calls `context.watch<Basket>()` to get the count, the **whole screen** rebuilds on each of those notifications, although the badge cares about one integer. Two changes fix it, and both are needed: 1. **Narrow the dependency** from the whole `Basket` to the derived value `count`. 2. **Narrow the scope** so the dependent element is the badge, not the AppBar or the screen. ## Option A: context.select in a small widget `context.select((Basket b) => b.count)` subscribes the calling element with an **aspect**. On each notification the provider package re-runs the selector synchronously and compares the new count with the one returned at the last build; only a difference marks the widget dirty. Because the badge is its own widget class, the `context` passed to `select` is the badge's element. Put the same `select` in the method that builds the AppBar and the **entire** method re-runs on a count change: the dependency attaches to whichever element owns the `context`, not to the widget that happens to use the value. ## Option B: Selector inline `Selector<A, S>` is the widget form. Its `selector` receives the context and the provided `A` and returns `S`; its `builder` receives the context, the selected `S` and an optional `child`, exactly like `Consumer`. It brings its own `BuildContext`, so it can sit inline in the AppBar's `actions` without a new class: ```dart Selector<Basket, int>( selector: (_, basket) => basket.count, builder: (context, count, child) => Badge( label: Text('$count'), isLabelVisible: count > 0, child: child, ), child: const Icon(Icons.shopping_basket), ) ``` The two differ slightly underneath. `Selector` reads the provider with a listening `Provider.of`, so its element is marked dirty on every notification; its build then runs the selector and, when the result compares equal, returns the **cached** widget from the previous build, so nothing below it is rebuilt. `context.select` does its comparison during the notification itself, before anything is marked dirty. ## How 'changed' is decided | | Default comparison | Override | |---|---|---| | `context.select` | `DeepCollectionEquality` | none | | `Selector` | `DeepCollectionEquality` | `shouldRebuild: (previous, next) => ...` | `DeepCollectionEquality` (package:collection) compares lists, maps, sets and iterables **by content** and everything else with `==`. An `int` count is compared by value, which is what the badge needs. `shouldRebuild` receives the previous and next selected values and returns `true` when `builder` should run; it is not consulted on the first build. Use it when the default is wrong for your data: - you select a large immutable list and every change produces a new list, so `(previous, next) => !identical(previous, next)` avoids a deep comparison on each notification; - the badge caps its label at '99+', so only a change in the displayed label warrants a rebuild. ## Checklist for the badge - The selector returns a value, not the notifier: `b.count`, not `b`. - The dependent is the smallest widget that shows the value. - Static parts, like the icon, go through `child` or are `const`. - Taps call `context.read<Basket>()` inside the callback, adding no dependency. - Nothing else on the screen watches `Basket` merely to pass a count down. ## Why not Consumer? `Consumer<Basket>` around the badge would narrow the **scope** but not the **dependency**: its builder would re-run on every basket notification, including price refreshes that leave the count unchanged. It is the right tool when the badge needs several fields that all change together; for one derived value, `select` or `Selector` is the precise fit. ## Verifying the fix Flutter DevTools can show rebuild counts per widget. After the change, adding or removing an item should rebuild the badge once, a price refresh or coupon change should not rebuild it, and the AppBar should not rebuild in either case. A widget test can assert the same thing by counting builder calls around a `notifyListeners()` that leaves `count` alone. In an interview, name both halves: select the smallest value, and subscribe from the smallest widget. Then mention how equality is decided, because that is where a later bug usually hides.

  • What is the mechanical difference between Selector and context.select when the provider notifies?
    `Selector` listens with `Provider.of`, so its element is marked dirty on every notification; during its build it re-runs the selector and returns the cached widget if the value compares equal. `context.select` registers an aspect: the package runs the selector during the notification and only marks the widget dirty if the result changed.
  • When would you pass shouldRebuild to a Selector for the badge?
    When the default deep comparison is not the rule you want: for example, the label caps at '99+', so going from 120 to 121 need not rebuild; or the selected value is a large immutable list where an identity check, `!identical(previous, next)`, is cheaper than comparing contents.

context.select is like asking a courier to ring only when the number of parcels on your order changes: the courier still checks every status update, but only disturbs you when your number is different.

saying these in an interview costs you the question

  • context.select in the AppBar's build rebuilds only the badge that uses the count
  • Selector compares selected values with identical() by default
  • Consumer<Basket> around the badge skips rebuilds when the count is unchanged
  • shouldRebuild returns true to skip the rebuild
  • Selecting the whole Basket object is as good as selecting count