From flutter-skills
Manages application and ephemeral state in a Flutter app. Use when sharing data between widgets or handling complex UI state transitions.
npx claudepluginhub gsmlg-dev/code-agent --plugin flutter-skillsThis skill uses the workspace's default tool permissions.
- [Core Concepts](#core-concepts)
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides MCP server integration in Claude Code plugins via .mcp.json or plugin.json configs for stdio, SSE, HTTP types, enabling external services as tools.
Flutter's UI is declarative; it is built to reflect the current state of the app (UI = f(state)). When state changes, trigger a rebuild of the UI that depends on that state.
Distinguish between two primary types of state to determine your management strategy:
PageView, current selected tab, animation progress). Manage this using a StatefulWidget and setState().InheritedWidget, the provider package, and the MVVM architecture.Implement the Model-View-ViewModel (MVVM) design pattern combined with Unidirectional Data Flow (UDF) for scalable app state management.
ChangeNotifier and call notifyListeners() to trigger UI rebuilds when data changes.Evaluate the scope of the state to determine the correct implementation strategy.
StatefulWidget and State.State class.setState() callback to mark the widget as dirty and schedule a rebuild.provider package (a wrapper around InheritedWidget) to inject state into the widget tree.ChangeNotifier to emit state updates.Follow this sequential workflow to implement app-level state management using MVVM and provider.
Task Progress:
ChangeNotifier).Create a repository class to act as the Single Source of Truth (SSOT) for the specific data domain. Handle all external API calls or database queries here.
ChangeNotifier)Create a ViewModel class that extends ChangeNotifier.
isLoading, data, errorMessage).notifyListeners() to trigger UI rebuilds.Use ChangeNotifierProvider from the provider package to provide the ViewModel to the widget subtree that requires it. Place the provider as low in the widget tree as possible to avoid polluting the scope.
Access the ViewModel in your StatelessWidget or StatefulWidget.
Consumer<MyViewModel> to rebuild specific parts of the UI when notifyListeners() is called.context.read<MyViewModel>() (or Provider.of<MyViewModel>(context, listen: false)) inside event handlers (like onPressed) to call ViewModel methods without triggering a rebuild of the calling widget.Run the following feedback loop to ensure data flows correctly:
notifyListeners().notifyListeners() calls or incorrect Provider scopes.setState)Use this pattern strictly for local, UI-only state.
class EphemeralCounter extends StatefulWidget {
const EphemeralCounter({super.key});
@override
State<EphemeralCounter> createState() => _EphemeralCounterState();
}
class _EphemeralCounterState extends State<EphemeralCounter> {
int _counter = 0; // Local state
void _increment() {
setState(() {
_counter++; // Mutate state and schedule rebuild
});
}
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: _increment,
child: Text('Count: $_counter'),
);
}
}
Use this pattern for shared data and complex business logic.
// 1. Model (Repository)
class CartRepository {
Future<void> saveItemToCart(String item) async {
// Simulate network/database call
await Future.delayed(const Duration(milliseconds: 500));
}
}
// 2. ViewModel (ChangeNotifier)
class CartViewModel extends ChangeNotifier {
final CartRepository repository;
CartViewModel({required this.repository});
final List<String> _items = [];
bool isLoading = false;
String? errorMessage;
List<String> get items => List.unmodifiable(_items);
Future<void> addItem(String item) async {
isLoading = true;
errorMessage = null;
notifyListeners(); // Trigger loading UI
try {
await repository.saveItemToCart(item);
_items.add(item);
} catch (e) {
errorMessage = 'Failed to add item';
} finally {
isLoading = false;
notifyListeners(); // Trigger success/error UI
}
}
}
// 3. Injection & 4. View (UI)
class CartApp extends StatelessWidget {
const CartApp({super.key});
@override
Widget build(BuildContext context) {
// Inject ViewModel
return ChangeNotifierProvider(
create: (_) => CartViewModel(repository: CartRepository()),
child: const CartScreen(),
);
}
}
class CartScreen extends StatelessWidget {
const CartScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Consumer<CartViewModel>(
builder: (context, viewModel, child) {
if (viewModel.isLoading) {
return const CircularProgressIndicator();
}
if (viewModel.errorMessage != null) {
return Text(viewModel.errorMessage!);
}
return ListView.builder(
itemCount: viewModel.items.length,
itemBuilder: (_, index) => Text(viewModel.items[index]),
);
},
),
floatingActionButton: FloatingActionButton(
// Use read() to access methods without listening for rebuilds
onPressed: () => context.read<CartViewModel>().addItem('New Item'),
child: const Icon(Icons.add),
),
);
}
}