Skip to main content

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.