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
| Endpoint | Method | Purpose |
|---|---|---|
parent/user-auth/login | POST | Request OTP |
parent/user-auth/otp-verify | POST | Verify OTP, get tokens |
parent/user-auth/set-pin | POST | Set 4-digit PIN |
parent/user-auth/refresh-token | POST | Refresh access token |
parent/student/get-students | GET | Get linked students |
parent/student/get-student-details | GET | Get student details |
parent/attendance/get-student-attendance | GET | Attendance records |
parent/exam/get-scheduled-exam | GET | Exam schedule |
parent/exam/get-exam-result | GET | Exam results |
parent/exam/get-assignments | GET | Assignments |
parent/exam/get-hall-ticket | GET | Hall ticket |
parent/exam/student-progress-report | GET | Progress report |
parent/fee/fees | GET | Fee balance and history |
parent/announcement/get-announcement | GET | Announcements |
parent/student/get-timetable-details | GET | Weekly timetable |
parent/leave/create-leave | POST | Apply for leave |
parent/leave/get-leaves | GET | Leave history |
parent/co-curricular/get-events | GET | School events |
parent/co-curricular/get-competitions | GET | Competitions |
parent/teacher/get-teachers | GET | Teacher directory |
parent/message/get-contacts | GET | Contacts list |
parent/message/get-messages | GET | Chat messages |
parent/message/send-message | POST | Send message |
parent/message/get-broadcast-message | GET | Broadcast messages |
parent/notification/delivered-notifications | GET | Notification history |
parent/notification/read | POST | Mark notification read |
parent/notification/token | POST | Register FCM token |
parent/bus-tracking/bus-assignment | GET | Assigned bus |
parent/bus-tracking/get-bus-location | GET | Live bus location |
parent/bus-tracking/active-trip | GET | Active trip info |
parent/bus-tracking/get-route-stops | GET | Route stops |
parent/bus-tracking/student-status | GET | Student boarding status |