npx skills add ...
npx skills add flutter/agent-plugins --skill flutter-apply-architecture-best-practices
npx skills add flutter/agent-plugins --skill flutter-apply-architecture-best-practices
Architects a Flutter application using the recommended layered approach (UI, Logic, Data). Use when structuring a new project or refactoring for scalability.
The same skill content is published under more than one repo. The install counts are split across them; any of these commands works.
Enforce strict Separation of Concerns by dividing the application into distinct layers. Never mix UI rendering with business logic or data fetching.
Implement the MVVM (Model-View-ViewModel) pattern to manage UI state and logic.
ChangeNotifier (or use Listenable) to expose state. Expose immutable state snapshots to the View. Inject Repositories into ViewModels via the constructor.Implement the Repository pattern to isolate data access logic and create a single source of truth.
Result wrappers.Organize the codebase using a hybrid approach: group UI components by feature, and group Data/Domain components by type.
Follow this sequential workflow when adding a new feature to the application. Copy the checklist to track progress.
freezed or built_value.ChangeNotifier. Inject required Repositories/Use Cases. Expose immutable state and command methods.ListenableBuilder or AnimatedBuilder to listen to ViewModel changes.provider or get_it).// 3. ViewModel (State management and presentation logic)
class ProfileViewModel extends ChangeNotifier {
ProfileViewModel({required UserRepository userRepository})
: _userRepository = userRepository;
final UserRepository _userRepository;
User? _user;
User? get user => _user;
bool _isLoading = false;
bool get isLoading => _isLoading;
Future<void> loadProfile(String id) async {
_isLoading = true;
notifyListeners();
try {
_user = await _userRepository.getUser(id);
} finally {
_isLoading = false;
notifyListeners();
}
}
}
// 4. View (Dumb UI component)
class ProfileView extends StatelessWidget {
const ProfileView({super.key, required this.viewModel});
final ProfileViewModel viewModel;
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: viewModel,
builder: (context, _) {
if (viewModel.isLoading) {
return const Center(child: CircularProgressIndicator());
}
final user = viewModel.user;
if (user == null) {
return const Center(child: Text('User not found'));
}
return Column(
children: [
Text(user.name),
ElevatedButton(
onPressed: () => viewModel.loadProfile(user.id),
child: const Text('Refresh'),
),
],
);
},
);
}
}