Skip to main content

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();
}
}