Skip to content

Every provider change rebuilds every watching screen #523

Description

@richardthe3rd

Summary

State is well decomposed behind BeerProviderDrinkFilterController, FestivalController, UserDrinkStateController, UserPreferencesController are clean, Flutter-free and individually testable. But the provider flattens all four back into a single notification channel, and the widget tree is wired to depend on that channel wholesale. A search keystroke, a theme change and a favourite toggle all produce the same rebuild.

Root cause

Two independent mechanisms, both of which have to change for either to help.

1. Screens subscribe to the whole provider. Every screen opens build() with a bare context.watch<BeerProvider>():

  • lib/screens/drinks_screen.dart:42
  • lib/screens/drink_detail_screen.dart, my_festival_screen.dart, brewery_screen.dart, style_screen.dart, festival_info_screen.dart, about_screen.dart (one each)

There is no use of context.select or Selector anywhere in lib/, and only three Consumer uses (lib/widgets/drink_filter_sheets.dart:43,226,378). BeerProvider has ~68 public members and 28 notifyListeners() call sites, so any of the 28 rebuilds all of the above. Concretely: changing theme mode re-runs the whole SliverChildBuilderDelegate in _buildDrinksListSliver and rebuilds every visible DrinkCard.

2. Widgets take BeerProvider as a constructor field — 18 sites:

  • lib/widgets/festival_header.dart:8
  • lib/widgets/festival_banner.dart:10
  • lib/widgets/festival_menu_sheets.dart:44,424,491
  • lib/widgets/drink_filter_sheets.dart:150

A const constructor holding a mutable ChangeNotifier means those widgets can never rebuild on their own — they are entirely dependent on an ancestor's watch. This is the inverse of what provider is for, and it means fixing (1) alone would break them. It also makes them untestable without constructing a full provider.

Fix approach

Incremental, one screen at a time — not a sweep (see AGENTS.md on this repo's history with sweeping UI changes).

  1. Drop the BeerProvider provider constructor parameter from the six widgets above; have each read what it needs via context.select<BeerProvider, T>(...) at its own build boundary. Keep the parameter vestigial for one pass if call sites are out of scope, then delete (AGENTS.md "File manifests hide cross-file API coupling").
  2. Replace top-level context.watch<BeerProvider>() in each screen with narrow context.select reads, or Selector around the subtrees that actually depend on a field.
  3. drinks_screen.dart first — it has the largest rebuild surface and the most measurable win.

Widget-level rebuild boundaries (_buildX helper methods) are a related but separate concern; tracked separately.

Verification

Existing widget tests should pass unchanged. Add a rebuild-count assertion on DrinkCard for a theme-mode change, which currently rebuilds every card and should rebuild none.

Files: lib/screens/*.dart, lib/widgets/festival_header.dart, lib/widgets/festival_banner.dart, lib/widgets/festival_menu_sheets.dart, lib/widgets/drink_filter_sheets.dart

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions