I'm fetching data in flutter and created a helper class as seen below
import 'dart:convert';
import 'package:http/http.dart' as http;
class HttpClient {
final Map<String, String> _headers = {};
void setHeader(String key, String value) {
if (!_headers.containsKey(key)) {
_headers[key] = value;
}
}
Future<http.Response> get(String url) async {
try {
var uri = Uri.parse(url);
var response = await http.get(uri, headers: _headers);
if (response.statusCode == 200) {
dynamic result = jsonDecode(response.body);
return result;
} else {
throw Error();
}
} on Exception catch (_) {
rethrow;
}
}
}
HttpClient appClient = HttpClient();
I call the method from a class as shown below
abstract class AbstractTodoService {
dynamic getTodos() {}
}
class HttpTodoService implements AbstractTodoService {
#override
Future<Response> getTodos() async {
try {
var todos =
await appClient.get('https://jsonplaceholder.typicode.com/todos');
print(todos);
return todos;
} on Exception catch (error) {
print(error);
rethrow;
}
}
}
However, when I print todos or any string in the HttpTodoService after the await call to the HttpClient I do not see anything. However, when I print the result inside the HttpClient I see the response but does not return. When I return a normal string or map everything works normally but when I attempt to use the jsonDecoded response nothing returns.
Your jsonDecode(response.body); returns a List<dynamic> type, but your function return type is Future<http.Response>. This is why you are not getting data.
You can check runtime datatype of a variable by
print(result.runtimeType); // variable_name.runtimeType
Change function return types to Future<List<dynamic>> of get(String url) and getTodos() functions.
It might be because your function returns a future of http.Response which is actually the type of the response after you use http.get. After you use jsonDecode you should get a Map<String, dynamic> which represent the json.
From the documentation:
By looking at the dart:convert documentation, you’ll see that you can decode the JSON by calling the jsonDecode() function, with the JSON string as the method argument.
Map<String, dynamic> user = jsonDecode(jsonString);
print('Howdy, ${user['name']}!');
print('We sent the verification link to ${user['email']}.');
Unfortunately, jsonDecode() returns a Map<String, dynamic>, meaning that you do not know the types of the values until runtime. With this approach, you lose most of the statically typed language features: type safety, autocompletion and most importantly, compile-time exceptions. Your code will become instantly more error-prone.
For example, whenever you access the name or email fields, you could quickly introduce a typo. A typo that the compiler doesn’t know about since the JSON lives in a map structure.
After you use jsonDecode you should turn the map into the object you want to work with using factory fromJson method.
You can read more about is in the documentation https://flutter.dev/docs/development/data-and-backend/json
Related
when I want to get a value from a json form in flutter, i face this error it says
type 'String' is not a subtype of type 'int' of 'index'
and Im using http: ^0.13.4
and this is my code
void getData() async{
Response res=await get("https://something.com");
String dat=res.body;
var datta=jsonDecode(dat)['title'];
print(datta);
}
how can I fix this problem?
dont forget to write Uri.parse
void getData() async{
Response res=await get(Uri.parse("https://something.com"));
String dat=res.body;
var datta=jsonDecode(dat)['title'];
print(datta);
}
You should handle the request in this way, worked for me:
void getData() async {
String baseUrl = "something.com";
String endpoint = "/your/endpoint";
Uri url = Uri.https(baseUrl, endpoint, {});
http.Response response = await http.get(url);
// Error handling
// if (response.statusCode != "200") {
// sendError();
// }
String stringBody = response.body;
}
Then you convert the string into a JSON object using dart:convert library:
import "dart:convert";
// Json object
Map<String, dynamic> jsonVar = json.decode(stringBody) as Map<String, dynamic>;
That should be enough, but I suggest you to implement a model class to handle your response, and another model to handle the kind of object you are working with (e.g. Book, Post, Product, etc).
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;
}
I have a selectedItems in the list:
List<String> _selectedItems = [];
Then, I want to pass this _selectedItems like this:
Future<void> _filterItem(List<String> _selectedItems) async {
ProgressDialog pr = ProgressDialog(context,
type: ProgressDialogType.Normal, isDismissible: false);
pr.style(message: "Loading...");
await pr.show();
http.post("https://one.com/myapp/php/load_data.php", body: {
"_selectedItems":_selectedItems,
}).then((res) {
if (res.body == "nodata") {
setState(() {
print("No Found");
});
} else {
setState(() {
var extractdata = json.decode(res.body);
tutordata = extractdata["mydata"];
});
}
}).catchError((err) {
print(err);
});
await pr.hide();
}
Passing this _selectedItems throws the error type 'List<String>' is not a subtype of type 'String' in type cast
What should I do?
body: takes <String, String>
and you are trying to give it <String, List<String>>
convert your List<String> to JSON string using jsonEncode
When exploring the documentation for http.post(), you can see what types the body can be:
body sets the body of the request. It can be a String, a List or a Map<String, String>
Right now, your code is passing a Map<String, List<String>> to the body, when it needs to be Map<String, String> instead:
http.post("https://one.com/myapp/php/load_data.php", body: {
"_selectedItems":_selectedItems,
})
You can do this:
import 'dart:convert';
http.post("https://one.com/myapp/php/load_data.php", body: {
"_selectedItems": jsonEncode(_selectedItems),
})
Btw, you can make some improvement on the code. First, for the logic flow, it's often good practice to separate the result of an API call to a setState() since it would be easier to anticipate the flow of the app. You can read more about FutureBuilder and StreamBuilder, as well as BLoC pattern.
For the clean code, you can apply principles from the styling documentation as well. Some adjustment can be made like using CamelCase instead of normal case, so extractData instead of extractdata.
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
I am trying to make a simple request to backend using rxDart. But the problem I face is that when I get a http error such as 404, onError is not called, however, it is possible to extract it in onData.
I have a little experience with RxJava + retrofit and there it works as expected, when there is a response with error http status code onError is called and can be handled appropriately.
1. What am I doing wrong, or is it intended behavior?.
Object sendProfileData() {
Stream<Response> stream = onboardingRepository.createUser(User(name: 'name', surname: 'surname', lat: 1.0, lng: 2.0));
stream.listen((response) {
print(response.statusCode);
setAttributes();
}, onError: (e) {
print(e);
});
}
OnboardingRepository.dart:
class OnboardingRepository {
Observable<Response> createUser(User user) {
return Observable.fromFuture(TMApi.createUser(user));
}
}
TMApi.dart:
class TMApi {
static Future<http.Response> createUser(User user) async {
String url = '$baseUrl/create_user';
return await http.post(url, body: json.encode(user.toJson()));
}
}
What would be the best way to handle the event in the View? There should be an error displayed if error occurs, otherwise it should open a new screen. sendProfileData() method will return an Object, based on that I am going to perform actions in the view, but that doesn't sound like a very elegant solution...
Any suggestions on architecture are welcome :)
the http library in dart works a bit different than Retrofit.
The Future returned by http.post only throws an exception when there is an io error (socket error, no internet).
Server responses like 404 are reflected in the http.Response.
I created a simple convenience method that might help you:
void throwIfNoSuccess(http.Response response) {
if(response.statusCode < 200 || response.statusCode > 299) {
print('http error!');
print(response.body);
throw new HttpException(response);
}
}
class HttpException implements Exception {
HttpException(this.response);
http.Response response;
}
How to use:
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<UserProfile> getUserProfile(String userId) async {
final url = 'https://example.com/api/users/$userId';
final response = await http.get(url);
throwIfNoSuccess(response);
final jsonBody = json.decode(response.body);
return UserProfile.fromJson(jsonBody);
}