Skip to main content

API Integration

Dio Client

The DioClient in lib/core/network/dio_client.dart is the single HTTP client instance used throughout the app.

// lib/core/network/dio_client.dart
class DioClient {
late final Dio _dio;

DioClient() {
_dio = Dio(BaseOptions(
baseUrl: 'https://dev-api.metaonus.in/parent',
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));

_dio.interceptors.addAll([
LoggerInterceptor(),
AuthInterceptor(), // #1 — attaches token
TokenRefreshInterceptor(), // #2 — auto-refresh on 401
EvaluationInterceptor(), // #3 — handles evaluationMode
]);
}
}

4 Interceptors

1. LoggerInterceptor

Logs all requests and responses in debug mode:

class LoggerInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
log('🌐 [REQUEST] ${options.method} ${options.uri}');
handler.next(options);
}

@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
log('✅ [RESPONSE] ${response.statusCode} ${response.requestOptions.uri}');
handler.next(response);
}

@override
void onError(DioException err, ErrorInterceptorHandler handler) {
log('❌ [ERROR] ${err.response?.statusCode} ${err.requestOptions.uri}: ${err.message}');
handler.next(err);
}
}

2. AuthInterceptor

Attaches the stored access token to every request:

class AuthInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final token = StorageService.instance.read('accessToken');
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
}

3. TokenRefreshInterceptor

Catches 401 responses, auto-refreshes the token, and retries the original request:

class TokenRefreshInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401) {
// Avoid infinite loop — don't refresh if already refreshing
if (_isRefreshing) return;

_isRefreshing = true;
try {
final newTokens = await _refreshToken();
// Retry original request with new token
final opts = err.requestOptions;
opts.headers['Authorization'] = 'Bearer ${newTokens.accessToken}';
final response = await _dio.fetch(opts);
_isRefreshing = false;
handler.resolve(response);
} catch (e) {
_isRefreshing = false;
// Token refresh failed — force logout
EventBus.instance.emit('TOKEN_EXPIRED');
handler.next(err);
}
} else {
handler.next(err);
}
}

Future<Tokens> _refreshToken() async {
final refreshToken = StorageService.instance.read('refreshToken');
final response = await Dio().post(
'https://dev-api.metaonus.in/parent/user-auth/refresh-token',
data: {'refreshToken': refreshToken},
);
final tokens = Tokens.fromJson(response['data']);
await StorageService.instance.write('accessToken', tokens.accessToken);
await StorageService.instance.write('refreshToken', tokens.refreshToken);
return tokens;
}
}

4. EvaluationInterceptor

Handles 403 EVALUATION_MODE_READONLY responses from the teachers app (parents app doesn't use evaluation mode, but the pattern is shared):

class EvaluationInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
if (err.response?.statusCode == 403 &&
err.response?.data['error'] == 'EVALUATION_MODE_READONLY') {
// Trigger permission sync in AuthViewModel
EventBus.instance.emit('EVALUATION_MODE_TRIGGERED');
}
handler.next(err);
}
}

Retry Policy

Failed requests (timeout, 5xx) are retried with exponential backoff:

// In DioClient constructor
_dio.interceptors.add(InterceptorsWrapper(
onError: (err, handler) async {
if (_isRetryable(err) && _retryCount < 3) {
_retryCount++;
await Future.delayed(Duration(seconds: _retryCount * 2));
try {
final response = await _dio.fetch(err.requestOptions);
_retryCount = 0;
return handler.resolve(response);
} catch (e) {
_retryCount = 0;
return handler.next(err);
}
}
_retryCount = 0;
return handler.next(err);
},
));

Key Endpoints

EndpointMethodPurpose
parent/user-auth/loginPOSTRequest OTP
parent/user-auth/otp-verifyPOSTVerify OTP, get tokens
parent/user-auth/set-pinPOSTSet 4-digit PIN
parent/user-auth/refresh-tokenPOSTRefresh access token
parent/student/get-studentsGETGet linked students
parent/student/get-student-detailsGETGet student details
parent/attendance/get-student-attendanceGETAttendance records
parent/exam/get-scheduled-examGETExam schedule
parent/exam/get-exam-resultGETExam results
parent/exam/get-assignmentsGETAssignments
parent/exam/get-hall-ticketGETHall ticket
parent/exam/student-progress-reportGETProgress report
parent/fee/feesGETFee balance and history
parent/announcement/get-announcementGETAnnouncements
parent/student/get-timetable-detailsGETWeekly timetable
parent/leave/create-leavePOSTApply for leave
parent/leave/get-leavesGETLeave history
parent/co-curricular/get-eventsGETSchool events
parent/co-curricular/get-competitionsGETCompetitions
parent/teacher/get-teachersGETTeacher directory
parent/message/get-contactsGETContacts list
parent/message/get-messagesGETChat messages
parent/message/send-messagePOSTSend message
parent/message/get-broadcast-messageGETBroadcast messages
parent/notification/delivered-notificationsGETNotification history
parent/notification/readPOSTMark notification read
parent/notification/tokenPOSTRegister FCM token
parent/bus-tracking/bus-assignmentGETAssigned bus
parent/bus-tracking/get-bus-locationGETLive bus location
parent/bus-tracking/active-tripGETActive trip info
parent/bus-tracking/get-route-stopsGETRoute stops
parent/bus-tracking/student-statusGETStudent boarding status