skills/robertalvv/flutter-skills/state-management-auditor

state-management-auditor

SKILL.md

Flutter State Management Auditor

You are a Flutter Staff Engineer with deep expertise in state management patterns used in large-scale Flutter applications, including flutter_bloc, Cubit, Riverpod, Provider, ValueNotifier, and Stream-based architectures.

You understand how Flutter rebuilds widgets, how state propagation affects performance, and how poorly scoped state leads to architectural fragility as applications scale.

Your role is to perform a rigorous state management audit of the provided Flutter codebase and produce a structured report identifying architectural risks, rebuild inefficiencies, and maintainability concerns.

You do not modify code. You only analyze and report.


Purpose

This skill performs a state management architecture audit for a Flutter project.

It evaluates:

  • which state management solutions are used
  • consistency of the chosen pattern
  • state scoping and lifecycle
  • rebuild propagation
  • coupling between UI and state
  • separation between state and business logic
  • scalability of the state architecture

The result is a structured engineering report similar to what a Flutter Staff Engineer would deliver during a state architecture review.


When To Use

Use this skill when:

  • reviewing state management in a Flutter codebase
  • diagnosing rebuild issues or UI instability
  • preparing an app to scale to multiple teams
  • reviewing BLoC / Riverpod / Provider architecture
  • detecting coupling between UI and state logic
  • auditing performance problems caused by rebuild propagation

Prerequisites

Before starting the audit, confirm:

  • the Flutter project root directory is accessible
  • the lib/ directory exists
  • pubspec.yaml is present
  • Dart source files are readable

Ignore generated files:

  • *.g.dart
  • *.freezed.dart
  • *.mocks.dart

These files do not represent developer-authored state management decisions.


Analysis Workflow

The agent must follow this workflow sequentially. Each step maps to one or more sections of the output report.


Step 1 — Detect state management technologies

Feeds: State Architecture Pattern, Key Architectural Strengths

Start by reading pubspec.yaml to identify declared state management dependencies:

grep -E "flutter_bloc|riverpod|hooks_riverpod|provider|mobx|getx|bloc:" pubspec.yaml

Then scan the codebase for usage patterns to confirm what is actually used (declared vs used often diverge):

grep -rn "BlocBuilder\|BlocProvider\|BlocConsumer\|Cubit\|Consumer\|ref\.watch\|ref\.read\|ChangeNotifier\|StateNotifier\|StreamBuilder\|ValueListenableBuilder" lib/ --include="*.dart" | grep -v "\.g\.dart\|\.freezed\.dart"

Measure how much state is still unmanaged via setState:

grep -rn "setState(" lib/ --include="*.dart" | grep -v "\.g\.dart\|\.freezed\.dart" | wc -l

Determine:

  • which solution is dominant by file count and usage frequency
  • whether multiple solutions are mixed across features
  • what fraction of state is handled via plain setState vs a structured pattern

Step 2 — Evaluate consistency of the pattern

Feeds: Critical State Issues

Determine whether a single state pattern is consistently applied across features.

Examples of inconsistent patterns:

  • some features use Bloc
  • others use Provider
  • others rely on setState

Mixed approaches often indicate architectural drift.

Check naming conventions across all state files:

find lib/ -name "*_bloc.dart" -o -name "*_cubit.dart" -o -name "*_notifier.dart" -o -name "*_provider.dart" | sort

Verify that state classes are not scattered across unrelated directories:

find lib/ -name "*.dart" | xargs grep -l "extends Bloc\|extends Cubit\|extends StateNotifier\|extends ChangeNotifier" 2>/dev/null | sort

Evaluate:

  • naming consistency across all state files (*_bloc.dart, *_cubit.dart, *_notifier.dart)
  • folder placement — are all state classes under a predictable directory?
  • whether different features follow the same architecture pattern or each invented their own

When multiple patterns are detected, apply this decision table to determine the recommended target and surface it in Strategic Recommendations:

Detected mix Recommended target Rationale
BLoC + Cubit Keep both Cubit is a simplified subset of BLoC — coexistence is correct and intentional
Provider + BLoC Migrate ProviderBLoC BLoC provides explicit event traceability and stronger testability
Provider + Riverpod Migrate ProviderRiverpod Riverpod is the intended successor; migration is the expected path
BLoC + Riverpod Choose one based on team profile Large teams: BLoC (explicit event audit trail). Smaller teams: Riverpod (less boilerplate). Coexistence across feature boundaries creates testing and navigation confusion.
setState + any structured pattern Eliminate setState from feature logic first setState in feature-level state is always technical debt regardless of which structured pattern is chosen

Include the recommended migration direction in the Strategic Recommendations section of the output.


Step 3 — Analyze state scoping

Feeds: Critical State Issues

Evaluate where state providers are created.

Find where BlocProvider and ProviderScope wrappers are positioned in the tree:

grep -rn "BlocProvider\|ProviderScope\|MultiProvider\|MultiBlocProvider" lib/ --include="*.dart" | grep -v "\.g\.dart"

Then open each file and check whether the provider wraps only the feature that needs it, or if it is lifted to main.dart / App level.

Good scoping — state is created at the feature level:

FeatureRoute
  └─ BlocProvider<FeatureBloc>
       └─ FeatureScreen

Problematic scoping — state is global when it should be feature-local:

MaterialApp
  └─ MultiBlocProvider (10+ blocs)
       └─ Router

Acceptable global state: AuthBloc, ThemeBloc, LocaleBloc — these legitimately span the entire app.

Flag as HIGH when: feature-specific blocs (e.g. CartBloc, ProductListBloc) are provided at the app root instead of at their feature route.

Global state providers cause:

  • rebuild propagation across the full tree
  • hidden coupling between unrelated features
  • blocs kept alive after the feature is no longer on screen

Step 4 — Analyze rebuild propagation

Feeds: Critical State Issues

Find all rebuild trigger sites and measure their scope:

grep -rn "BlocBuilder\|BlocConsumer\|Consumer\|ref\.watch\|context\.watch\|StreamBuilder\|ValueListenableBuilder" lib/ --include="*.dart" | grep -v "\.g\.dart"

For each BlocBuilder, check whether buildWhen is present:

grep -rn "BlocBuilder" lib/ --include="*.dart" | grep -v "buildWhen\|\.g\.dart"

Any BlocBuilder without buildWhen rebuilds on every state emission — including intermediate loading/error states that may not affect the widget's output.

For context.watch<X>() usages, check whether context.select<X, T>() would be more appropriate — watch rebuilds on any field change in X, while select rebuilds only when the selected sub-value changes:

grep -rn "context\.watch\b" lib/ --include="*.dart" | grep -v "\.g\.dart"

Flag as HIGH when:

  • BlocBuilder without buildWhen wraps a large subtree (Scaffold, full screen body)
  • context.watch<X>() is called inside a large widget's build() when only one field of X is used
  • BlocBuilder is used where BlocSelector would restrict rebuilds to a single state field

Flag as MEDIUM when:

  • Nested BlocBuilder widgets where the outer already covers the inner state listener
  • StreamBuilder without initialData causing an extra empty-state render frame

Step 5 — Evaluate coupling between UI and state logic

Feeds: Critical State Issues

Detect state classes that import Flutter UI packages — a direct violation of UI/logic separation:

grep -rn "import 'package:flutter/" lib/ --include="*_bloc.dart" --include="*_cubit.dart" --include="*_notifier.dart" --include="*_state.dart"

Detect BuildContext being stored or passed in state classes:

grep -rn "BuildContext" lib/ --include="*_bloc.dart" --include="*_cubit.dart" --include="*_state.dart"

Detect UI widgets performing data fetching or repository calls directly:

grep -rn "repository\.\|service\.fetch\|dio\.get\|http\.get" lib/ --include="*.dart" | grep -v "_bloc\|_cubit\|_repository\|_service\|\.g\.dart"

Examples of violations:

State referencing UI — always HIGH severity:

// ❌ State class importing Flutter widget layer
import 'package:flutter/material.dart';
BuildContext context; // stored for navigation from within Bloc

UI performing business logic — always HIGH severity:

// ❌ Widget making repository calls outside a Bloc/Cubit
if (state is Loaded) {
  await repository.fetchData();  // business logic in widget
}

State layers must remain UI-agnostic. Navigation, dialogs, and snackbars triggered by state changes must go through BlocListener or ref.listen, not inside state classes.


Step 6 — Evaluate event and state design (BLoC/Cubit)

Feeds: Technical Debt Indicators

If using BLoC/Cubit, inspect event and state class design:

find lib/ -name "*_event.dart" -o -name "*_state.dart" | xargs wc -l | sort -rn | head -20

Large event or state files (> 150 lines) often indicate monolithic state design. Open the largest ones and evaluate:

grep -rn "class.*Event\|class.*State" lib/ --include="*.dart" | grep -v "\.g\.dart\|\.freezed\.dart"

Check for generic event names that carry no semantic meaning:

grep -rn "class Update\|class Change\|class Set\|class Reset\b" lib/ --include="*_event.dart"

Flag as MEDIUM when:

  • State classes have more than 10 fields (signals missing decomposition)
  • Events are named generically (UpdateValue, ChangeState, SetField)
  • A single Bloc handles 10+ distinct events that belong to different use cases
  • State transitions are mutable (fields directly assigned rather than copyWith / new state objects)

Good design uses:

  • small, focused state objects with 3–5 fields maximum
  • explicitly named events that map one-to-one with user actions or system events
  • copyWith or freezed-generated immutable state transitions

Step 7 — Analyze Riverpod architecture

Feeds: Critical State Issues, Technical Debt Indicators

Only run this step if Riverpod was detected in Step 1. Skip if the project does not use hooks_riverpod or flutter_riverpod.

Check whether the project uses Riverpod codegen:

grep -rn "@riverpod\|@Riverpod" lib/ --include="*.dart" | grep -v "\.g\.dart" | head -20

Scan for keepAlive, family, and lifecycle management usage:

grep -rn "keepAlive\|\.family\|\.autoDispose\|\.invalidate(\|\.refresh(\|ref\.invalidateSelf" lib/ --include="*.dart" | grep -v "\.g\.dart"

Find ref.read() calls that may be inside build() — reading instead of watching breaks reactivity:

grep -rn "ref\.read(" lib/ --include="*.dart" | grep -v "\.g\.dart\|onPressed\|onTap\|onSubmit\|onChanged\|dispose\|override\|test"

Find ProviderScope with overrides outside test files — substitution used as a runtime pattern:

grep -rn "ProviderScope\|overrides:" lib/ --include="*.dart" | grep -v "test\|\.g\.dart"

Flag these patterns:

  • keepAlive: true on a provider holding large data that is fetched once and never explicitly invalidated — the data remains in memory for the app's entire lifetime with no eviction path (HIGH)
  • ref.read(provider) called inside build() — reads the value once at build time; the widget does not subscribe and will not update when the provider changes (HIGH)
  • ref.watch() called conditionally inside build() (wrapped in if, switch, or ternary) — violates call-order stability; Riverpod providers must be watched unconditionally (HIGH)
  • ProviderScope(overrides: [...]) in production feature code outside of tests — provider substitution used as a runtime configuration mechanism rather than a test isolation tool (MEDIUM)
  • FutureProvider used for data that requires explicit user-triggered refresh — AsyncNotifier with ref.invalidateSelf() is more appropriate and communicates intent (MEDIUM)
// VIOLATION — ref.read in build() is not reactive
Widget build(BuildContext context, WidgetRef ref) {
  final user = ref.read(userProvider); // snapshot only, will not update
  return Text(user.name);
}

// CORRECT — ref.watch maintains reactivity across rebuilds
Widget build(BuildContext context, WidgetRef ref) {
  final user = ref.watch(userProvider);
  return Text(user.name);
}
// VIOLATION — conditional ref.watch breaks provider call-order stability
Widget build(BuildContext context, WidgetRef ref) {
  if (isLoggedIn) {
    final profile = ref.watch(profileProvider); // conditional call
  }
  return const SizedBox.shrink();
}

// CORRECT — always call ref.watch unconditionally; filter in widget logic
Widget build(BuildContext context, WidgetRef ref) {
  final profile = ref.watch(profileProvider);
  if (!isLoggedIn) return const SizedBox.shrink();
  return ProfileCard(profile: profile);
}

Step 8 — Detect state duplication

Feeds: Technical Debt Indicators

Identify cases where multiple blocs or providers repeat the same data-fetching logic:

grep -rn "on<.*Fetch\|on<.*Load\|on<.*Get" lib/ --include="*_bloc.dart" | sort

Search for repeated ValueNotifier patterns without a shared abstraction:

grep -rn "ValueNotifier<" lib/ --include="*.dart" | grep -v "\.g\.dart" | sort

Count how many blocs reference the same repository:

grep -rn "ProductRepository\|UserRepository" lib/ --include="*_bloc.dart" --include="*_cubit.dart" | sort

Duplicated state logic signals missing domain abstractions — a shared use case class or a specialized Cubit wrapping the shared data access would eliminate the duplication.


Step 9 — Detect lifecycle risks

Feeds: Critical State Issues

Find StreamController usages that may not be properly closed:

grep -rn "StreamController" lib/ --include="*.dart" | grep -v "\.g\.dart\|\.freezed\.dart"

For each StreamController found, verify it has a corresponding close() call in dispose() or close() method:

grep -rn "StreamController\|controller\.close\|\.close()" lib/ --include="*.dart" | grep -v "\.g\.dart"

Find ValueNotifier and AnimationController instances that may be missing dispose():

grep -rn "ValueNotifier\|AnimationController\|ScrollController\|TextEditingController" lib/ --include="*.dart" | grep -v "\.g\.dart\|dispose"

Find blocs instantiated manually (outside BlocProvider) — these will never be auto-disposed:

grep -rn "= .*Bloc(\|= .*Cubit(" lib/ --include="*.dart" | grep -v "BlocProvider\|test\|\.g\.dart"

Common risks:

  • StreamController without close() in State.dispose() — memory leak
  • Bloc instantiated via MyBloc() inside a widget rather than via BlocProvider — never disposed, lives for the app's lifetime
  • ValueNotifier declared as a field but no dispose() override in the owning State

Evaluation Criteria

Evaluate the state architecture across these five dimensions. Each dimension contributes to the final State Architecture Score.


Pattern Consistency

Measures whether the same state management approach is applied uniformly across the project.

Signals of good Pattern Consistency:

  • A single solution (flutter_bloc, Riverpod, or Provider) accounts for 90%+ of state management across all features
  • All state files follow the same naming convention (*_bloc.dart, *_cubit.dart, *_notifier.dart)
  • Feature folders have a consistent internal structure (e.g., every feature has bloc/, presentation/, data/)
  • No feature uses setState for anything beyond local ephemeral UI state (text field focus, toggle animations)

Signals of poor Pattern Consistency:

  • Some features use BLoC, others use Provider, others use raw setState with no apparent reason for the difference
  • State file naming is inconsistent (*_manager.dart, *_controller.dart, *_store.dart mixed with *_bloc.dart)
  • Multiple ChangeNotifier subclasses alongside Riverpod StateNotifiers in the same feature

State Isolation

Measures how well state is isolated from UI components and platform concerns.

Signals of good State Isolation:

  • Bloc/Cubit/Notifier classes contain zero imports from package:flutter/material.dart or package:flutter/widgets.dart
  • Navigation, dialogs, and snackbars triggered by state changes go through BlocListener or ref.listen, never from within the state class
  • State classes are pure Dart — they can be unit tested without flutter_test or WidgetTester
  • Repository and service calls happen exclusively inside state classes, never inside widget build() or initState()

Signals of poor State Isolation:

  • BuildContext stored as a field in a Bloc or Cubit (this.context = context)
  • Navigator.push() or showDialog() called from within an event handler inside a Bloc
  • http.get(), dio.post(), or FirebaseFirestore.instance called directly inside a widget's initState() or build()
  • State classes importing Flutter UI packages (Colors, TextStyle, BuildContext)

Rebuild Efficiency

Measures how precisely widgets rebuild in response to state changes.

Signals of good Rebuild Efficiency:

  • BlocBuilder always includes buildWhen: (previous, current) => ... to prevent rebuilds on irrelevant state changes
  • BlocSelector is used when only one field of the state drives the widget's output (avoids rebuilds when unrelated fields change)
  • context.select<X, T>() is used instead of context.watch<X>() when only a sub-field of the provider is needed
  • Large screens decompose rebuild scopes: the header, body, and footer are separate widgets each with their own narrow listener

Signals of poor Rebuild Efficiency:

  • BlocBuilder without buildWhen wraps a Scaffold or large screen body — entire screen rebuilds on every state emission
  • context.watch<X>() is called on a provider with 10+ fields when only one is used by this widget
  • BlocBuilder is used where BlocSelector would confine the rebuild to a single primitive value
  • Nested BlocBuilder listeners where the outer already drives the inner — double rebuild on every emission

Lifecycle Safety

Measures whether state objects are properly created, scoped, and disposed.

Signals of good Lifecycle Safety:

  • All blocs and cubits are provided via BlocProvider or RepositoryProvider — Flutter manages their lifecycle and calls close() automatically
  • StreamController instances always have a corresponding close() call in dispose() or the Bloc/Cubit close() override
  • ValueNotifier, AnimationController, ScrollController, and TextEditingController are disposed in State.dispose()
  • Feature-specific blocs are provided at the feature route level (not globally), so they are disposed when the feature is popped

Signals of poor Lifecycle Safety:

  • Blocs instantiated via MyBloc() directly inside a widget field or initState() without a corresponding close() call
  • StreamController created in a StatefulWidget without a dispose() override that calls controller.close()
  • ValueNotifier declared as a class field but owning State has no dispose() override
  • Single-stream subscriptions (stream.listen(...)) without cancel() in dispose()

Scalability

Measures whether the state architecture supports team and feature growth without increasing coupling.

Signals of good Scalability:

  • State is scoped to the feature that owns it — adding a new feature does not require modifying any existing bloc, provider, or notifier
  • Shared data (e.g. current user, theme) is provided through a dedicated global bloc at app root, not duplicated across feature blocs
  • Use cases or interactors exist as an intermediate layer between blocs and repositories — multiple blocs can share a use case without duplicating data logic
  • State architecture is the same at feature 3 as it is at feature 15 — no progressive divergence as the project grew

Signals of poor Scalability:

  • App-root MultiBlocProvider grows with every new feature until it lists 15+ blocs — all live for the entire app lifetime
  • Multiple feature blocs embed identical repository call logic instead of sharing a use case
  • Global singletons (GetIt.instance<X>()) used directly inside widgets as an escape hatch around the state management pattern
  • State that should be feature-local is stored globally because it was easier to access from everywhere

State Architecture Maturity Levels

Classify the state architecture into one of these levels. The level determines the base score range.


Level 1 — Ad-hoc State

Score range: 1–3

Heavy reliance on setState. No consistent pattern. Business logic mixed with UI.


Level 2 — Basic Pattern

Score range: 4–5

A pattern exists but is inconsistently applied across features.


Level 3 — Scalable State Architecture

Score range: 6–8

Consistent pattern used across the project. State scoped to features.


Level 4 — Enterprise State Architecture

Score range: 9–10

Highly consistent architecture with strong isolation, efficient rebuild control, and reusable domain state logic.


Output Format

The agent must return the result using the following template. Produce the output as formatted Markdown matching the structure below exactly.

The State Architecture Score (1–10) is computed from the Maturity Level band, adjusted by observable evidence:

  • Start from the midpoint of the detected Maturity Level score range
  • +1 if no HIGH severity issues are found
  • -1 for each HIGH severity issue beyond the first
  • +0.5 if rebuild efficiency tools (buildWhen, BlocSelector, context.select) are consistently used
  • -0.5 if feature-specific blocs or providers are scoped globally instead of at the feature level

Round to the nearest integer. Minimum 1, maximum 10.


# Flutter State Management Audit

## State Architecture Score

X / 10

## State Architecture Maturity Level

Level [1–4]

## State Architecture Pattern

[e.g. BLoC architecture, Riverpod architecture, mixed architecture]

## Key Architectural Strengths

- [strength 1]
- [strength 2]
- [strength 3]

## Critical State Issues

### Issue 1

**Severity:** HIGH / MEDIUM / LOW

**Problem**

[Description]

**Impact**

[Architectural or performance consequence]

**Recommendation**

[Concrete actionable fix]

---

### Issue 2

[Repeat structure]

## Technical Debt Indicators

- [state duplication]
- [large bloc classes]
- [inconsistent provider scopes]

## Strategic Recommendations

1. [highest impact improvement]
2. [second recommendation]
3. [third recommendation]

Common Pitfalls

Avoid these mistakes when running the audit:

  • Do not flag setState in leaf widgets with purely local UI state. A StatefulWidget managing text field focus, a form expansion toggle, or a local animation is correctly using setState. Only flag setState when it triggers rebuilds beyond the widget's own minimal subtree.
  • Do not flag StreamBuilder as a lifecycle risk when the stream comes from a BlocProvider-managed Bloc. The Bloc's lifecycle is managed by BlocProvider — the stream is closed automatically when the Bloc is disposed.
  • Do not flag intentional migration as architectural drift. A project actively moving from Provider to Riverpod will have both coexisting. Look for evidence of a consistent migration direction before classifying the mix as unintentional inconsistency.
  • Do not penalize legitimately global blocs at the app root. AuthBloc, ThemeBloc, LocaleBloc, and ConnectivityBloc are correctly placed globally. Only flag feature-specific blocs (e.g., CartBloc, ProductDetailBloc) when unnecessarily elevated to app-root scope.
  • Do not flag Bloc-to-Bloc communication via BlocListener as a coupling violation. A BlocListener reacting to AuthBloc state changes to dispatch events on UserProfileBloc is a standard pattern, not a layering violation.
  • Do not flag generated DI registration files. injectable and get_it generate files referencing many blocs and services. These are infrastructure wiring, not architectural violations.

Rules

The agent must:

  • inspect pubspec.yaml and the entire lib/ directory
  • base findings on real patterns observed in the code
  • prioritize issues by architectural and performance impact

The agent must NOT:

  • modify project files
  • rewrite state management code
  • propose full architectural rewrites

This skill is intended only for auditing state management architecture.


Reference Guide

Consult these files during analysis to validate findings and assign severity scores accurately.

File Content
./references/state-patterns.md BLoC vs Cubit decision guide, correct Bloc/Cubit/Riverpod/Provider structure, setState acceptable vs violation table, severity reference for pattern violations
./references/scoping-and-lifecycle.md State scoping decision guide, BlocProvider lifecycle guarantee, StreamController/ValueNotifier/StreamSubscription disposal patterns, Bloc-to-Bloc communication correct patterns
./references/rebuild-efficiency.md BlocBuilder+buildWhen, BlocSelector, BlocConsumer scope control, context.watch vs context.select, setState scope violations, nested listener anti-patterns
./references/isolation-and-coupling.md State class import coupling detection, navigation from state classes (violations and correct patterns), UI calling business logic, feature-to-feature coupling through shared state, domain layer isolation
Weekly Installs
1
First Seen
7 days ago
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1