Generic deserialization in Flutter / Dart
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?
1 answer
-
answered 2021-04-08 04:02
Bach
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; }
See also questions close to this topic
-
Getting the value of Dropdown Menu selection and sending it to Firebase ( Flutter )
In my Flutter project, I have a form which is consisted of different fields that when completed, are sent collectively to Firebase in a new document. Said form has various text inputs and two dependent drop-down menus ( by dependent, I mean that the selection of the first dropdown changes the items which will populate the second drop-down menu). What I want to achieve, is to take the values from both drop-down menus and send them along with the rest of the details to the document in firebase. As concerning the code, the code which is responsible for sending the information as a document to the Firebase is this:
Future<void> addUser() { // Call the user's CollectionReference to add a new user return _firebaseAuthServices.productsReference .add({ 'name': nameController.text, // Size ( Size Format (value of drop-down menu 1), Size Number (value of drop-down menu 2) ) 'price': priceController.text, 'description': descController.text, 'images': FieldValue.arrayUnion([imageUrl]), }) .then((value) => print("Product Added")) .catchError((error) => print("Failed to add product: $error")); }
and the code for the dependent drop-down menus is this:
child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ DropdownButton<String>( items: [ DropdownMenuItem<String>( value: "EU", child: Center( child: Text("EU"), ), ), DropdownMenuItem<String>( value: "Length (cm)", child: Center( child: Text("Length (cm)"), ), ), DropdownMenuItem<String>( value: "UK", child: Center( child: Text("UK"), ), ), ], onChanged: (_value) => valueChanged(_value), hint: Text("Select Size Format"), ), DropdownButton<String>( items: menuItems, onChanged: disabledDropdown ? null : (_value) => secondValueChanged(_value), hint: Text("Select Size Number"), ), ], ),
How can I get seperately the values from both drop-down Menus and send them to Firebase ?
-
Flutter: disabling textfields based on toggle button and ignoring their textfield texteditingcontroller requirements
I am trying to have my the title of my toggle buttons cause different calculations to be preformed. I also have the visibility of some textfields based on the selection of the toggle buttons. However, I can not get my app to run without providing actual values in each textfield. Is there a way to prevent this requirement? i.e. I don't need to provide values for the percent removed, flow rate, and the SCr but instead only provide one of those.
import 'package:pocketpk/models/calculator_vanc.dart'; import 'package:pocketpk/screens/vancomycin/vanc_results_screen.dart'; import 'package:pocketpk/widgets/input_row.dart'; import 'package:pocketpk/widgets/my_unit.dart'; import 'package:pocketpk/widgets/my_button.dart'; import 'package:pocketpk/widgets/make_buttons.dart'; import 'package:pocketpk/constants.dart'; import 'package:pocketpk/widgets/rounded_button.dart'; import 'package:auto_size_text/auto_size_text.dart'; import 'dart:math'; class VancDosingScreen extends StatefulWidget { static const String id = 'vancomycin dosing screen'; @override _VancDosingScreenState createState() => _VancDosingScreenState(); } class _VancDosingScreenState extends State<VancDosingScreen> { final MyButton genderSelected = MyButton(); final MyButton renalReplacementTherapy = MyButton(); final TextEditingController weightController = TextEditingController(); final TextEditingController heightController = TextEditingController(); final TextEditingController creatController = TextEditingController(); final TextEditingController ageController = TextEditingController(); final TextEditingController hDController = TextEditingController(); final TextEditingController flowRateController = TextEditingController(); final MyUnit heightUnit = MyUnit(); final MyUnit weightUnit = MyUnit(imperial: 'lbs', metric: 'kg'); final MyUnit creatUnit = MyUnit(imperial: 'mg/dL', metric: 'mg/dL'); final MyUnit ageUnit = MyUnit(imperial: 'years', metric: 'years'); final MyUnit hDUnit = MyUnit(imperial: '%', metric: '%'); final MyUnit flowRateUnit = MyUnit(imperial: 'mL/hr', metric: 'L/hr'); List<bool> isSelected = [ false, false, ]; List<bool> rRTIsSelected = [false, false, false]; String buttonName = 'didntWork'; String rRTButtonName = 'didntWork'; @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, body: Column(children: <Widget>[ ClipPath( clipper: MyClipper(), child: Container( height: MediaQuery.of(context).size.height * 0.28, width: double.infinity, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topRight, end: Alignment.bottomLeft, colors: [ Color(0xff3383CD), Color(0xff11249F), ], ), image: DecorationImage( image: AssetImage('images/equations1.png'), fit: BoxFit.fill, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Padding(padding: EdgeInsets.all(20)), AppBar( leading: null, actions: <Widget>[ IconButton( icon: Icon(Icons.close), onPressed: () { Navigator.pop(context); }), ], title: Text('Vancomycin'), backgroundColor: Colors.transparent, elevation: 0.0, ), ], ), ), ), Container( width: MediaQuery.of(context).size.width * 0.9, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Text( 'Gender', style: TextStyle( color: Colors.black, fontSize: 20, ), ), ValueListenableBuilder<Option>( valueListenable: genderSelected, builder: (context, option, _) => MakeButtons( num0: 3, num1: 5, makeButtonWidth: MediaQuery.of(context).size.width * 0.20, selected: option, onChanged: (newOption) => genderSelected.option = newOption, ), ), ], ), ), InputRow( myUnit: heightUnit, inputParameter: 'height', textField: heightController, colour: kEmoryDBlue, ), InputRow( myUnit: weightUnit, inputParameter: 'weight', textField: weightController, colour: kEmoryDBlue, ), InputRow( myUnit: ageUnit, inputParameter: 'Age', textField: ageController, colour: kEmoryDBlue, ), Container( width: MediaQuery.of(context).size.width * 0.9, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ AutoSizeText( 'Renal Replacement Therapy', style: TextStyle( color: Colors.black, ), ), ToggleButtons( color: kEmoryDBlue, selectedColor: Colors.white, fillColor: kEmoryDBlue, children: <Widget>[ Container( padding: EdgeInsets.fromLTRB(12, 12, 12, 12), constraints: BoxConstraints.expand(width: 100, height: 56), child: Center( child: Text( 'Yes', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w500), ), ), ), Container( padding: EdgeInsets.fromLTRB(12, 12, 12, 12), constraints: BoxConstraints.expand(width: 100, height: 56), child: Center( child: Text( 'No', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w500), ), ), ), ], isSelected: isSelected, onPressed: (int index) { setState(() { for (int indexBtn = 0; indexBtn < isSelected.length; indexBtn++) { if (indexBtn == index) { isSelected[indexBtn] = !isSelected[indexBtn]; } else { isSelected[indexBtn] = false; } } buttonName = isSelected[index] == isSelected[0] ? 'Yes' : 'No'; }); }, ), ], ), ), Visibility( visible: isSelected[0], child: Container( width: MediaQuery.of(context).size.width * 0.9, child: Center( child: ToggleButtons( color: kEmoryDBlue, selectedColor: Colors.white, fillColor: kEmoryDBlue, children: <Widget>[ Container( padding: EdgeInsets.fromLTRB(12, 12, 12, 12), constraints: BoxConstraints.expand(width: 125, height: 56), child: Center( child: Text( 'HD', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w500), ), ), ), Container( padding: EdgeInsets.fromLTRB(12, 12, 12, 12), constraints: BoxConstraints.expand(width: 125, height: 56), child: Center( child: Text( 'CRRT', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w500), ), ), ), Container( padding: EdgeInsets.fromLTRB(12, 12, 12, 12), constraints: BoxConstraints.expand(width: 125, height: 56), child: Center( child: Text( 'PD', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w500), ), ), ), ], isSelected: rRTIsSelected, onPressed: (int index) { setState(() { for (int indexBtn = 0; indexBtn < rRTIsSelected.length; indexBtn++) { if (indexBtn == index) { rRTIsSelected[indexBtn] = !rRTIsSelected[indexBtn]; } else { rRTIsSelected[indexBtn] = false; } } if (rRTIsSelected[index] == rRTIsSelected[0] && isSelected[0]) { return rRTButtonName = 'HD'; } else if (rRTIsSelected[index] == rRTIsSelected[1] && isSelected[0]) { return rRTButtonName = 'CRRT'; } else if (isSelected[0]) { return rRTButtonName = 'PD'; } else { return rRTButtonName = 'X'; } }); }, ), ), ), ), Visibility( visible: rRTIsSelected[0] & isSelected[0], child: InputRow( myUnit: hDUnit, enable: rRTButtonName == 'HD' ? true : false, inputParameter: 'Estimated Percent Removed by Dialysis', textField: hDController, colour: kEmoryDBlue, ), ), Visibility( visible: rRTIsSelected[1] & isSelected[0], child: InputRow( myUnit: flowRateUnit, enable: rRTButtonName == 'CRRT' ? true : false, inputParameter: 'Flow rate', textField: flowRateController, colour: kEmoryDBlue, ), ), Visibility( visible: isSelected[1], child: InputRow( myUnit: creatUnit, enable: buttonName == 'No' ? true : false, inputParameter: 'SCr', textField: creatController, colour: kEmoryDBlue, ), ), RoundedButton( title: 'Calculate', onPressed: () { double weightSelected; double height = heightUnit == 'cm' ? double.parse(heightController.text) : double.parse(heightController.text) * 2.54; double weight = weightUnit == 'kg' ? double.parse(weightController.text) : double.parse(weightController.text) / 2.2; double ideal = genderSelected.title == 'Female' ? 45 + 2.3 * ((height - 152.4) / 2.54) : 50 + 2.3 * ((height - 152.4) / 2.54); double adjust = (weight - ideal) * 0.4 + ideal; if (weight > 1.3 * ideal) { weightSelected = adjust; } else if (ideal > weight) { weightSelected = weight; } else { weightSelected = ideal; } double isFemale = genderSelected.title == 'Female' ? 0.85 : 1.00; double crcl = (((140 - double.parse(ageController.text)) * weightSelected) / (72 * double.parse(creatController.text))) * isFemale; double percentRemoved = double.parse(hDController.text); double flowRate = flowRateUnit == 'L/hr' ? double.parse(flowRateController.text) * 1000 : double.parse(flowRateController.text); double ke = 0.00083 * crcl + 0.0044; double tau = (log(35 / 17.5) / ke + 1); double buttonsPushed; double updateRoundedTau; double roundedTau() { if (tau < 7) { return 6; } else if (tau < 10) { return 8; } else if (tau < 15) { return 12; } else if (tau < 20) { return 18; } else if (tau < 30) { return 24; } else if (tau < 40) { return 36; } else if (tau < 55) { return 48; } else { return 72; } } if (isSelected[1]) { buttonsPushed = 0; updateRoundedTau = roundedTau(); flowRate = 0; percentRemoved = 0; } else if (isSelected[0] && rRTIsSelected[0]) { buttonsPushed = 10; percentRemoved; flowRate = 0; crcl = 0; ke = 0; tau = 0; updateRoundedTau = 0; } else if (isSelected[0] && rRTIsSelected[1]) { buttonsPushed = 11; percentRemoved = 0; crcl = 0; ke = 0; tau = 0; if (flowRate < 3000) { updateRoundedTau = 24; } else if (flowRate < 4000) { updateRoundedTau = 18; } else { updateRoundedTau = 12; } } else { buttonsPushed = 12; flowRate = 0; crcl = 0; ke = 0; tau = 0; updateRoundedTau = 0; percentRemoved = 0; } print(percentRemoved); print(flowRate); CalculatorVancDosing vancCalc; vancCalc = CalculatorVancDosing( button: buttonsPushed, weight: weightSelected, tau: updateRoundedTau, percent: percentRemoved, flow: flowRate, ke: ke, ); Navigator.push( context, MaterialPageRoute( builder: (context) => VancResultsScreen( vancTau: vancCalc.calculateTau(), vancDosingLoad: vancCalc.calculateLoadDose(), vancDosingMaint: vancCalc.calculateMaintDose(), screenLayout: 2, ), ), ); }) ])); } } class MyClipper extends CustomClipper<Path> { @override Path getClip(Size size) { var path = Path(); path.lineTo(0, size.height - 80); path.quadraticBezierTo( size.width / 2, size.height, size.width, size.height - 80); path.lineTo(size.width, 0); path.close(); return path; } @override bool shouldReclip(CustomClipper<Path> oldClipper) { return false; } }
-
Changing tab does not rebuild list
I have two tabs using a
TabBar
.In each tab a list of items is displayed from FireStore database - this is the same list of items only with different filter applied.
The list is obtained from the DB using
Provider
:List<Item> userItems = Provider.of<List<Item>>(context, listen: true) ?? [];
The list is filtered on each tab using
retainWhere
:Tab 1 Filter
userItems.retainWhere( (item) => item.usedDate == null && item.isActive != true, );
Tab 2 Filter
userItems.retainWhere( (item) => item.usedDate == null && item.isActive == true, );
The problem is the list filters out all items when moving to either tab. It seems to be filtering the already filtered list, rather than filtering it from scratch. Even though each tab is its own
Stateful
widget function and builds from scratch each time.It is not cacheing data, my
print
statements show it is building each list every time I change tabs, but still it does not work.I can't work out why this is happening.
-
Can't resolve error in dropdownfield flutter
I kept getting this error
Invalid value in this field
This is my code:
return DropDownField( controller: ngoSelected, hintText: "Select your NGO", enabled: true, items: namesOfNgos, itemsVisibleInDropdown: 10, value: String, onValueChanged: (value){ setState(() { selectedNgo=value; }); }, ); }, future: _getNgoDetails(), ),
Everything is working fine except this redline error. I have also attached the image.
-
how can i solve blocProvider context problem?
I have submit button, but when i press the button it gives this error:
BlocProvider.of() called with a context that does not contain a CreatePostCubit.
No ancestor could be found starting from the context that was passed to BlocProvider.of(). This can happen if the context you used comes from a widget above the BlocProvider. The context used was: BuilderI suppose contexts get mixed. how can i solve this error?
my code:
Widget createPostButton(BuildContext context) { final TextEditingController _postTitleController = TextEditingController(); final TextEditingController _postDetailsController = TextEditingController(); final TextEditingController _priceController = TextEditingController(); final _formKey = GlobalKey<FormState>(debugLabel: '_formKey'); return BlocProvider<CreatePostCubit>( create: (context) => CreatePostCubit(), child: Padding( padding: const EdgeInsets.only(right: 13.0, bottom: 13.0), child: FloatingActionButton( child: FaIcon(FontAwesomeIcons.plus), onPressed: () { showDialog( context: context, barrierDismissible: false, builder: (context) { return AlertDialog( content: Form( key: _formKey, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: <Widget>[ Padding( padding: EdgeInsets.all(8.0), child: TextFormField( autocorrect: true, controller: _postTitleController, textCapitalization: TextCapitalization.words, enableSuggestions: false, validator: (value) { if (value.isEmpty || value.length <= 4) { return 'Please enter at least 4 characters'; } else { return null; } }, decoration: InputDecoration(labelText: 'Post Title'), )), Padding( padding: EdgeInsets.all(8.0), child: TextFormField( controller: _postDetailsController, autocorrect: true, textCapitalization: TextCapitalization.words, enableSuggestions: false, validator: (value) { if (value.isEmpty || value.length <= 25) { return 'Please enter at least 25 characters'; } else { return null; } }, decoration: InputDecoration( labelText: 'Write a post details'), )), Padding( padding: EdgeInsets.all(8.0), child: TextFormField( controller: _priceController, enableSuggestions: false, inputFormatters: <TextInputFormatter>[ FilteringTextInputFormatter.digitsOnly ], keyboardType: TextInputType.number, validator: (value) { if (value.isEmpty || value.length >= 4) { return 'Please enter a valid value'; } else { return null; } }, decoration: InputDecoration(labelText: 'Enter the Price'), )), OutlinedButton( style: OutlinedButton.styleFrom( primary: Colors.white, backgroundColor: Colors.blue, ), child: Text("Submit"), onPressed: () => { BlocProvider.of<CreatePostCubit>(context) .createNewPost( postTitle: _postTitleController.text, postDetails: _postDetailsController.text, price: _priceController.text) }, ), ], ), ), ), ); }); }, ), ), ); }
-
Items not being added to list using .Add()
I have a class named
Model
which contains a propertyElements
. Upon the deserialization of a json, I wish to add the newly resulted object to the list ofElements
- however, it does not get added.Firstly, my Model.cs:
public class Model { public new IEnumerable<Element> Elements { get; } public new IEnumerable<Connection> Connections { get; } }
Please note that there are no setters, and I can not add them. This class should remain as it is.
Next, the deserialization and my attempt to add the Elements to the newly created model.
Model newModel = new Model(); foreach (var item in jsonResponse) { JArray jElements = (JArray) item.Value; foreach (var rItem in jElements) { JObject rItemValueJson = (JObject) rItem; Models.Element rowsResult = Newtonsoft.Json.JsonConvert.DeserializeObject<Models.Element>(rItemValueJson.ToString()); newModel.Elements.ToList().Add(rowsResult); } }
I can not instantiate a new list like below:
newModel.Elements = new List<MEP.Calculations.Common.Models.Element>();
or assign the
rowsResult
because:Property or indexer 'Model.Elements' cannot be assigned to -- it is read only The property Model.Elements has no setter
Is there any possible workaround for this, except for, as I previously mentioned, adding a setter?
-
Generating classes from Avro schema that includes java core package's classes : How to use those fields properly?
Assuming this java class :
public class Status { private Long id; private String name; private Point position; }
With
Point position
being the object fromjava.awt.Point
(a java core package class), you can serialize it producing this schema :{ "type": "record", "name": "Status", "namespace": "com.example.entities", "fields": [ { "name": "id", "type": "long" }, { "name": "name", "type": "string" }, { "name": "position", "type": { "type": "record", "name": "Point", "namespace": "java.awt", "fields": [] } } ] }
However, when deserializing this schema to generate classes (using the avro-maven-plugin), I end up with :
- The
Status
class (seems ok) - The
java.awt.Point
in a new Package, generated with Avro. It is this one that is referenced in theStatus
class
The Point package does not include any fields that the core package contains (ie.
double x
anddouble y
).A few questions :
- Is there a way to exclude java core package from Avro class generation ? (I want to use the original package, not a generated one)
- If not, how do you assign the
Point
fields in theStatus
generated class ? (put(String field, Object value)
?)
Edit : Note that it behaves similarly with
java.awt.Polygon
andjava.sql.Timestamp
(and I'm sure other classes from java core package would too) - The
-
MissingMethodException Json.Serializer Constructor on type not found exception in WASM unoplatform
I am using
System.Text.Json.JsonSerializer.Deserialize
to deserialize string into my class.Here is my class:
namespace Database { public class Song { public uint id { get; set; } public string name { get; set; } public string author { get; set; } public int bpm { get; set; } } }
Here is code of deserialization with jsonData being
[{"id":1,"name":"1","author":"1","bpm":0},{"id":2,"name":"2","author":"2","bpm":0}]
List<Song> songs = JsonSerializer.Deserialize<List<Song>>(jsonData);
It works perfectly fine on UWP and Android projects in Uno-Platform. However, on WASM exception
MissingMethodException
is thrown. Here is the log:System.MissingMethodException: Constructor on type 'System.Text.Json.Serialization.Converters.ListOfTConverter`2[[System.Collections.Generic.List`1[[Database.Song, BP.Wasm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], mscorlib, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[Database.Song, BP.Wasm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]' not found.
As far I understand the log. It doesn't know how to deserialize
List<Song>
. Neither the overview or any of the Additional resources says anything about some exceptions to what types are supported by de/serialization in what framework.Do I have to define my own converters for
List
ofSong
and potentially other Collections? Could this be related to Uno Platform anyhow? Or am I just missing something trivial?