I'm learning Flutter and this is how I handle a post http request right now
ApartmentRepository class:
Future<bool> createApartment(Apartment apartment) async {
String path = baseUrl;
initAdapter(_dio);
_dio.options.headers['accept'] = 'application/json';
_dio.options.headers['Content-Type'] = 'application/json';
var response = await _dio.post(path, data: apartment.toJson());
if (response.statusCode == 201) {
return true;
}
return false;
}
ApartmentForm calss :
child: StyledElevatedSaveFormButton(
onPressed: () async {
if (_apartmentFormKey.currentState?.validate() ?? false) {
_apartmentFormKey.currentState?.save();
Apartment newApartment = Apartment(
ref: refController.text,
apartmentType: _selectedApartmentType.type,
area: int.parse(areaController.text),
address: addressController.text,
zipCode: zipCodeController.text,
city: cityController.text,
);
bool result = false;
if (widget.apartment?.id == null) {
result = await ApartmentRepository().createApartment(newApartment);
} else {
Apartment newApartmentUpdate = newApartment.copyWith(iriId: widget.apartment!.iriId);
result = await ApartmentRepository().updateApartment(newApartmentUpdate);
}
if (result == true) {
// make push and show snackBar
onButtonPressed();
} else {}
}
},
isLoding: isLoading,
),
As you can see, I do return a bool var, but now I'd like to be able to return and Apartment object if the status code is 201, or if the status code is 422 so return the json errors object witch contains an array of violations
This is how the api returns the json body if the code is 422
And this is a real example:
{
"status": 422,
"violations": [
{
"propertyPath": "ref",
"message": "This value is too short. It should have 20 characters or more.",
"code": "9ff3fdc4-b214-49db-8718-39c315e33d45"
},
{
"propertyPath": "address",
"message": "This value is too short. It should have 200 characters or more.",
"code": "9ff3fdc4-b214-49db-8718-39c315e33d45"
}
],
"detail": "ref: This value is too short. It should have 20 characters or more.\naddress: This value is too short. It should have 200 characters or more.",
"type": "/validation_errors/0=9ff3fdc4-b214-49db-8718-39c315e33d45;1=9ff3fdc4-b214-49db-8718-39c315e33d45",
"title": "An error occurred"
}
For that I have created and http Violations class but I don't understand how to apply it in the http request and display violations messages on the screen inside the form.
HttpViolations class :
import 'dart:convert';
// code 422
class HttpViolations {
final int status;
final List<Violation> violations;
final String detail;
final String type;
final String title;
HttpViolations({
required this.status,
required this.violations,
required this.detail,
required this.type,
required this.title,
});
factory HttpViolations.fromRawJson(String str) => HttpViolations.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory HttpViolations.fromJson(Map<String, dynamic> json) => HttpViolations(
status: json["status"],
violations: List<Violation>.from(json["violations"].map((x) => Violation.fromJson(x))),
detail: json["detail"],
type: json["type"],
title: json["title"],
);
Map<String, dynamic> toJson() => {
"status": status,
"violations": List<dynamic>.from(violations.map((x) => x.toJson())),
"detail": detail,
"type": type,
"title": title,
};
}
class Violation {
final String propertyPath;
final String message;
final String code;
Violation({required this.propertyPath, required this.message, required this.code});
factory Violation.fromRawJson(String str) => Violation.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Violation.fromJson(Map<String, dynamic> json) =>
Violation(propertyPath: json["propertyPath"], message: json["message"], code: json["code"]);
Map<String, dynamic> toJson() => {"propertyPath": propertyPath, "message": message, "code": code};
}
Create Failure class
class Failure implements Exception {
final String message;
Failure(this.message);
@override
String toString() => message;
}
Repository fix
You need to make createApartment function to return Apartment (I assume that the response of your createApartment API is Apartment class) and if it's error then throw Failure.
Future<Apartment> createApartment(Apartment apartment) async {
final String path = baseUrl;
initAdapter(_dio);
_dio.options.headers['accept'] = 'application/json';
_dio.options.headers['Content-Type'] = 'application/json';
final response = await _dio.post(
path,
data: apartment.toJson(),
);
if (response.statusCode == 201) {
return Apartment.fromJson(response.data);
} else if (response.statusCode == 422) {
final violations = HttpViolations.fromJson(response.data);
final message = violations.violations
.map((v) => "${v.propertyPath}: ${v.message}")
.join("\n");
throw Failure(message);
} else {
throw Failure("Unexpected error: ${response.statusCode}");
}
}
In that code, when statusCode is 201, then we parsed response.data to Apartment then return it. But if it's not 201, then we structured the violation message. Next step I will explain how to show the error message properly.
Handling in the form
In your StatefulWidget, add a Map<String, String> to store field-level errors.
Map<String, String> fieldErrors = {};
Then modify code inside your StyledElevatedSaveFormButton to catch the exceptions and update fieldErrors.
StyledElevatedSaveFormButton(
onPressed: () async {
if (_apartmentFormKey.currentState?.validate() ?? false) {
_apartmentFormKey.currentState?.save();
Apartment newApartment = Apartment(
ref: refController.text,
apartmentType: _selectedApartmentType.type,
area: int.parse(areaController.text),
address: addressController.text,
zipCode: zipCodeController.text,
city: cityController.text,
);
try {
if (widget.apartment?.id == null) {
await ApartmentRepository().createApartment(newApartment);
} else {
Apartment newApartmentUpdate =
newApartment.copyWith(iriId: widget.apartment!.iriId);
await ApartmentRepository().updateApartment(newApartmentUpdate);
}
// make push and show snackBar
onButtonPressed();
} catch (e) {
// Parse violation messages back into fieldErrors
if (e is Failure) {
final newErrors = <String, String>{};
final msg = e.toString();
for (var line in msg.split("\n")) {
final parts = line.split(":");
if (parts.length > 1) {
final field = parts[0].trim();
final errorMsg = parts.sublist(1).join(":").trim();
newErrors[field] = errorMsg;
}
}
setState(() {
fieldErrors = newErrors;
});
}
}
}
},
isLoding: isLoading,
),
Showing errors in the form
In each of your TextFormField you must add errorText with the value of fieldErrors. If the errorText is not null, there will be a red error message below your TextFormField.
TextFormField(
controller: refController,
decoration: InputDecoration(
labelText: "Reference",
errorText: fieldErrors["ref"],
),
),
TextFormField(
controller: addressController,
decoration: InputDecoration(
labelText: "Address",
errorText: fieldErrors["address"],
),
),
This way, we have successfully return the appropriate data and show human-readable error. Note that if you don't want to show the error in each of your TextFormField, you can always show all errors directly by showing a SnackBar when you catch the error. Let me know if you have any questions.