Architecture
Overview
The driver app follows a provider-based MVVM architecture with service locator (GetIt) for dependency injection. It is the simplest of the four SyncAD apps — no evaluation mode, no complex permission system, and no offline sync beyond location batching.
Layers
View (Widget)
→ ViewModel (ChangeNotifier via Provider)
→ Repository (abstract interface)
→ Repository Impl
→ Dio Client → API
Service Locator (GetIt)
service_locator.dart registers all dependencies:
// Auth
getIt.registerLazySingleton<AuthRepo>(() => AuthRepoImpl(getIt()));
getIt.registerFactory<AuthViewModel>(() => AuthViewModel(getIt()));
// Home
getIt.registerLazySingleton<HomeRepo>(() => HomeRepoImpl(getIt()));
getIt.registerFactory<HomeViewModel>(() => HomeViewModel(getIt(), getIt()));
// Trip Detail
getIt.registerLazySingleton<TripDetailRepo>(() => TripDetailRepoImpl(getIt()));
getIt.registerFactoryParam<TripDetailViewModel, String, void>(
(tripId, _) => TripDetailViewModel(getIt(), tripId),
);
// Students
getIt.registerLazySingleton<StudentsRepo>(() => StudentsRepoImpl(getIt()));
getIt.registerFactoryParam<StudentsViewModel, String, void>(
(tripId, _) => StudentsViewModel(getIt(), tripId),
);
// Incidents
getIt.registerLazySingleton<IncidentsRepo>(() => IncidentsRepoImpl(getIt()));
getIt.registerFactory<IncidentsViewModel>(() => IncidentsViewModel(getIt()));
// Profile
getIt.registerLazySingleton<ProfileRepo>(() => ProfileRepoImpl(getIt()));
getIt.registerFactory<ProfileViewModel>(() => ProfileViewModel(getIt()));
// Core
getIt.registerLazySingleton(() => DioClient());
getIt.registerLazySingleton(() => LocationService());
getIt.registerLazySingleton(() => SocketService());
Dio Client
class DioClient {
late final Dio _dio;
DioClient() {
_dio = Dio(BaseOptions(
baseUrl: 'https://dev-api.metaonus.in',
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
));
_dio.interceptors.addAll([
LoggerInterceptor(),
AuthInterceptor(), // attaches Bearer token
TokenRefreshInterceptor(), // auto-refresh on 401
]);
}
}
Interceptors
AuthInterceptor
Attaches the stored access token from secure storage:
class AuthInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final token = SecureStorage.instance.read('accessToken');
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
}
TokenRefreshInterceptor
Catches 401, refreshes the token using /driver/user-auth/refresh-token, and retries the original request. On failure, emits TOKEN_EXPIRED event to force logout.
Location Batching
The LocationService buffers GPS updates locally and flushes them in batches via POST /driver/bus-tracking/batch-locations to reduce API call frequency.
Socket.IO
Socket.IO is used to push driver location to parents and school admin for live tracking. The SocketService connects when a trip is in_progress:
// Emits location update to server
socket.emit('driver-location', {
'tripId': tripId,
'lat': position.latitude,
'lng': position.longitude,
'timestamp': DateTime.now().toIso8601String(),
});
The driver app itself does NOT receive Socket.IO events — it polls REST endpoints for student status and stop updates.