managing-flutter-provider-state — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited managing-flutter-provider-state (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Implement state management and dependency injection using the provider package in Flutter applications. Provider is built on Flutter's native InheritedWidget and remains the foundation for many production apps worldwide. This guide enforces MVVM (Model-View-ViewModel) architecture patterns and eliminates unnecessary widget rebuilds through targeted state consumption.
Before implementing Provider, determine if the state truly needs to be shared:
StatefulWidget + setState() for UI state confined to a single widget (e.g., form input, animation progress, current tab index).If uncertain about the scope, ask the user to clarify the intended lifecycle and accessibility requirements.
dependencies:
provider: ^6.1.5#### A. Create the Model Layer (Data / Repository) Handle low-level data operations (HTTP requests, database queries, caching):
class UserRepository {
Future<User> fetchUser(String id) async {
// API call or database query
return User(id: id, name: 'John Doe');
}
}Constraints: Models MUST NOT reference ChangeNotifier, BuildContext, or any UI code.
#### B. Create the ViewModel Layer Extend ChangeNotifier to hold UI state and expose commands:
class UserViewModel extends ChangeNotifier {
final UserRepository _repository;
UserViewModel(this._repository);
User? _user;
User? get user => _user;
bool _isLoading = false;
bool get isLoading => _isLoading;
String? _errorMessage;
String? get errorMessage => _errorMessage;
// Command invoked by View
Future<void> loadUser(String id) async {
_isLoading = true;
_errorMessage = null;
notifyListeners(); // Trigger loading UI
try {
_user = await _repository.fetchUser(id);
} catch (e) {
_errorMessage = e.toString();
} finally {
_isLoading = false;
notifyListeners(); // Trigger success/error UI
}
}
}Critical: Always call notifyListeners() after state mutations to trigger widget rebuilds.
#### C. Provide State to Widget Tree Use MultiProvider to inject dependencies at the root or page level:
void main() {
runApp(
MultiProvider(
providers: [
Provider(create: (_) => UserRepository()),
ChangeNotifierProvider(
create: (context) => UserViewModel(
context.read<UserRepository>(),
),
),
],
child: const MyApp(),
),
);
}Best Practice: Provide state as close to its usage point as possible. Avoid putting every provider at the app root if only used in specific screens.
#### D. Consume State in View Layer
For reading state in `build()` method:
Use Consumer or Selector to minimize rebuild scope:
class UserProfileView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Profile')),
body: Consumer<UserViewModel>(
builder: (context, viewModel, child) {
if (viewModel.isLoading) {
return Center(child: CircularProgressIndicator());
}
if (viewModel.errorMessage != null) {
return Center(child: Text('Error: ${viewModel.errorMessage}'));
}
if (viewModel.user != null) {
return Center(child: Text('Hello, ${viewModel.user!.name}'));
}
return Center(child: Text('No user loaded'));
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<UserViewModel>().loadUser('123'),
child: Icon(Icons.refresh),
),
);
}
}For executing commands (event handlers, callbacks):
Use context.read<T>() with listen: false:
// ✅ CORRECT: No rebuild triggered on button widget
onPressed: () => context.read<CartModel>().addItem(item),
// ❌ WRONG: Causes infinite rebuild loops
onPressed: () => Provider.of<CartModel>(context).addItem(item),#### ⚠️ CRITICAL: .value vs create Constructor
This is the #1 cause of memory leaks in Provider 6.x applications.
Provider offers two constructors for providing objects. Understanding when to use each is essential:
Rule:
create for objects YOU create and manage (Provider will call dispose() automatically).value ONLY for pre-existing instances you manage elsewhere (Provider will NOT call dispose())Anti-pattern (Memory Leak):
// ❌ WRONG: Creates a new instance but .value won't dispose it!
ChangeNotifierProvider.value(
value: MyNotifier(), // MEMORY LEAK - never disposed!
child: MyWidget(),
)Why it leaks: The .value constructor assumes you're passing in an instance that already exists and will be disposed elsewhere. Creating a new instance here means it will never be cleaned up.
Correct Pattern:
// ✅ CORRECT: create constructor manages lifecycle
ChangeNotifierProvider(
create: (_) => MyNotifier(),
child: MyWidget(),
)When to actually use `.value`:
// ✅ CORRECT: Reusing an existing instance from parent scope
final existingNotifier = context.watch<MyNotifier>();
return ChangeNotifierProvider.value(
value: existingNotifier, // Pre-existing instance
child: ChildWidget(),
)#### ChangeNotifier Disposal Checklist
Every ChangeNotifier subclass MUST implement dispose() and clean up all resources:
class MyViewModel extends ChangeNotifier {
StreamSubscription? _subscription;
Timer? _timer;
TextEditingController? _controller;
MyViewModel() {
_subscription = someStream.listen(_onData);
_timer = Timer.periodic(Duration(seconds: 1), _onTick);
_controller = TextEditingController();
}
@override
void dispose() {
// Cancel ALL subscriptions
_subscription?.cancel();
// Cancel ALL timers
_timer?.cancel();
// Dispose ALL controllers
_controller?.dispose();
// Close ALL streams you own
// _myStreamController?.close();
// ALWAYS call super.dispose() last
super.dispose();
}
}Checklist - Before shipping, verify each ChangeNotifier cleans up:
.cancel()).cancel()).dispose()).dispose()).dispose()).dispose()).close())Testing for leaks: Use Flutter DevTools Memory tab to verify instances are released when navigating away from screens.
#### Context Access Patterns & Solutions
Problem: "Provider not found" or "Bad state" errors when accessing providers in initState.
// ❌ WRONG: Context not ready during initState
@override
void initState() {
super.initState();
final provider = context.watch<MyProvider>(); // Error!
}Solution 1: Access providers in build() method
@override
Widget build(BuildContext context) {
final provider = context.watch<MyProvider>(); // ✅ Works
return ...;
}Solution 2: Use didChangeDependencies lifecycle method
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Context is ready here
final provider = context.read<MyProvider>();
provider.loadInitialData();
}Solution 3: Use context.read for one-time initialization
@override
void initState() {
super.initState();
// Schedule for after first frame
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<MyProvider>().initialize();
});
}Critical Distinction:
context.watch<T>() - Subscribes to changes, triggers rebuilds (use in build())context.read<T>() - One-time access, no subscription (use in callbacks/lifecycle methods)context.select<T, R>() - Subscribes to specific property (use in build() for granular rebuilds)#### Using Selector for Granular Rebuilds
When a widget only needs a small part of a large model:
Selector<UserModel, String>(
selector: (context, user) => user.name,
builder: (context, name, child) {
// Only rebuilds when user.name changes
return Text('Hello, $name');
},
)#### Using ProxyProvider for Dependent Services
When a ViewModel depends on another Provider:
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthProvider()),
ProxyProvider<AuthProvider, CartService>(
update: (context, auth, previous) => CartService(auth.userId),
),
],
child: MyApp(),
)For detailed implementation guides:
Provider.of<T>(context) with listen: true at the root of large build methods. Use Consumer<T> or Selector<T, R> to scope rebuilds.context.read<T>() or Provider.of<T>(context, listen: false).watch inside initState(). Use context.read instead or didChangeDependencies().ChangeNotifier MUST implement dispose() and clean up all subscriptions, timers, controllers, and resources. Use create constructor (not .value) when creating new instances.ChangeNotifierProvider.value(value: NewInstance()). Always use ChangeNotifierProvider(create: (_) => NewInstance()) for proper lifecycle management.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.