skip to content

In the provider package, what problems does the Consumer widget solve, and what does its child parameter optimise?

level: middleimportance: should knowfreq 48%

answer

  1. a new BuildContext below the provider
  2. Provider.of in its own build
  3. builder(context, value, child)
  4. child built once, passed through
  5. Consumer2 to Consumer6

basics

~20 s

Consumer<T> calls Provider.of<T> with its own context, giving a reader below a provider created in the same build and limiting rebuilds to its builder. Its child is built once and handed back unchanged, so that subtree is reused.

solid answer

~40 s

`Consumer<T>` takes a required `builder(context, value, child)` and an optional `child`. Its `build` just calls `Provider.of<T>(context)` with its own `BuildContext` and passes the result to `builder`. That solves two problems. First, when a widget creates a provider and wants to read it in the same `build`, its own context sits above the provider and the lookup throws `ProviderNotFoundException`; the Consumer's context is below it. Second, it narrows rebuilds: only the builder re-runs on a notification, not the whole enclosing `build`. The `child` parameter goes further: a subtree that does not depend on `T` is built once outside `builder` and passed back in, so Flutter sees the same widget instance and skips rebuilding it. `Consumer2` to `Consumer6` read several types at once.

code

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

// Basket is a ChangeNotifier exposing int count.

class PromoBanner extends StatelessWidget {
  const PromoBanner({super.key, required this.code});

  final String code;

  @override
  Widget build(BuildContext context) => Text('Use $code at checkout');
}

class BasketBar extends StatelessWidget {
  const BasketBar({super.key, required this.promoCode});

  final String promoCode;

  @override
  Widget build(BuildContext context) {
    return Consumer<Basket>(
      builder: (context, basket, child) {
        return Row(
          children: [
            child!, // same PromoBanner instance on every rebuild
            Text('${basket.count} items'),
          ],
        );
      },
      child: PromoBanner(code: promoCode),
    );
  }
}

go deeper

for a junior

Recall the builder signature (context, value, child) and that Consumer gives you a context below a provider you just created.

for a middle

Explain how the child argument is built once and passed through, and why Flutter skips rebuilding an identical widget instance.

for a senior

Show when Consumer is the wrong tool: it scopes rebuilds but never filters them, so value-level filtering needs Selector or select.

for a principal

Set a guideline for when a team prefers inline Consumers versus extracted widget classes, weighing readability, const-ness and review clarity.

## What Consumer is `Consumer<T>` is a small widget from the provider package. Its constructor takes an optional `key`, a required `builder` and an optional `child`; the builder's signature is `Widget Function(BuildContext context, T value, Widget? child)`. Its build does one thing: it calls `Provider.of<T>(context)` with **its own** `BuildContext` and hands the result to `builder`. The package documentation says plainly that it 'doesn't do any fancy work'. Variants `Consumer2` to `Consumer6` read two to six provider types and pass each value as its own builder argument, with `child` always last. Because `Provider.of<T>(context)` listens by default, a Consumer behaves like a `context.watch<T>()` whose rebuild is **scoped to the Consumer's builder**. That one fact gives it both of its purposes. ## Purpose 1: a context below the provider A `BuildContext` only sees providers **above** its widget. A common mistake is to create a provider and read it in the same `build` method: ```dart @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (_) => Basket(), child: Text('${Provider.of<Basket>(context).count}'), ); } ``` This `context` belongs to the widget whose `build` is running, an **ancestor** of the `ChangeNotifierProvider`, so the lookup finds nothing and throws `ProviderNotFoundException`. Wrapping the reader in `Consumer<Basket>` fixes it, because the Consumer's own context sits **below** the provider. A `Builder` or a separate widget class works for the same reason. ## Purpose 2: narrower rebuilds When a widget calls `context.watch<T>()` in its `build`, the **whole** build method re-runs on every notification and every widget it constructs is recreated. If only a small part of that tree depends on `T`, wrapping just that part in `Consumer<T>` moves the dependency down: the parent's `build` no longer subscribes, and only the builder re-runs. ## The child parameter Sometimes the widget that depends on `T` **wraps** a subtree that does not. Rebuilding the wrapper through `builder` would reconstruct the inner subtree too. The `child` parameter avoids that: 1. The independent subtree is built **once**, where the Consumer itself is constructed. 2. On each rebuild, the Consumer passes that same instance to `builder` as its third argument. 3. `builder` places it in the new tree. When Flutter updates the element at that position and finds the **identical** widget instance, it skips rebuilding that subtree. So when `Basket` notifies in the code example, the builder's own widgets (a `Row` and a `Text`) are recreated while the `PromoBanner` instance is reused. `child` is typed `Widget?` in the builder because the parameter is optional, hence the `!` when you know you passed one. A `const` child already gets this for free, because a `const` expression is canonicalised to one instance. The `child` parameter matters most for **non-const** subtrees, such as a widget built from runtime arguments. ## Consumer inside MultiProvider Consumer extends `SingleChildStatelessWidget` from the `nested` package, the same base the providers use, so it can appear in `MultiProvider(providers: [...])`. The documentation's rule there: it **must** return the `child` passed to `builder` somewhere in the tree it creates, because that child carries the rest of the nesting. ## Consumer next to the other readers | Tool | Subscribes to | What re-runs on a notification | Filters by value | |---|---|---|---| | `context.watch<T>()` in `build` | whole `T` | the calling widget's `build` | no | | `Consumer<T>` | whole `T` | the Consumer's `builder` | no | | `context.select` in `build` | selector result | the calling widget's `build`, if the result changed | yes | | `Selector<T, S>` | selector result | the Selector's `builder`, if the result changed | yes | Key takeaways: - A Consumer narrows **where** a rebuild happens, not **whether** it happens: every notification from `T` still runs `builder`. - To filter by value, use `Selector` or `context.select`. - Extracting a small widget class that calls `context.watch` gives the same scoping as a Consumer; the Consumer is simply the inline form. - The `child` optimisation composes with all of this: `Selector` takes a `child` with the same meaning. ## Mistakes reviewers catch - Building the would-be `child` inside `builder` anyway, which throws the optimisation away. - Passing `child` but never placing it in the returned tree, so the subtree silently disappears. - Wrapping an entire screen in one `Consumer`, which is no narrower than a screen-level `watch`. - Expecting a Consumer to skip rebuilds when the fields its builder reads are unchanged; it cannot see which fields the builder uses.

  • Does wrapping a widget in Consumer reduce how often the builder runs?
    No. A Consumer listens to the whole value, so every notification from the provider runs its builder. It reduces how much is rebuilt, by moving the dependency from a large build method to a small builder. To skip rebuilds when the relevant value is unchanged, use `Selector` or `context.select`.
  • When is a Consumer preferable to extracting a small widget class that calls context.watch?
    They scope rebuilds the same way. A Consumer is handy inline, especially right under a provider created in the same build, or when a non-const subtree should pass through `child`. A named widget class reads better when the dependent part is reused or large, and it can take a `const` constructor.

Consumer's child is like a prefabricated wall panel delivered to a renovation: each time the room is redone around it, the builders slot the same panel back in instead of building a new one.

saying these in an interview costs you the question

  • Consumer only rebuilds when the fields its builder uses change
  • The child passed to Consumer is rebuilt on every notification anyway
  • Provider.of in the same build that creates the provider works because create runs first
  • Consumer needs listen: false to avoid rebuilding its parent
  • A Consumer inside MultiProvider may ignore the child argument