New list still references old list after being cloned - flutter

I have a list of maps which are initialized as follow
List<dynamic> _people = [];
Map<String, dynamic> _person = {
'image' : null,
'name': null,
'age': null
};
On one of the methods inside the program I intend to copy the value inside _people to a new List variable so I can make changes to it.
List _peopleCopy = List.from(_people);
for (var i=0; i < _people.length; i++) {
if (_people[i]['image'] != null) {
List<num> img = new List.from(_people[i]['image'].readAsBytesSync());
_peopleCopy[i]['image'] = img;
}
}
Now the problem here is that everytime I assign the value of img to _peopleCopy, it would change the respective value on _people list as well even though I did List.from() method to clone it. Obviously, I wanted to preserve the original content of _people list. What did I do wrong here?

For such case what I do is, I store the originalJson of the object inside a variable in the object class like below,
class People {
int? age;
String? image;
String? name;
Map<String, dynamic> originalJson = {};
People({this.age, this.image, this.name});
People.fromJson(Map<String, dynamic> json) {
originalJson = json;
age = json['age']?.toInt();
image = json['image']?.toInt();
name = json['name']?.toString();
}
Map<String, dynamic> toJson() {
'age' = age;
'image' = image;
'name' = name;
}
}
So when you are parsing the http reponse, now all you have to do is,
People.fromJson(json.decode(response.body));
This way you can preserve the json and while cloning the list of People, all you have to do is,
// With the following line _people will now be new list with new objects but with mutated _people data
_people = _people.map((e) => People.fromJson(e.toJson())).toList();
// With the following line _people will now be new list with new objects but with data which we have recieved from the API call
_people = _people.map((e) => People.fromJson(e.originalJson)).toList();
This way of maintaining the class should for sure help you out..

You have successfully created a new list. But the list contains the references to the same maps. So changing a map will change it in both lists since it's the same element.
You need to copy the elements, too.
Since your map also contains a dynamic type that you probably want to deep copy, I would suggest to just jsonEncode/jsonDecode your whole structure. Serializing and Deserializing is not the most efficient way, but it will create a deep copy of whatever structure is in there.
That said, Dart is a real programming language. It helps a lot to use it that way. If you use actual types, instead of dynamic, it becomes less guessing, praying and duct-typing and more solid, fact based programming. Since this seem to be JSON structures, use model classes. Then you can have your own deep copy methods and have compiler support that you just don't get when you use dynamic.

Related

Access class variables like objects using strings in dart/flutter

I have a class called preferences below
preferences.dart:
class Preferences {
Map<String, int> cuisineTypes;
Map<String, int> mealSpecialties;
Map<String, int> allergiesAndDietaryRestrictions;
Preferences({
required this.cuisineTypes,
required this.mealSpecialties,
required this.allergiesAndDietaryRestrictions
});
}
I want to update the variables in this class based on the string preferenceCategory. My code currently has to look like three lines to accomplish this.
if(preferenceCategory == 'cuisineTypes') localGlobalState.person.preferences!.cuisineTypes[preferenceType] = preferenceValueUpdated;
if(preferenceCategory == 'mealSpecialties') localGlobalState.person.preferences!.mealSpecialties[preferenceType] = preferenceValueUpdated;
if(preferenceCategory == 'allergiesAndDietaryRestrictions') localGlobalState.person.preferences!.allergiesAndDietaryRestrictions[preferenceType] = preferenceValueUpdated;
I would like to accomplish this on one line like below, but I can't because class variables can't be accessed by strings like objects.
localGlobalState.person.preferences![preferenceCategory][preferenceType] = preferenceValueUpdated;
Is there alternative around this? My restriction is that I can't/shouldn't update preferences.dart if I don't have to.

json_serializable not generating code for final fields and getters

This is my class:
#JsonSerializable()
class Foo {
final int a = 0;
int get b => 42;
}
The generated code doesn't include any of the a or b field:
Foo _$FooFromJson(Map<String, dynamic> json) => Foo();
Map<String, dynamic> _$FooToJson(Foo instance) => <String, dynamic>{};
Note: Please don't write solutions for doing something like Foo(this.a) or Foo() : a = 0 etc. I need to keep my structure as it is.
Simple answer - this won't be possible as this is done intentionally by design.
If adjusting your model structure is out of the question, you can try manual conversion or a mix of these two.
After encountering the same issues myself, not long ago, I opted in for manual converting my models - and also a combination of two in some cases.
With #JsonSerializable, you can make use of the from/to generated methods, and then in addition, manually convert your final and getter fields.
Another option you can try, at least for the getter fields, is making use of #JsonSerializable(crateFactory: false). This will indicate the engine to convert getters as well.
I suggest reading Flutter JSON Docs in detail and figuring out what will suit you the most, depending on your models, what you want, or what you can compromise on.
Worth reading the Github thread on getter conversion.
There's no out of the box workaround for final fields though, but as per this Github thread on final fields, PRs are welcome :)
If you want easier json parsing, use Dart Data Class Generator extension in VS Code.
I think since a is final, it is considered a constant and not variable and b is a getter and not a variable, there are no variables (fields) for json_serializable to generate code for.
You need to add a default constructor before json_serializable can run. Since you have no fields, you wil have an empty constructor and empty methods generated.
class Foo {
final int a = 0;
int get b => 42;
const Foo();
factory Foo.fromJson(Map<String, dynamic> json) => Foo();
Map<String, dynamic> toJson() => <String, dynamic>{};
}

How to perform deep copy for a list to another list without affecting the original one in Flutter

I have two lists of Type object, _types and _filteredTypes:
List<Type> _types = []; // Original list
List<Type> _filteredTypes = []; // Where i bind filter the contents
a Type object is:
class Type extends Equatable {
final int id;
final String title;
List<SubType>? subTypes;
Type({
required this.id,
required this.title,
this.subTypes,
});
}
a SubType object is:
class SubType extends Equatable {
final int id;
final String title;
Type({
required this.id,
required this.title,
});
}
I need to filter the list based on search text, so whenever user types a letter the _filteredTypes being updated.
I do the filter on _onSearchChangedEvent() within setState() like this:
_filteredTypes = List.from(_types); // To get the original list without filter
for(var i = 0; i < _filteredTypes.length; i++) {
// This is where i filter the search result when a subType.title matches the search query:
_filteredTypes[i].subTypes = List.from(_types[i].subTypes!.where((element) => element.title.toLowerCase().contains(query!.toLowerCase())).toList());
}
// This is where i filter the search result and remove any type doesn't match any subType.title:
_filteredTypes.removeWhere((element) => element.subTypes!.length == 0);
bindTypes(); // To refresh the list widget
The problem is when i need get the original list i get the main type but type.subTypes is still filtered based on the previous search not the original one! even it is copied without reference _filteredTypes = List.from(_types);
It seems like a bug in Flutter, any idea guys?
List.from does not provide you with a deep copy of _types- it gives you a shallow copy. Meaning both _filteredTypes and _types share the same subTypes. It's similar in that behavior to this example
var sharedList = [1];
final listOne = [1, sharedList];
final listTwo = [1, sharedList];
sharedList[0] = 2;
Changing sharedList will change the value in both listOne and listTwo. If shared list was just an integer, changing that integer would not produce the same effect. Like in this example:
var sharedInteger = 1;
final listOne = [1, sharedInteger];
final listTwo = [1, sharedInteger];
sharedInteger = 2;
When you create an instance of a class may it be built in like List or your own custom class, what you get returned is a reference or a pointer to that instance/object. The object itself is allocated on the heap memory area rather than on the stack which means that this object can be referenced outside of functions. As in its life (object life) is not bound by the scope of the function so when you reach the end } of the function the object still exists, and its memory is freed by a special program called the garbage collector.
In dart as in many modern programming languages garbage collectors are used and objects are automatically allocated on the heap. In languages such as C++ for example you can allocate objects on the stack, and you have to be explicit about heap allocation, and deallocate any objects on the heap when you are done with them.
All of the above you can look up the gist of it is since subtypes is a list, it's a reference type so both _filteredTypes and _types have that reference. If you want a deep copy you can do that as well, and I'll leave it for you to look that up.
This is how to perform a deep copy for a list has sub-list, thanks moneer alhashim, your answer guided me.
I'm posting it as an answer to help someone else find the solution easy.
So, the key here is to map the original list types.map(...) and fill it manually instead of using List.from(), and that will create a new instance for deep objects.
First, i declared one function for each list:
//For the parent list:
List<Types> getNewTypesInstance {
// This function allows to perform a deep copy for the list.
return types.map((e) =>
Type(id: e.id, title: e.title, subTypes: e.subTypes))
.toList();
}
// For the child list:
List<SubType> getNewSubTypesInstance(List<SubType> lst) {
// This function allows to perform a deep copy for the list:
return lst.map((e) =>
SubType(id: e.id, title: e.title))
.toList();
}
And if you have more deep list(s), you will need third function to obtain it as new instance and so on.
Finally, the way how to call them is to write this code within setState():
_filteredTypes = getNewTypesInstance
for(var i = 0; i < _filteredTypes.length; i++) {
// This is where i filter the search result when a subType.title matches the search query:
_filteredTypes[i].subTypes = List.from(getNewSubTypesInstance(_types[i].subTypes!.where((element) => element.title.toLowerCase().contains(query!.toLowerCase())).toList()));
}
// This is where i filter the search result and remove any type doesn't match any subType.title:
_filteredTypes.removeWhere((element) => element.subTypes!.length == 0);
bindTypes(); // To refresh the list widget

Deserialise JSON response from API into type in Dart

I'm going out of my mind trying to figure this out because it's such a simple thing to do and there's no information on it anywhere.
I have an API call that is returning a Student which is defined like this
class Student {
String photo;
String upn;
String fullName;
String studentClass;
String studentYear;
}
I'm using the dart http library to make a call to an API that returns an array of a number of students. All I want to do is deserialise the string returned by the API into a typed list so that I can actually do something with it.
studentFuture.then((res) {
log(res.body.toString());
List<dynamic> dynamicList = jsonDecode(res.body);
var student = dynamicList[0] as Student;
});
Trying to cast it to Student using as doesn't work, I just get this
Unhandled Exception: type '_InternalLinkedHashMap<String, dynamic>' is not a subtype of type 'Student' in type cast
All I want is to be able to do something like this like you can in C#
var obj = DeserialiseJson<Type>(jsonString);
No matter what I try I can't get the string to be deserialised into an object, how do I do this?
You can't do this in Dart/Flutter in the way that you want. This is because in order to accomplish this functionality in C#, Java, or any other strongly-typed language that supports this feature, the language uses reflection under the hood to map JSON fields to object fields.
Dart also has reflection functionality in the dart:mirror library. However, this library is disabled in Flutter due to its tendency to create large app footprints due to all the debug information for various types and fields and things needing to be included in the compiled binary. (That's the very simple explanation: the official reason given by the Flutter team goes into more detail.)
What this means is when you deserialize a JSON string, you get an object that is essentially a List<dynamic> or Map<String, dynamic> (depending on the JSON string in question; it may even just be some primitive value). Assuming the latter, you cannot directly convert between a Map and a Student because they are completely different types with nothing in common. There is no implicit way to take the Map and generate a Student because the reflection tools necessary to do so don't exist within a Flutter app. As a result, you get an error saying you cannot convert a Map to a Student, as you can see.
To work around this, you are encouraged to add a toJson/fromJson method to your model class so you can explicitly generate an instance of the class from a deserialized JSON string:
class Student {
String photo;
String upn;
String fullName;
String studentClass;
String studentYear;
factory Student.fromJson(Map<String, dynamic> jsonData) {
return Student()
..photo = jsonData['photo']
..upn = jsonData['upn']
..fullName = jsonData['fullName']
..studentClass = jsonData['studentClass']
..studentYear = jsonData['studentYear'];
}
}
And use it like so:
studentFuture.then((res) {
log(res.body.toString());
List<dynamic> dynamicList = jsonDecode(res.body);
var student = Student.fromJson(dynamicList[0]);
});
This is all well and good, but if you have more than a few model classes you will quickly realize that adding this boilerplate code to every model class gets really tedious really quick. That's why various packages exist to try and alleviate the burden with automatic code generation, such as json_serializable and freezed.
TL;DR: What you are asking for is not currently possible in Flutter Dart, and that isn't likely to change any time soon. You need to either write a manual factory constructor to make the conversion or use a code-gen package that generates the constructors for you.
Have a look at https://pub.dev/packages/json_serializable which, through code generation, will give you methods like
#JsonSerializable()
class Student {
String photo;
String upn;
String fullName;
String studentClass;
String studentYear;
Student({this.photo, this.upn, this.fullName, this.studentClass, this.studentYear});
factory Student.fromJson(Map<String, dynamic> json) => _$StudentFromJson(json);
Map<String, dynamic> toJson() => _$StudentToJson(this);
}
Then you can do:
var list = jsonDecode(res.body);
for (var map in list) {
print(Student.fromJson(map)); // Student class
}

Please help me understand fromMap and toMap from this code?

I got this code from the internet and I can not seem to understand it or find anything on the internet for it.
In the code below toMap is a method that returns 2 items, How is that possible?
And what is fromMap, is it a user created method? I thought methods used {} or => so it is a bit confusing.
Also, what is the key here for the Map? Can the map only store 2 categories of items? One is the key and the other is the value. Or it can have one key but multiple categories of values.
For example, there might be a single unique key, which could help take out the task title, time, reminder data, notes, etc as values of the map.
class Task {
String title;
bool completed;
Task({
this.title,
this.completed = false,
});
Task.fromMap(Map<String, dynamic> map): title = map['title'],completed = map['completed'];
updateTitle(title) {
this.title = title;
}
Map toMap() {
return {
'title': title,
'completed': completed,
};
}
}
In the code below toMap is a method that returns 2 items, How is that
possible?
No, it returns a Map (with two items). More about maps can be found here.
And what is fromMap, is it a user created method? I thought methods
used {} or => so it is a bit confusing.
Task.fromMap(Map<String, dynamic> map) is called "named constructor". The : title = map['title'],completed = map['completed'] part is initializer list
My understanding is;
In fromMap, you retrieve the title and completed from some map, and save it in your local variables.
In the toMap you take the saved values in your local variables and can return a Map.
The key is whatever you put you chose it to be, but here you chose one key to be titleand one to be completed.
Does this help you?
First of all we discuss about FormMap So what is fromMap()?
whenever you have any api and at firetime you will get json format so when you want to convert that data into any class format then you have to do like
Map temp = json.decode(response.body);
so your function can understand map key and retrieve that value and set in class local variable
and now Second point is toMap So what is toMap()?
Whenever you want to post something into api or somewhere you have map data so you can post in api
like
Abc a = Abc(name:"hari",address:"india");
a.toMap();