State Management
Overview
Provider + ChangeNotifier is used for state management. Each ViewModel is a ChangeNotifier subclass that extends BaseProvider.
ViewModel Registry
All ViewModels registered in service_locator.dart:
// service_locator.dart
Future<void> setupServiceLocator() async {
// Auth
getIt.registerFactory<AuthViewModel>(() => AuthViewModel(getIt<AuthService>()));
// Core
getIt.registerFactory<HomeViewModel>(() => HomeViewModel(getIt<StudentService>()));
// Features
getIt.registerFactory<AttendanceViewModel>(() => AttendanceViewModel(getIt<AttendanceService>()));
getIt.registerFactory<ExamViewModel>(() => ExamViewModel(getIt<ExamService>()));
getIt.registerFactory<FeeViewModel>(() => FeeViewModel(getIt<FeeService>()));
getIt.registerFactory<AnnouncementViewModel>(() => AnnouncementViewModel(getIt<AnnouncementService>()));
getIt.registerFactory<TimetableViewModel>(() => TimetableViewModel(getIt<TimetableService>()));
getIt.registerFactory<LeaveViewModel>(() => LeaveViewModel(getIt<LeaveService>()));
getIt.registerFactory<EventViewModel>(() => EventViewModel(getIt<EventService>()));
getIt.registerFactory<TeacherViewModel>(() => TeacherViewModel(getIt<TeacherService>()));
getIt.registerFactory<MessageViewModel>(() => MessageViewModel(getIt<MessageService>()));
getIt.registerFactory<NotificationViewModel>(() => NotificationViewModel(getIt<NotificationService>()));
}
Using ViewModels in Screens
// In MultiProvider at app root:
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => getIt<AuthViewModel>()),
ChangeNotifierProvider(create: (_) => getIt<HomeViewModel>()),
ChangeNotifierProvider(create: (_) => getIt<AttendanceViewModel>()),
// ...
],
child: MaterialApp(...),
)
// In a screen:
class AttendanceScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<AttendanceViewModel>(
builder: (context, vm, _) {
if (vm.isLoading) return const CircularProgressIndicator();
if (vm.hasError) return Text(vm.errorMessage!);
return ListView.builder(
items: vm.records,
itemBuilder: (_, record) => AttendanceTile(record: record),
);
},
);
}
}
BaseProvider Request Handler
The requestHandler wrapper in BaseProvider handles loading/error state automatically:
class AttendanceViewModel extends BaseProvider {
Future<void> loadAttendance() async {
await requestHandler(() async {
_records = await _service.getAttendance(_selectedStudentId);
});
notifyListeners(); // Call after requestHandler since it doesn't notify
}
}
Loading Overlay
A common pattern is to show a loading overlay during navigation or data fetches:
if (vm.isLoading) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
Student Context
The HomeViewModel holds the currently selected studentId which is passed to all other ViewModels:
class HomeViewModel extends BaseProvider {
String? _currentStudentId;
List<Student> _students = [];
String? get currentStudentId => _currentStudentId;
List<Student> get students => _students;
void switchStudent(String studentId) {
_currentStudentId = studentId;
_loadStudentData();
notifyListeners();
}
}