Response Parser makes it easier to parse data and error responses from the server.
Do you want to write pretty functions like this...
Future<Either<ApiFailure, User>> fetchUser() async {
return parseApiResponse(
requestAction: () => dio.get('/user'),
mapper: User.fromJson,
);
}
...instead of this boring code?
Future<Either<ApiFailure, User>> fetchUser() async {
final dio = Dio(BaseOptions(baseUrl: 'https://example.com'));
try {
final request = await dio.get('/user');
final data = request.data?['data'];
if (data == null) {
final error = request.data?['error'];
if (error != null) {
return left(ApiFailure.serverFailure(error['message']));
} else {
return left(ApiFailure.unknown());
}
} else {
return right(User.fromJson(data));
}
} catch (error, st) {
ApiFailure? apiFailure;
if (error is DioError) {
final responseFailure = error.response?.data;
if (responseFailure is Map<String, dynamic>) {
apiFailure = ApiFailure.serverFailure(responseFailure['message']);
} else {
apiFailure = ApiFailure.httpError(error.response?.statusCode);
}
}
return left(apiFailure ?? ApiFailure.unknown());
}
}
Then continue reading!
Don't know what is `Either`?
It's a type from fpdart
package.
It's used to return either error (left) or data (right).
To do so, you need to do a little preparation.
For example, let's assume your server returns such response:
{
"data": {
// Data you requested
},
"error": {
// Server error which you should parse and show to user
"message": "Something went wrong"
}
}
And your error model looks this way:
class ApiFailure {
factory ApiFailure.unknown() = _UnknownApiFailure;
factory ApiFailure.serverFailure(String errorMessage) = _ServerFailure;
factory ApiFailure.httpError(int? statusCode) = _HttpError;
}
Then you need to implement dataExtractor
, failureParser
and errorCatcher
this way:
final _exampleResponseParser = ResponseParser<Response, ApiFailure>(
dataExtractor: (response) => response.data['data']!,
failureParser: (response) {
final error = json['error'];
if (error is Map<String, dynamic>) {
return ApiFailure.serverFailure(error['message']);
} else {
return null;
}
},
errorCatcher: (error, stackTrace) {
ApiFailure? apiFailure;
if (error is DioError) {
apiFailure = ApiFailure.httpError(error.response?.statusCode);
}
return apiFailure ?? ApiFailure.unknown();
},
);
And create top level parseApiResponse
, parseListApiResponse
and parseEmptyApiResponse
functions.
final parseApiResponse = _exampleResponseParser.parseApiResponse;
final parseListApiResponse = _exampleResponseParser.parseListApiResponse;
final parseEmptyApiResponse = _exampleResponseParser.parseEmptyApiResponse;
This diagram shows how parseApiResponse
works under the hood:
Actually, everything in the parseApiResponse
method is wrapped with the try-catch
block.
So this method is safe and can't throw any exceptions.
Instead of creating top-level functions, you can create a class
which extends ResponseParserBase
and overrides it's methods:
class DefaultResponseParser extends ResponseParser<Response, ApiFailure>{
Object extractData(Response response) => response.data['data']!;
Failure? parseFailure(Response response) {
final error = json['error'];
if (error is Map<String, dynamic>) {
return ApiFailure.serverFailure(error['message']);
} else {
return null;
}
};
Failure catchError(Object error, StackTrace stackTrace) {
ApiFailure? apiFailure;
if (error is DioError) {
apiFailure = ApiFailure.httpError(error.response?.statusCode);
}
return apiFailure ?? ApiFailure.unknown();
};
}
That's all!
For more info, you can take a look at the example.