Architecture
MVVM Pattern
Each feature module follows the MVVM (Model-View-ViewModel) structure:
modules/
└── {feature}/
├── model/ # Data classes (freezed or plain Dart)
├── view/ # UI widgets (Screens)
├── viewmodel/ # ChangeNotifier providers
└── services/ # API calls for this feature
Example — Attendance module:
modules/attendance/
├── model/
│ ├── attendance_record.dart
│ └── attendance_month.dart
├── view/
│ ├── attendance_screen.dart
│ └── widgets/
│ └── attendance_calendar.dart
├── viewmodel/
│ └── attendance_viewmodel.dart
└── services/
└── attendance_service.dart
GetIt Service Locator
Dependency injection via get_it. All services and ViewModels are registered in service_locator.dart:
// service_locator.dart
final getIt = GetIt.instance;
Future<void> setupServiceLocator() async {
// Network
getIt.registerLazySingleton<DioClient>(() => DioClient());
// Services
getIt.registerLazySingleton<AuthService>(() => AuthService(getIt<DioClient>()));
getIt.registerLazySingleton<StudentService>(() => StudentService(getIt<DioClient>()));
getIt.registerLazySingleton<AttendanceService>(() => AttendanceService(getIt<DioClient>()));
// ViewModels
getIt.registerFactory<AuthViewModel>(() => AuthViewModel(getIt<AuthService>()));
getIt.registerFactory<HomeViewModel>(() => HomeViewModel(getIt<StudentService>()));
getIt.registerFactory<AttendanceViewModel>(() => AttendanceViewModel(getIt<AttendanceService>()));
}
BaseProvider
All ViewModels extend BaseProvider which provides common state management:
// core/base/base_provider.dart
abstract class BaseProvider extends ChangeNotifier {
bool _isLoading = false;
String? _errorMessage;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
bool get hasError => _errorMessage != null;
void setLoading(bool value) {
_isLoading = value;
notifyListeners();
}
void setError(String? message) {
_errorMessage = message;
notifyListeners();
}
void clearError() {
_errorMessage = null;
notifyListeners();
}
Future<T> requestHandler<T>(Future<T> Function() apiCall) async {
setLoading(true);
clearError();
try {
final result = await apiCall();
return result;
} catch (e) {
setError(e.toString());
rethrow;
} finally {
setLoading(false);
}
}
}
ViewModel Example
// modules/attendance/viewmodel/attendance_viewmodel.dart
class AttendanceViewModel extends BaseProvider {
final AttendanceService _service;
List<AttendanceRecord> _records = [];
DateTime _selectedMonth = DateTime.now();
List<AttendanceRecord> get records => _records;
Future<void> loadAttendance(String studentId) async {
await requestHandler(() async {
_records = await _service.getAttendance(studentId, _selectedMonth);
});
notifyListeners();
}
void setMonth(DateTime month) {
_selectedMonth = month;
notifyListeners();
}
}
API Client
Dio client with 4 interceptors (see API Integration).
Navigation
AppNavigator class manages screen-to-screen navigation with named routes. See Navigation.