With the provider package, how do you rebuild only a basket badge when the item count changes, not on every Basket notification?
answer
- narrow the value, then the scope
- select on count, not the Basket
- the dependent is the context's owner
- Selector<Basket, int> with a builder
- DeepCollectionEquality unless shouldRebuild
basics
~20 sPut 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 sTwo 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 linesimport '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
Recall that select or Selector let a widget depend on one field, like the count, instead of the whole Basket.
Explain both halves, the narrowed value and the narrowed scope, and how DeepCollectionEquality and shouldRebuild decide whether the builder runs.
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.
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