This skill defines how to design, structure, and implement Flutter applications using the recommended layered architecture with feature-first file organization.
It is state management agnostic: the business logic holder in the UI layer may be named ViewModel, Controller, Cubit, Bloc, Provider, or Notifier — depending on the chosen state management approach. The architectural rules apply equally to all of them.
Use this skill when:
Separate every app into a UI Layer and a Data Layer. Add a Logic (Domain) Layer between them only for complex apps.
┌──────────────────────────────────────────────────────────────┐
│ UI Layer │ Views + business logic holders │
│ │ (ViewModel / Cubit / Controller / Provider) │
├──────────────────────────────────────────────────────────────┤
│ Logic Layer │ Use Cases / Interactors (optional) │
├──────────────────────────────────────────────────────────────┤
│ Data Layer │ Repositories + Services │
└──────────────────────────────────────────────────────────────┘
Rules:
Organize code by feature, not by type. Group all layers belonging to one feature together in a single directory.
lib/
├── app.dart
├── main.dart
├── core/ # Shared utilities, theme, DI setup
│ ├── di/
│ │ └── service_locator.dart
│ ├── theme/
│ │ └── app_theme.dart
│ └── network/
│ └── api_client.dart
├── features/
│ ├── auth/
│ │ ├── data/
│ │ │ ├── auth_repository.dart
│ │ │ └── auth_api_service.dart
│ │ ├── domain/ # Optional — only for complex logic
│ │ │ └── login_usecase.dart
│ │ └── ui/
│ │ ├── auth_viewmodel.dart
│ │ ├── login_screen.dart
│ │ └── widgets/
│ │ └── login_form.dart
│ └── profile/
│ ├── data/
│ │ ├── profile_repository.dart
│ │ └── profile_api_service.dart
│ └── ui/
│ ├── profile_viewmodel.dart
│ └── profile_screen.dart
└── shared/ # Shared widgets, models, extensions
├── models/
│ └── user.dart
└── widgets/
└── loading_indicator.dart
Each feature directory contains the files needed for that feature, named according to the chosen state management approach:
| Approach | Business logic holder file |
|---|---|
| MVVM / ChangeNotifier | *_viewmodel.dart / *_controller.dart |
| BLoC | *_cubit.dart / *_bloc.dart |
| Provider / Riverpod | *_provider.dart / *_notifier.dart |
widgets/ subdirectory.StatelessWidget when possible; keep build methods simple.class AuthViewModel extends ChangeNotifier {
final AuthRepository _authRepo;
AuthViewModel(this._authRepo);
bool _isLoading = false;
bool get isLoading => _isLoading;
String? _error;
String? get error => _error;
Future<bool> login(String email, String password) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
await _authRepo.login(email, password);
return true;
} catch (e) {
_error = e.toString();
return false;
} finally {
_isLoading = false;
notifyListeners();
}
}
}
Introduce use cases/interactors only when:
Do not add a domain layer for simple CRUD apps.
Use dependency injection to provide components with their dependencies, enabling testability and flexibility.
// In service_locator.dart — register dependencies at startup
void setupDependencies() {
final apiClient = ApiClient();
// Services
final authService = AuthApiService(apiClient);
final profileService = ProfileApiService(apiClient);
// Repositories
final authRepo = AuthRepository(authService);
final profileRepo = ProfileRepository(profileService);
// Register with your DI framework (get_it, provider, riverpod, etc.)
getIt.registerSingleton<AuthRepository>(authRepo);
getIt.registerSingleton<ProfileRepository>(profileRepo);
}
features/<name>/ directory with data/, ui/, and optionally domain/ subdirectories.data/<name>_api_service.dart.data/<name>_repository.dart.ui/<name>_viewmodel.dart.ui/<name>_screen.dart.