Architects a Flutter application using the recommended layered approach (UI, Logic, Data). Use when structuring a new project or refactoring for scalability.
71
87%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
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.
lib/
├── data/
│ ├── models/ # API models
│ ├── repositories/ # Repository implementations
│ └── services/ # API clients, local storage wrappers
├── domain/
│ ├── models/ # Clean domain models
│ └── use_cases/ # Optional business logic classes
└── ui/
├── core/ # Shared widgets, themes, typography
└── features/
└── [feature_name]/
├── view_models/
└── views/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).// 1. Service (Raw API interaction)
class ApiClient {
Future<UserApiModel> fetchUser(String id) async {
// HTTP GET implementation...
}
}
// 2. Repository (Single source of truth, returns Domain Model)
class UserRepository {
UserRepository({required ApiClient apiClient}) : _apiClient = apiClient;
final ApiClient _apiClient;
User? _cachedUser;
Future<User> getUser(String id) async {
if (_cachedUser != null) return _cachedUser!;
final apiModel = await _apiClient.fetchUser(id);
_cachedUser = User(id: apiModel.id, name: apiModel.fullName); // Transform to Domain Model
return _cachedUser!;
}
}// 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'),
),
],
);
},
);
}
}8aaa41d
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.