Use object setters dynamically with dart - flutter

Objective
Create a method that sets value to the object created above.
This method could be used like below.
setValueToUser(userPropertyName, value);
Given
A mutable model made with freezed.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
#unfreezed
class User with _$User {
factory User({
required int id,
required String email,
required String password,
required DateTime birthday,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) =>
_$UserFromJson(json);
}
What I tried
switch-case statement
First, I made something like this, but this ends up being very tedious as an actual user model has 109 property members :(
User setValueToUser(String propertyName, dynamic value){
final user = User();
switch (propertyName) {
case 'id':
user.id = value as int;
break
case 'email':
user.email = value as String;
break
case 'password':
user.password = value as String;
break
case 'birthday':
user.birthday = value as DateTime;
break
}
return user;
}
Map
Then, I created a map, but dart requires all fields of a map to be initialized when the map variable initializes, so failed achieve the goal.
User setValueToUser(String propertyName, dynamic value) {
final user = User();
final userDictionary = <String, dynamic>{
'id': user.id = value as int,
'email': user.email = value as String,
'password': user.password = value as String,
'birthday': user.birthday = value as DateTime,
};
userDictionary[propertyName]; // I intend to set value to ONLY one specific property at a time. Apparently, this line means nothing to dart :(
return user;
}
Conclusion
I know setting a property name with string looks very ridiculous. I do want to avoid using string if possible, so comments and answers regarding this is very much welcomed. (But setting enum for all properties of the class would be tedious, I think)

Related

Flutter Parse JSON to Model with fromJson Function Always Fails

I am very confused about this problem, parsing JSON to model always fails with the message "null is not subtype of type string in type cast". I've made sure all values are not null. When manually initiating the model with the constructor, it works, but using the fromJson function always fails.
This is my model:
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user_model.g.dart';
part 'user_model.freezed.dart';
#freezed
class UserModel with _$UserModel {
const factory UserModel({
required String id,
required String identity,
required String name,
required String email,
required String phone,
required String role,
required String? createdAt,
required String? updatedAt,
required String? accountVerifiedAt,
required String jsonWebToken,
}) = _UserModel;
const UserModel._();
factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json);
}
This is how I parse with fromJson, but it always throw exception (null is not a subtype of type string in type cast):
var user = UserModel.fromJson(object['data']);
But it works:
var user = UserModel(
id: object['data']['id'],
identity: object['data']['identity'],
name: object['data']['name'],
email: object['data']['email'],
phone: object['data']['phone'],
role: object['data']['role'],
createdAt: object['data']['createdAt'],
updatedAt: object['data']['updatedAt'],
accountVerifiedAt: object['data']['accountVerifiedAt'],
jsonWebToken: object['data']['jsonWebToken'],
);
Maybe your json has null value that you don't recognized. It happens to me all the times. By the way, I never use freezed_annotation because model is easy to write by hands, plus if there is is a bug, it would be easier to fix. You can do like this:
When back-end responds data:
if (json["error_code"] != 0) { // Back-end responds an error
// handle error
} else {
YourModel.fromJson(json);
}
And here is your model:
class YourModel {
String variable;
YourModel(this.variable = <default value>);
YourModel.fromJson(Map<String, dynamic> json) {
this.variable = json["blah"]["yolo"] ?? <value if json null>;
}
}
I just figured out, that it's caused by the generated freezed class, my JSON response use camelCase as keys, but the model generates with snake_case. So basically I convert my response keys from camelCase to snake_case before initiating the object using the fromJson function.
Map<String, dynamic> data = {};
for (var element in Map<String, dynamic>.from(object['data']).entries) {
data[StringUtils.camelCaseToLowerUnderscore(element.key)] =
element.value;
}
object['data'] = UserModel.fromJson(data);
If anyone facing the same problem, I hope it can help. And if anyone has a better solution, you guys can post it here too. Thank you.
NB: I use basic_utils: ^4.4.3
EDIT:
Following this article https://codewithandrea.com/articles/parse-json-dart-codegen-freezed/, we can add #JsonKey annotation to make sure the property name is correct, example:
#JsonKey(name: 'customerId') required String? customerId
Without giving #JsonKey annotation, customerId will be written as customer_id.

Can we use json_serializable package to serialize and deserialize user models?

After generating part 'UserModel.g.dart'; successfully, I got the following error upon trying to do
if (user != null) {
var uid = user.providerData.first.uid;
var displayName = user.providerData.first.displayName;
var email = user.providerData.first.email;
var phoneNumber = user.providerData.first.phoneNumber;
var providerId = user.providerData.first.providerId;
var photoUrl = user.providerData.first.photoURL;
UserModel userModel = UserModel(phoneNumber,
uid: uid,
displayName: displayName,
email: email,
providerId: providerId,
photoUrl: photoUrl);
Query query = users.where('uid', isEqualTo: uid);
query.get().then((querySnapshot) => {
if (querySnapshot.size < 1) {addUser(userModel)}
});
}
}
And here is my UserModel.dart without any errors in the file.
import 'package:json_annotation/json_annotation.dart';
part 'UserModel.g.dart';
#JsonSerializable()
class UserModel {
late String? uid;
late String? displayName;
late String? email;
late String? phoneNumber;
late String providerId;
late String? photoUrl;
UserModel(this.phoneNumber, {required this.uid, required this.displayName, required this.email,
required this.providerId, required this.photoUrl});
factory UserModel.fromJson(Map<String, dynamic> json) => _$UserModelFromJson(json);
Map<String, dynamic> toJson() => _$UserModelToJson(this);
}
Offcourse, I could manually map each value. But as recommended here https://flutter.dev/docs/development/data-and-backend/json, to prevent
Manual decoding does not perform well when your project becomes bigger. Writing decoding logic by hand can become hard to manage and error-prone. If you have a typo when accessing a nonexistent JSON field, your code throws an error during runtime.
I used the plugin json_serializable to sort of automate it.
But I got the following error:
Expected a value of type 'Map<String, dynamic>', but got one of type 'UserModel$'
Is this error expected due to incompatibility or am I doing something wrong? Please respond. Thank you very much. :)
I'm guessing this is where your issue is.
query.get().then((querySnapshot) => {
if (querySnapshot.size < 1) {addUser(userModel)}
});
Use the toJson method you created in your model class to pass in a Map instead of your custom UserModel object.
{addUser(userModel.toJson())}

Flutter: Merge objects of same type and combine defined properties (like javascript?)

Using Flutter and Dart, lets say I have this class:
#JsonSerializable()
class User {
#JsonKey(nullable: true, required: false)
final String name;
#JsonKey(nullable: true, required: false)
final int age;
User({
this.name,
this.age,
});
factory User.fromJson(Map<String, dynamic> json) => _$AddressFromJson(json);
Map<String, dynamic> toJson() => _$AddressToJson(this);
#override
String toString() {
return 'User ${toJson().toString()}';
}
}
In my code I'm trying to create a new instance of this class to be sent to /update-user endpoint on the server. My goal is to send an object that contains just the properties I would like the server to update. let's say only update age.
Using
final dto = new UpdateUserRequest(
age: 34
);
results in this json representation: {name: null, age: 34}, which will override the already existing name on the server.
I also tried 'json merging' 2 objects, the one I already have with the name, and the new dto that updates age:
final combined = UpdateUserRequest.fromJson({
...(dtoWithAge.toJson()),
...(existingUserWithName.toJson()),
});
but no matter how I play around with these, they end up overriding each other.
So, is there anyway to get a json/DTO instance of the class, that only contains the properties and values I want the server to update? (trying to achieve something very similar to javascript)
I don't think there's a pre-implemented solution for that. Assuming I understood what you're trying to accomplish, how about adding a copyWith method to User?
User copyWith({
String name,
int age,
}) => User(
name: name ?? this.name,
age: age ?? this.age,
);
You'd use it like this:
final existingUser = User(name: 'John');
final updatedUser = existingUser.copyWith(age: 25);
sendUpdateRequestWith(updatedUser);
For you convenience, there's a plugin to generate it.
vscode: https://marketplace.visualstudio.com/items?itemName=BendixMa.dart-data-class-generator
IDEA/Android Studio: https://plugins.jetbrains.com/plugin/12429-dart-data-class

Acessing data from instance of a class

I have a question, I started learning dart/flutter, and when passing data from one screen to another I access the data like this
final orderData = ModalRoute.of(context).settings.arguments;
OrderData class looks like this
class OrderItem {
final String id;
final String name;
final String date;
final String address;
final String recordNumber;
OrderItem({
#required this.id,
#required this.name,
#required this.date,
#required this.address,
#required this.recordNumber,
});
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'date': date,
'address': address,
'recordNumber': recordNumber,
};
}
factory OrderItem.fromMap(Map<String, dynamic> map) {
if (map == null) return null;
return OrderItem(
id: map['id'],
name: map['name'],
date: map['date'],
address: map['address'],
recordNumber: map['recordNumber'],
);
}
String toJson() => json.encode(toMap());
factory OrderItem.fromJson(String source) => OrderItem.fromMap(json.decode(source));
#override
String toString() {
return 'OrderItem(id: $id, name: $name, date: $date, address: $address, recordNumber: $recordNumber)';
}
#override
bool operator ==(Object o) {
if (identical(this, o)) return true;
return o is OrderItem &&
o.id == id &&
o.name == name &&
o.date == date &&
o.address == address &&
o.recordNumber == recordNumber;
}
#override
int get hashCode {
return id.hashCode ^
name.hashCode ^
date.hashCode ^
address.hashCode ^
recordNumber.hashCode;
}
}
My question is two fold(this is what vs code plugin generates for class). First is how can I access the data in the instance of the class(do I need for each specific getter), and second can somebody explain what toMap(), fromMap(), toJson(), fromJson(), bool operator ==(Object o), and hashCode getter do.
Looks like you're trying to use Flutter navigation methods.
Have a look at this blog post that explains how it works.
In summary, to "push" state to be used in the next screen, do something like this:
final arguments = OrderItem(
id = 'id',
name = 'name',
date = 'date',
address = 'address',
recordNumber = '10',
);
Navigator.pushNamed(context, NamedPagePassed.route, arguments: arguments);
You can later access it like this:
final OrderItem args = ModalRoute.of(context).settings.arguments;
print('The id of the order is ${args.id}');
You seem to have a whole lot of generated code in your class. The fromJson, toJson, fromMap and toMap methods are used for serialization (i.e. turn a Dart object into something that can be "transferred" to/from another language/network/etc).
The == operator and hashCode are used to check if an object instance is equal to another (which is very common in Flutter as Flutter wants to know if your UI state has been modified). hashCode allows a fast way to check that two objects are definitely not equal (you can know for sure that two objects are not equal if their hash-codes are different... if the hash-codes are equal, the objects may or may not be equal, but the probability they are NOT equal will be low because hash functions try to avoid "collisions", which is when two different objects have the same hash-code).
Hash-code and == are normally implemented together to give your class "identity". Just google around and you'll see how this all works.
I would recommend you don't use code generation from your IDE like this. Instead, get familiar with how Dart builders work, then use a codegen library that will automatically create these methods for you every time you compile (so changes to the data model are immediately reflected in the implementation of all these generated methods).
My recommendation is to use freezed for that.
Your code will be much more maintainable this way.

What does #override toString method do in a Flutter Dart class?

In the following model class, what purpose does overriding the toString() method achieve?
class User {
final int id;
String name, email, token;
User(this.id, this.name, this.email, this.token);
User.fromJson(dynamic json) : this.id = json['id'] {
this.name = json['name'];
this.email = json['email'];
this.token = json['token'];
}
dynamic toJson() => {'id': id, 'name': name, 'email': email, 'token': token};
#override
String toString() {
return toJson().toString();
}
}
The purpose of the toString() method is to provide a literal representation of whatever object it is called on, or to convert an object to a string (example converting primitive types to strings). For a user defined class, it can vary depending on your use case. You might want to print out the object to a console. A simple print command on the object won't give you any useful information. However, you can use the toString() method and convert it to a more understandable version. Example
#override
String toString() {
return "($someProperty,$someOtherProperty)";
}
This will return whatever the properties of that object were set at that time. Another (although not good) purpose could be to compare two objects of the same class. Similar to the above example, you can say that two objects of the class would be equal if the output of the toString() method of both objects is the same. Again, it totally depends on your use case.