Generic deserialization in Flutter / Dart - flutter

I'm trying to write a HTTP driver class that takes in a generic class and deserializes the response. I haven't found a good, clean way to do this in Flutter.
I've defined datamodel classes like this:
class MyClass {
String field1;
String field2;
MyClass.fromJson(Map<dynamic, dynamic> json)
: field1 = json["field1"],
field2 = json["field2"];
}
This works well and good if I do it manually...
MyClass makeRequest() {
Response response = http.get(url);
MyClass class = MyClass.fromJson(jsonDecode(response.body));
return class;
}
What I want, is to make a generic HTTP driver like this:
void makeRequest<T>() {
Response response = http.get(url);
T parsed = T.fromJson(jsonDecode(response.body));
return parsed;
}
Is there a way to do this in Flutter/Dart? I've been trying to figure out the right syntax to use a base class and extends but haven't gotten it. Any ideas?

This is what I usually use in my network call, feel free to use. Btw, I recommend the dio package for convenient headers and params config, as well as other error handling features.
// Define an extension
extension BaseModel on Type {
fromJson(Map<String, dynamic> data) {}
}
// For single object
Future<T> makeGetRequest<T>({String url, Map<String, dynamic> params}) {
return http
.get(buildUrl(url, params)) // Don't need the buildUrl() if you use Dio
.then((response) => handleJsonResponse(response))
.then((data) => T.fromJson(data));
// For list of object
Future<List<T>> makeGetRequestForList<T>({String url, Map<String, dynamic> params}) {
return http
.get(buildUrl(url, params)) // Don't need the buildUrl() if you use Dio
.then((response) => handleJsonResponse(response))
.then((data) => List<T>.from(data.map((item) => T.fromJson(item)));
}
// Helper classes without Dio
String buildUrl(String url, [Map parameters]) {
final stringBuilder = StringBuffer(url);
if (parameters?.isNotEmpty == true) {
stringBuilder.write('?');
parameters.forEach((key, value) => stringBuilder.write('$key=$value&'));
}
final result = stringBuilder.toString();
print(result);
return result;
}
// With Dio, you can simply do this:
final res = await API().dio
.get(url, queryParameters: params) // Don't need the [buildUrl] here
.then((response) => handleJsonResponse(response))
.then((data) => T.fromJson(data));
// Handle JSON response
handleJsonResponse(http.Response response, [String endpoint = '']) {
print(
'API: $endpoint \nCODE: ${response.statusCode} \nBODY: ${response.body}');
if (_okStatus.contains(response.statusCode)) {
return jsonDecode(response.body);
}
if (response.statusCode == HttpStatus.unauthorized) {
throw Exception(response.statusCode);
} else {
throw Exception("HTTP: ${response.statusCode} ${response.body}");
}
}
Usage:
// Example class
class Post {
final String title;
Post({this.title});
#override
Post.fromJson(Map<String, dynamic> data) : title = data['title'];
}
// Use the function
Future<Post> getPost() async {
final result = await makeGetRequest<Post>(params: {'post_id': 1});
return result;
}

Related

Dependency Injection In Flutter Clean Architecture with Provider State Management

I am trying to wrap my head around clean architecture and I'd be grateful if someone could explain to me how to implement dependency injection in a flutter front end, and give me a basic idea of why it's useful.
I've been trying to make it work with get_it but there's so much info out there it just overwhelming so maybe if I could get one explanation on how to do this in the following case, I'd appreciate that very much.
Just a quick thank you in advance for reading through the below code.
member_remote_data.dart file inside data layer:
abstract class MemberRemoteDataSource {
Future create({required Map<String, dynamic> member});
}
class MemberRemoteDataSourceImpl implements MemberRemoteDataSource {
final http.Client _client = http.Client();
final String _apiHost = Environment().config!.apiHost;
#override
Future create({required Map<String, dynamic> member}) async {
final String url = "$_apiHost/api/member/create";
final Uri uri = Uri.parse(url);
final Map<String, String> headers = {
'Content-Type': 'application/json',
};
final http.Response response = await _client.post(
uri,
headers: headers,
body: member['type'] == 1
? ParentModel.toJson(member)
: ChildModel.toJson(member),
);
if (response.statusCode >= 200 && response.statusCode < 300) {
return jsonDecode(response.body);
} else if (response.statusCode == 422) {
return ValidationError(error: response.body);
} else if (response.statusCode == 404) {
return HttpPageNotFoundError();
} else if (response.statusCode == 500) {
return HttpInternalServerError();
} else if (response.statusCode == 400) {
return HttpBadRequestError(error: response.body);
}
}
}
member_repositroy_impl.dart file inside data layer:
class MemberRepositoryImpl implements MemberRepository {
MemberRepositoryImpl({required this.memberRemoteDataSource});
final MemberRemoteDataSource memberRemoteDataSource;
#override
Future create({required Map<String, dynamic> member}) async {
return await memberRemoteDataSource.create(member: member);
}
}
member_repository.dart file inside domain layer:
abstract class MemberRepository {
Future create({required Map<String, dynamic> member});
}
create_member.dart use case file inside domain layer:
class CreateMember implements UseCase {
CreateMember({required this.memberRepository});
final MemberRepository memberRepository;
#override
Future create({required Map<String, dynamic> member}) async {
return await memberRepository.create(member: member);
}
}
Now, inside the presentation layer, I have my change notifiers where I want to "connect" all of the above together.
How do I do that? How do I add those parameters in a way that makes sens in clean architecture?
enum NotifierState { initial, loading, loaded }
class MemberNotifier extends ChangeNotifier {
// How do I "connect" everything together
// here to show this on the front end?
final UseCase createMember = CreateMember();
NotifierState _notifierState = NotifierState.initial;
NotifierState get notifierState => _notifierState;
void _setState(NotifierState notifierState) {
_notifierState = notifierState;
notifyListeners();
}
String? _emailAlreadyExists;
String? get emailAlreadyExists => _emailAlreadyExists;
void setFailure(emailAlreadyExists) {
_emailAlreadyExists = emailAlreadyExists;
notifyListeners();
}
Future create({required Map<String, dynamic> member}) async {
// _setState(NotifierState.loading);
var body = await createMember.create(member: member);
if (body is ValidationError) {
setFailure(body.emailAlreadyExists());
_setState(NotifierState.loaded);
} else {}
_setState(NotifierState.loaded);
return body;
}
}

Flutter mockito post always return null

I'm trying to test my code that makes a post to login from an API using a mocked http client, but instead of returning what I asked for, it returns null, I did the same test but changing the endpoint and method to GET and it worked perfectly. I'm currently using flutter's http to make the requests, but I've already tested it with Dio and the result was the same, below is my code
Future<String> signIn(String email, String password) async {
final Map<String, dynamic> body = {"email": email, "password": password};
final String url = url_base + Urls.auth_login;
final Map<String, String> customHeader = {
"Content-type": "application/json",
};
String returnCode;
try {
var x = jsonEncode(body);
http.Response response = await client.post(Uri.parse(url), body: x, headers: customHeader);
var parsedJson = json.decode(response.data);
if (parsedJson.containsKey("token")) {
returnCode = parsedJson["token"];
} else {
returnCode = parsedJson["non_field_errors"][0];
}
}catch (e) {
throw ServerException();
}
if (returnCode == null) {
throw ServerException();
} else {
return returnCode;
}
}
and the test case:
class ClientMock extends Mock implements http.Client {}
void main() {
RemoteData remoteData;
group('Test signIn', () {
test('Login with email and wrong password', () async {
final clientMock = ClientMock();
remoteData = RemoteData(client: clientMock);
String jsonMockResponse =
'{non_field_errors: [Unable to log in with provided credentials.]}';
when(clientMock.post(any))
.thenAnswer((_) async => http.Response(jsonMockResponse, 400));
String loginReturn =
await remoteData.signIn('test#email.com', 'password123');
expect(loginReturn,throwsA(const TypeMatcher<ServerException>()));
});
}
I've already tested some things like changing 'any' for exactly the same thing the real function gets and it didn't work either.
The actual test return 'Instance of 'ServerException'', an in debug mode i could see that the return is null, and the last if is the one who throws this exception.

flutter return future list in a var to use outside the loop

Hello I'm trying to recuperate the list value of a database.
i can but what i want is to export the result in a var so i can use in all my code just by calling "print(myList);"
this is my code :
static const URL =
'https://xxxhost/employee_actions3.php';
static Future<List<Employee>> getEmployees() async {
try {
final response = await http.post(Uri.parse(
URL,
));
print("getEmployees >> Response:: ${response.body}");
if (response.statusCode == 200) {
List<Employee> list = parsePhotos(response.body);
return list;
} else {
throw <Employee>[];
}
} catch (e) {
return <Employee>[];
}
}
and my classe Employee
class Employee {
String id;
String firstName;
String lastName;
Employee({required this.id, required this.firstName, required this.lastName});
factory Employee.fromJson(Map<String, dynamic> json) {
return Employee(
id: json['id'] as String,
firstName: json['lat'] as String,
lastName: json['lng'] as String,
);
}
}
can i have help please ?
There are two ways to access async data in most modern languages, including dart, they are:
1. By providing a callback then
2. By using the function in an async context and awaiting the result
I've wrapped the code above in a class called API so the examples below are easier to follow,
class API {
static const URL = 'https://xxxhost/employee_actions3.php';
static Future<List<Employee>> getEmployees() async {
try {
final response = await http.post(Uri.parse(URL));
print("getEmployees >> Response:: ${response.body}");
if (response.statusCode == 200) {
List<Employee> list = parsePhotos(response.body);
return list;
} else {
throw("${response.statusCode} Failed to parse photos");
}
} catch (e) {
throw e;
}
}
}
Method 1: Providing a callback to .then, this method will allow you to work with async actions in a synchronous context, but be aware it will not halt the execution flow.
void main() {
API.getEmployees().then((resp) => print(resp)).catchError(e) => print(e);
}
Method 2: Async/Await, this method will allow you to access the data inline, that is var x = await myAsyncFunc() remember the await keyword requires the function to be called within an async context. And the await keyword will halt the execution flow till the future completes.
void main() async {
try {
final list = await API.getEmployees();
print(list);
} catch (e) {
print(e);
}
}
Using either one of the two methods outlined above will allow you to access the data of the list later on.
Additional Reading:
Async programming in dart
Futures and error handling

Flutter generic REST API call function

I am working on first my flutter app. App required to call rest api and return back result. I am looking to create generic function to call rest api. I have written below code but I am not understating, how can I decode api response in specific model.
Future<T> apiRequest<T>(
String endPoint,
RequestMethod method, {
String body = '',
String token = '',
}) async {
http.Response resp;
final String url = LocalConstants.apiBaseUrl + endPoint;
final Map<String, String> headers = new Map<String, String>();
headers.putIfAbsent(
HttpHeaders.contentTypeHeader, () => 'application/json');
if (token != null && token.isNotEmpty) {
headers.putIfAbsent(
HttpHeaders.authorizationHeader, () => 'Bearer ' + token);
}
try {
if (method == RequestMethod.get) {
resp = await http.get(
url,
headers: headers,
);
} else if (method == RequestMethod.put) {
resp = await http.put(
url,
headers: headers,
body: body,
);
} else if (method == RequestMethod.post) {
resp = await http.post(
url,
headers: headers,
body: body,
);
} else if (method == RequestMethod.delete) {
resp = await http.delete(
url,
headers: headers,
);
}
if (resp != null && this.validateResponse(resp)) {
return json.decode(resp.body);
}
// else {
// Response resp = new Response();
// resp.respMsg = LocalConstants.genericError;
// resp.respCode = LocalConstants.resp_failure;
// Response.
// }
} on TimeoutException catch (e) {
//handleTimeout();
} on SocketException catch (e) {
print('Socket Error: $e');
//handleTimeout();
} on Error catch (e) {
print('General Error: $e');
//showError();
}
}
Below is code which I can use to call rest api
await ApiService.newInstance(context)
.apiRequest<GenericResp>('/api/people', RequestMethod.get);
Here is my GenericResp class
import 'package:project/models/Response.dart';
class GenericResp extends Response {
int id;
int otherId;
String mappingId;
GenericResp({
this.id,
this.otherId,
this.mappingId,
});
GenericResp.fromJson(Map<String, dynamic> json) {
id = json['id'];
otherId = json['other_id'];
mappingId = json['mapping_id'];
respCode = json['resp_code'];
respMsg = json['resp_msg'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = this.id;
data['other_id'] = this.otherId;
data['mapping_id'] = this.mappingId;
data['resp_code'] = this.respCode;
data['resp_msg'] = this.respMsg;
return data;
}
}
How can I decode body json.decode(resp.body); to GenericResp of type T?
You can add a generic argument that will deserialize your json data to GenericResp. Something like that:
Future<T> apiRequest<T>(
String endPoint,
RequestMethod method, T Function(Object json) fromJson, {
String body = '',
String token = '',
}) async { ... }
And after json decoding you are to use fromJson argument:
if (resp != null && this.validateResponse(resp)) {
return fromJson(json.decode(resp.body));
}
And then a call would look like this:
await ApiService.newInstance(context).apiRequest<GenericResp>('/api/people',
RequestMethod.get, (json) => GenericResp.fromJson(json));

How do I call a method as a parameter from a subclass in the Object class in dart?

I have a method inside a service class:
#override
Future<String> registerNewVoter(Object deviceAppInfo) async {
Dio dio = new Dio();
final url = API().endpointVoterUri(EndpointVoter.newVoter).toString();
final header = {'Content-type': 'application/json'};
final data = await deviceAppInfo; ///need to call the method getInfo() on the Object class which returns a future
final response =
await dio.post(url, data: data, options: Options(headers: header));
if (response.statusCode == 200) {
Map map = response.data;
final uuid = map['result']['voter_uuid'];
return uuid;
}
print(
'Request $url failed\nResponse: ${response.statusCode} ${response.statusMessage}');
throw response;
}
I'm using type Object deviceAppInfo as a parameter in the method to keep the service as pure as possible(adhering to mvvm principles). The subclass is DeviceAppInfo which has an async method called getInfo()(and where the data comes from) which is supposed to be assigned to data(see the comments in the code). I'm struggling to see how I can keep the class decoupled from DeviceAppInfo class. Any suggestions...? I'm thinking of calling a factory constructor but not sure how to implement it. Here is my DeviceAppInfo class:
class DeviceAppInfo {
DeviceAppInfo({
this.platform,
this.platformVersion,
this.appVersion,
});
final String platform;
final String platformVersion;
final String appVersion;
Map<String, dynamic> toMap() => {
'platform': this.platform,
'platform_version': this.platformVersion,
'app_version': this.appVersion,
};
Future<Map<String, dynamic>> getInfo() async {
final values = await Future.wait([
getPlatform(),
getPlatformVersion(),
getProjectVersion(),
]);
return DeviceAppInfo(
platform: values[0],
platformVersion: values[1],
appVersion: values[2],
).toMap();
}
Future<String> getPlatform() async {
try {
if (Platform.isIOS) {
return 'ios';
}
return 'android';
} on PlatformException catch (e) {
return e.toString();
}
}
Future<String> getPlatformVersion() async {
try {
final platformVersion = await GetVersion.platformVersion;
return platformVersion;
} on PlatformException catch (e) {
return e.toString();
}
}
Future<String> getProjectVersion() async {
try {
final projectVersion = await GetVersion.projectVersion;
return projectVersion;
} on PlatformException catch (e) {
return e.toString();
}
}
}
I believe that DeviceAppInfo is a clear collaborator of your service, and hiding it behind Object is simply bad engineering:
it will make your Api hard to use correctly and easy to use incorrectly
Your api is no longer self-documenting, without reading the docs or code it is impossible to use it correctly.
However, it can be discussed if it should be exposed as a parameter or provided to the constructor of your service.
Having said that, There are at least 3 options that will decouple your service from DeviceAppInfo:
Option 1: Pass in the result of getInfo() to your method
least questionable and a common form of decoupling inbound data
I am a bit sceptical if you use a Map as an input type, it is still easy to provide a map with incorrect keys
Option 2: take a function as an argument
Function a bit harder to use, it is not evident what functions accross the codebase can be used (compared to a class)
Option 3: cast to dynamic
Please dont do that
Most closely matches your goal from question
function is extremely hard to use correctly Without reading docs / code
You change compile-time errors to runtime errors
Is this what you want?
#override
Future<String> registerNewVoter(DeviceAppInfo deviceAppInfo) async {
Dio dio = new Dio();
final url = API().endpointVoterUri(EndpointVoter.newVoter).toString();
final header = {'Content-type': 'application/json'};
final data = await deviceAppInfo.getInfo();
final response =
await dio.post(url, data: data, options: Options(headers: header));
if (response.statusCode == 200) {
Map map = response.data;
final uuid = map['result']['voter_uuid'];
return uuid;
}
print(
'Request $url failed\nResponse: ${response.statusCode} ${response.statusMessage}');
throw response;
}
NOTE: I just changed the type of deviceAppInfo from Object to DeviceAppInfo