Refactor API error handling and add try-catch blocks

Added try-catch blocks for error handling in API's files to rethrow the errors to the cubit so cubits can update the UI based on them.

Refactored error handling in HTTPInterceptor and HTTPService classes.
This commit is contained in:
Mohammad Salameh
2024-04-15 15:47:13 +03:00
parent dd90a2133f
commit df13c66b1a
9 changed files with 247 additions and 191 deletions

View File

@ -125,14 +125,17 @@ class HomeCubit extends Cubit<HomeState> {
emitSafe(GetSpacesLoading()); emitSafe(GetSpacesLoading());
try { try {
spaces = await SpacesAPI.getSpaces(); spaces = await SpacesAPI.getSpaces();
selectedSpace = spaces!.isNotEmpty } catch (failure) {
? emitSafe(GetSpacesError(failure.toString()));
// selectSpace(spaces!.first) return;
selectedSpace = spaces!.first }
: null;
emitSafe(GetSpacesLoaded(spaces!)); if (spaces != null && spaces!.isNotEmpty) {
} on ServerFailure catch (failure) { selectedSpace = spaces!.first;
emitSafe(GetSpacesError(failure.errMessage)); emitSafe(GetSpacesSuccess(spaces!));
fetchRooms(selectedSpace!);
} else {
emitSafe(GetSpacesError("No spaces found"));
} }
} }
@ -140,13 +143,14 @@ class HomeCubit extends Cubit<HomeState> {
emitSafe(GetSpaceRoomsLoading()); emitSafe(GetSpaceRoomsLoading());
try { try {
space.rooms = await SpacesAPI.getRoomsBySpaceId(space.id!); space.rooms = await SpacesAPI.getRoomsBySpaceId(space.id!);
if (space.rooms != null) { } catch (failure) {
emitSafe(GetSpaceRoomsLoaded(space.rooms!)); emitSafe(GetSpaceRoomsError(failure.toString()));
} else { return;
emitSafe(GetSpaceRoomsError("No rooms found")); }
} if (space.rooms != null) {
} on ServerFailure catch (failure) { emitSafe(GetSpaceRoomsSuccess(space.rooms!));
emitSafe(GetSpacesError(failure.errMessage)); } else {
emitSafe(GetSpaceRoomsError("No rooms found"));
} }
} }
@ -169,14 +173,6 @@ class HomeCubit extends Cubit<HomeState> {
), ),
], ],
'Devices': [ 'Devices': [
// IconButton(
// icon: Image.asset(
// Assets.iconsFilter,
// height: 20,
// width: 20,
// ),
// onPressed: () {},
// ),
IconButton( IconButton(
icon: const Icon( icon: const Icon(
Icons.add, Icons.add,

View File

@ -4,33 +4,45 @@ abstract class HomeState {}
class HomeInitial extends HomeState {} class HomeInitial extends HomeState {}
class GetSpacesLoading extends HomeState {} //base states
class HomeLoading extends HomeState {}
class GetSpacesLoaded extends HomeState { class HomeError extends HomeState {
final String errMessage;
HomeError(this.errMessage);
}
class HomeSuccess extends HomeState {}
///specific states
//get spaces
class GetSpacesLoading extends HomeLoading {}
class GetSpacesSuccess extends HomeSuccess {
final List<SpaceModel> spaces; final List<SpaceModel> spaces;
GetSpacesLoaded(this.spaces); GetSpacesSuccess(this.spaces);
} }
class GetSpacesError extends HomeState { class GetSpacesError extends HomeError {
final String errMessage; GetSpacesError(super.errMessage);
GetSpacesError(this.errMessage);
} }
class GetSpaceRoomsLoading extends HomeState {} //get rooms
class GetSpaceRoomsLoading extends HomeLoading {}
class GetSpaceRoomsLoaded extends HomeState { class GetSpaceRoomsSuccess extends HomeSuccess {
final List<RoomModel> rooms; final List<RoomModel> rooms;
GetSpaceRoomsLoaded(this.rooms); GetSpaceRoomsSuccess(this.rooms);
} }
class GetSpaceRoomsError extends HomeState { class GetSpaceRoomsError extends HomeError {
final String errMessage; GetSpaceRoomsError(super.errMessage);
GetSpaceRoomsError(this.errMessage);
} }
//UI states
class SpaceSelected extends HomeState { class SpaceSelected extends HomeState {
final SpaceModel space; final SpaceModel space;

View File

@ -41,7 +41,7 @@ class AuthCubit extends Cubit<AuthState> {
static UserModel? user; static UserModel? user;
static Token token = Token.emptyConstructor(); static Token token = Token.emptyConstructor();
/////////////////////////////////////API CALLS/////////////////////////////////////
login() async { login() async {
emit(AuthLoginLoading()); emit(AuthLoginLoading());
try { try {
@ -51,23 +51,23 @@ class AuthCubit extends Cubit<AuthState> {
password: passwordController.text, password: passwordController.text,
), ),
); );
} catch (failure) {
if (token.accessTokenIsNotEmpty) { emit(AuthLoginError(message: failure.toString()));
FlutterSecureStorage storage = const FlutterSecureStorage(); return;
await storage.write( }
key: Token.loginAccessTokenKey, value: token.accessToken); if (token.accessTokenIsNotEmpty) {
const FlutterSecureStorage().write( FlutterSecureStorage storage = const FlutterSecureStorage();
key: UserModel.userUuidKey, await storage.write(
value: Token.decodeToken(token.accessToken)['uuid'].toString()); key: Token.loginAccessTokenKey, value: token.accessToken);
user = UserModel.fromToken(token); const FlutterSecureStorage().write(
emailController.clear(); key: UserModel.userUuidKey,
passwordController.clear(); value: Token.decodeToken(token.accessToken)['uuid'].toString());
emit(AuthLoginSuccess()); user = UserModel.fromToken(token);
} else { emailController.clear();
emit(AuthLoginError(message: 'Something went wrong')); passwordController.clear();
} emit(AuthLoginSuccess());
} on ServerFailure catch (failure) { } else {
emit(AuthError(message: failure.errMessage)); emit(AuthLoginError(message: 'Something went wrong'));
} }
} }
@ -79,8 +79,9 @@ class AuthCubit extends Cubit<AuthState> {
NavigationService.navigatorKey.currentState! NavigationService.navigatorKey.currentState!
.popAndPushNamed(Routes.authLogin); .popAndPushNamed(Routes.authLogin);
emit(AuthLogoutSuccess()); emit(AuthLogoutSuccess());
} on ServerFailure catch (failure) { } catch (failure) {
emit(AuthError(message: failure.errMessage)); emit(AuthLogoutError(message: failure.toString()));
return;
} }
} }
@ -109,11 +110,4 @@ class AuthCubit extends Cubit<AuthState> {
emit(AuthTokenError(message: "Something went wrong")); emit(AuthTokenError(message: "Something went wrong"));
} }
} }
static void logUserOut() async {
user = null;
token = Token.emptyConstructor();
FlutterSecureStorage storage = const FlutterSecureStorage();
await storage.delete(key: Token.loginAccessTokenKey);
}
} }

View File

@ -251,32 +251,42 @@ class DevicesCubit extends Cubit<DevicesState> {
code: control.code, code: control.code,
)); ));
try { try {
await DevicesAPI.controlDevice(control).then((response) { var response = await DevicesAPI.controlDevice(control);
emitSafe(DeviceControlSuccess(
code: control.code, if (response['success'] ?? false) {
)); emitSafe(DeviceControlSuccess(code: control.code));
if (response['success'] ?? false) { //this delay is to give tuya server time to update the status
Future.delayed(const Duration(milliseconds: 400), () { Future.delayed(const Duration(milliseconds: 400), () {
getDevicesStatues( getDevicesStatues(
deviceId, deviceId,
HomeCubit.getInstance().selectedSpace!.rooms!.indexOf( HomeCubit.getInstance()
HomeCubit.getInstance().selectedRoom!, .selectedSpace!
), .rooms!
code: control.code); .indexOf(HomeCubit.getInstance().selectedRoom!),
}); code: control.code);
} else { });
emitSafe(DeviceControlError('Failed to control the device')); } else {
} emitSafe(DeviceControlError('Failed to control the device'));
}); }
} on ServerFailure catch (failure) { } catch (failure) {
emitSafe(DeviceControlError(failure.errMessage)); emitSafe(DeviceControlError(failure.toString()));
return;
} }
} }
fetchGroups(int spaceId) async { fetchGroups(int spaceId) async {
emitSafe(DevicesCategoriesLoading()); emitSafe(DevicesCategoriesLoading());
allCategories = await DevicesAPI.fetchGroups(spaceId); try {
emitSafe(DevicesCategoriesSuccess()); allCategories = await DevicesAPI.fetchGroups(spaceId);
} catch (e) {
emitSafe(DevicesCategoriesError(e.toString()));
return;
}
if (allCategories!.isNotEmpty && allCategories != null) {
emitSafe(DevicesCategoriesSuccess());
} else {
emitSafe(DevicesCategoriesError('No Groups found'));
}
} }
fetchDevicesByRoomId(int? roomId) async { fetchDevicesByRoomId(int? roomId) async {
@ -287,46 +297,46 @@ class DevicesCubit extends Cubit<DevicesState> {
.selectedSpace! .selectedSpace!
.rooms! .rooms!
.indexWhere((element) => element.id == roomId); .indexWhere((element) => element.id == roomId);
HomeCubit.getInstance().selectedSpace!.rooms![roomIndex].devices = try {
await SpacesAPI.getDevicesByRoomId(roomId); HomeCubit.getInstance().selectedSpace!.rooms![roomIndex].devices =
await DevicesAPI.getDevicesByRoomId(roomId);
} catch (e) {
emitSafe(GetDevicesError(e.toString()));
return;
}
emitSafe(GetDevicesSuccess());
//get status for each device //get status for each device
//TODO get devices status per page via page controller instead of getting all devices status at once
for (var device for (var device
in HomeCubit.getInstance().selectedSpace!.rooms![roomIndex].devices!) { in HomeCubit.getInstance().selectedSpace!.rooms![roomIndex].devices!) {
getDevicesStatues(device.id!, roomIndex); getDevicesStatues(device.id!, roomIndex);
} }
emitSafe(GetDevicesSuccess());
} }
getDevicesStatues(String deviceId, int roomIndex, {String? code}) async { getDevicesStatues(String deviceId, int roomIndex, {String? code}) async {
emitSafe(GetDeviceStatusLoading(code: code));
int deviceIndex = HomeCubit.getInstance()
.selectedSpace!
.rooms![roomIndex]
.devices!
.indexWhere((element) => element.id == deviceId);
List<StatusModel> statuses = [];
try { try {
emitSafe(GetDeviceStatusLoading(code: code));
int deviceIndex = HomeCubit.getInstance()
.selectedSpace!
.rooms![roomIndex]
.devices!
.indexWhere((element) => element.id == deviceId);
List<StatusModel> statuses = [];
var response = await DevicesAPI.getDeviceStatus(deviceId); var response = await DevicesAPI.getDeviceStatus(deviceId);
// if (response['result']['status'].length > 4)
// print('response : ${response['result']['status'][4]}');
for (var status in response['result']['status']) { for (var status in response['result']['status']) {
statuses.add(StatusModel.fromJson(status)); statuses.add(StatusModel.fromJson(status));
} }
} catch (e) {
HomeCubit.getInstance() emitSafe(GetDeviceStatusError(e.toString()));
.selectedSpace! return;
.rooms![roomIndex]
.devices![deviceIndex]
.status = statuses;
emitSafe(GetDeviceStatusSuccess(code: code));
} on ServerFailure catch (failure) {
emitSafe(
GetDeviceStatusError(failure.errMessage),
);
} }
HomeCubit.getInstance()
.selectedSpace!
.rooms![roomIndex]
.devices![deviceIndex]
.status = statuses;
emitSafe(GetDeviceStatusSuccess(code: code));
} }
///Lights ///Lights

View File

@ -12,10 +12,8 @@ class SceneCubit extends Cubit<SceneState> {
void getScenes() { void getScenes() {
emit(SceneLoading()); emit(SceneLoading());
//TODO: remove this it's causing the Bad State because its being after the cubit is closed
Future.delayed(const Duration(milliseconds: 50), () { emit(SceneSuccess());
emit(SceneSuccess());
});
} }
List<SceneModel> scenes = []; List<SceneModel> scenes = [];

View File

@ -1,5 +1,6 @@
import 'package:syncrow_app/features/devices/model/device_category_model.dart'; import 'package:syncrow_app/features/devices/model/device_category_model.dart';
import 'package:syncrow_app/features/devices/model/device_control_model.dart'; import 'package:syncrow_app/features/devices/model/device_control_model.dart';
import 'package:syncrow_app/features/devices/model/device_model.dart';
import 'package:syncrow_app/services/api/api_links_endpoints.dart'; import 'package:syncrow_app/services/api/api_links_endpoints.dart';
import 'package:syncrow_app/services/api/http_service.dart'; import 'package:syncrow_app/services/api/http_service.dart';
@ -8,18 +9,19 @@ class DevicesAPI {
static Future<Map<String, dynamic>> controlDevice( static Future<Map<String, dynamic>> controlDevice(
DeviceControlModel controlModel) async { DeviceControlModel controlModel) async {
// print( try {
// 'contoling [${controlModel.deviceId}] with code [${controlModel.code}] and value [${controlModel.value}'); final response = await _httpService.post(
path: ApiEndpoints.control,
final response = await _httpService.post( body: controlModel.toJson(),
path: ApiEndpoints.control, showServerMessage: false,
body: controlModel.toJson(), expectedResponseModel: (json) {
showServerMessage: false, return json;
expectedResponseModel: (json) { },
return json; );
}, return response;
); } catch (e) {
return response; rethrow;
}
} }
static Future<List<DevicesCategoryModel>> fetchGroups(int spaceId) async { static Future<List<DevicesCategoryModel>> fetchGroups(int spaceId) async {
@ -28,24 +30,52 @@ class DevicesAPI {
"pageSize": 100, "pageSize": 100,
"pageNo": 1 "pageNo": 1
}; };
final response = await _httpService.get( try {
path: ApiEndpoints.groups, final response = await _httpService.get(
queryParameters: params, path: ApiEndpoints.groups,
showServerMessage: false, queryParameters: params,
expectedResponseModel: (json) => showServerMessage: false,
DevicesCategoryModel.fromJsonList(json['groups']), expectedResponseModel: (json) =>
); DevicesCategoryModel.fromJsonList(json['groups']),
return response; );
return response;
} catch (e) {
rethrow;
}
} }
static Future<Map<String, dynamic>> getDeviceStatus(String deviceId) async { static Future<Map<String, dynamic>> getDeviceStatus(String deviceId) async {
final response = await _httpService.get( try {
path: '${ApiEndpoints.deviceStatus}/$deviceId/functions/status', final response = await _httpService.get(
showServerMessage: false, path: '${ApiEndpoints.deviceStatus}/$deviceId/functions/status',
expectedResponseModel: (json) { showServerMessage: false,
return json; expectedResponseModel: (json) {
}, return json;
); },
return response; );
return response;
} catch (e) {
rethrow;
}
}
static Future<List<DeviceModel>> getDevicesByRoomId(int roomId) async {
try {
final response = await _httpService.get(
path: ApiEndpoints.devicesByRoom,
queryParameters: {"roomId": roomId, "pageSize": 10},
showServerMessage: false,
expectedResponseModel: (json) {
List<DeviceModel> devices = [];
for (var device in json['devices']) {
devices.add(DeviceModel.fromJson(device));
}
return devices;
},
);
return response;
} catch (e) {
rethrow;
}
} }
} }

View File

@ -1,18 +1,30 @@
import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart'; import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart';
import 'package:syncrow_app/features/auth/model/token.dart'; import 'package:syncrow_app/features/auth/model/token.dart';
import 'package:syncrow_app/navigation/navigation_service.dart'; import 'package:syncrow_app/navigation/navigation_service.dart';
import 'package:syncrow_app/services/api/api_links_endpoints.dart';
import 'dart:async'; import 'dart:async';
import 'dart:developer' as developer; import 'dart:developer' as developer;
import 'package:syncrow_app/services/api/network_exception.dart'; import 'package:syncrow_app/services/api/network_exception.dart';
import 'package:syncrow_app/utils/helpers/snack_bar.dart'; import 'package:syncrow_app/utils/helpers/snack_bar.dart';
class HTTPInterceptor extends InterceptorsWrapper { class HTTPInterceptor extends InterceptorsWrapper {
@override List<String> headerExclusionList = [];
List<String> headerExclusionListOfAddedParameters = [
ApiEndpoints.login,
];
@override @override
void onResponse(Response response, ResponseInterceptorHandler handler) async { void onResponse(Response response, ResponseInterceptorHandler handler) async {
return handler.next(response); if (await validateResponse(response)) {
super.onResponse(response, handler);
} else {
handler.reject(DioException(
requestOptions: response.requestOptions, response: response));
}
} }
@override @override
@ -20,17 +32,20 @@ class HTTPInterceptor extends InterceptorsWrapper {
RequestOptions options, RequestInterceptorHandler handler) async { RequestOptions options, RequestInterceptorHandler handler) async {
var storage = const FlutterSecureStorage(); var storage = const FlutterSecureStorage();
var token = await storage.read(key: Token.loginAccessTokenKey); var token = await storage.read(key: Token.loginAccessTokenKey);
// options.headers['Authorization'] = 'Bearer $token'; if (checkHeaderExclusionListOfAddedParameters(options.path)) {
options.headers['Authorization'] = 'Bearer ${'${token!}123'}'; options.headers
.putIfAbsent(HttpHeaders.authorizationHeader, () => "Bearer $token");
}
// options.headers['Authorization'] = 'Bearer ${'${token!}123'}';
super.onRequest(options, handler); super.onRequest(options, handler);
} }
@override @override
void onError(DioException err, ErrorInterceptorHandler handler) async { void onError(DioException err, ErrorInterceptorHandler handler) async {
developer.log('Error Message: ${err.message}'); // developer.log('Error Message: ${err.message}');
developer.log('Error res Code: ${err.response?.statusCode}'); // developer.log('Error res Code: ${err.response?.statusCode}');
developer.log('Error res Data: ${err.response?.data}'); // developer.log('Error res Data: ${err.response?.data}');
developer.log('Error res status message: ${err.response?.statusMessage}'); // developer.log('Error res status message: ${err.response?.statusMessage}');
ServerFailure failure = ServerFailure.fromDioError(err); ServerFailure failure = ServerFailure.fromDioError(err);
CustomSnackBar.displaySnackBar(failure.toString()); CustomSnackBar.displaySnackBar(failure.toString());
@ -39,8 +54,8 @@ class HTTPInterceptor extends InterceptorsWrapper {
if (err.response?.statusCode == 401 && token != null) { if (err.response?.statusCode == 401 && token != null) {
await AuthCubit.get(NavigationService.navigatorKey.currentContext!) await AuthCubit.get(NavigationService.navigatorKey.currentContext!)
.logout(); .logout();
super.onError(err, handler);
} }
super.onError(err, handler);
} }
/// Validates the response and returns true if it is successful (status code 2xx). /// Validates the response and returns true if it is successful (status code 2xx).
@ -60,4 +75,15 @@ class HTTPInterceptor extends InterceptorsWrapper {
return false; return false;
} }
} }
checkHeaderExclusionListOfAddedParameters(String path) {
bool shouldAddHeader = true;
for (var urlConstant in headerExclusionListOfAddedParameters) {
if (path.contains(urlConstant)) {
shouldAddHeader = false;
}
}
return shouldAddHeader;
}
} }

View File

@ -57,8 +57,6 @@ class HTTPService {
queryParameters: queryParameters, queryParameters: queryParameters,
options: options, options: options,
); );
// developer.log("status code is ${response.statusCode}");
// developer.log("response data is ${response.data}");
return expectedResponseModel(response.data); return expectedResponseModel(response.data);
} catch (error) { } catch (error) {
rethrow; rethrow;

View File

@ -12,47 +12,39 @@ class SpacesAPI {
static Future<List<SpaceModel>> getSpaces() async { static Future<List<SpaceModel>> getSpaces() async {
var uuid = var uuid =
await const FlutterSecureStorage().read(key: UserModel.userUuidKey); await const FlutterSecureStorage().read(key: UserModel.userUuidKey);
final response = await _httpService.get( try {
path: ApiEndpoints.spaces, final response = await _httpService.get(
queryParameters: { path: ApiEndpoints.spaces,
"userUuid": uuid, queryParameters: {
}, "userUuid": uuid,
showServerMessage: false, },
expectedResponseModel: (json) => SpaceModel.fromJsonList(json), showServerMessage: false,
); expectedResponseModel: (json) => SpaceModel.fromJsonList(json),
return response; );
return response;
} catch (e) {
rethrow;
}
} }
//get rooms by space id //get rooms by space id
static Future<List<RoomModel>> getRoomsBySpaceId(int spaceId) async { static Future<List<RoomModel>> getRoomsBySpaceId(int spaceId) async {
final response = await _httpService.get( try {
path: ApiEndpoints.rooms, final response = await _httpService.get(
queryParameters: {"homeId": spaceId}, path: ApiEndpoints.rooms,
showServerMessage: false, queryParameters: {"homeId": spaceId},
expectedResponseModel: (json) { showServerMessage: false,
List<RoomModel> rooms = []; expectedResponseModel: (json) {
for (var room in json) { List<RoomModel> rooms = [];
rooms.add(RoomModel.fromJson(room)); for (var room in json) {
} rooms.add(RoomModel.fromJson(room));
return rooms; }
}, return rooms;
); },
return response; );
} return response;
} catch (e) {
static Future<List<DeviceModel>> getDevicesByRoomId(int roomId) async { rethrow;
final response = await _httpService.get( }
path: ApiEndpoints.devicesByRoom,
queryParameters: {"roomId": roomId, "pageSize": 10},
showServerMessage: false,
expectedResponseModel: (json) {
List<DeviceModel> devices = [];
for (var device in json['devices']) {
devices.add(DeviceModel.fromJson(device));
}
return devices;
},
);
return response;
} }
} }