Flutter: Remove item for a list reflect another list - list

I have the following flutter code,
I have a list, let's call it list1, and I am coping the value of this list to another list list2.
When I remove an element from list1, it's automatically removed from list2!
There is a minimal code that reproduce the issue.
I couldn't understand this strange behavior, could someone explain it to me?
What I want to do is remove an item form list1 but keep list2 as it is.
This is the code:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() => runApp(EmptyFile());
class EmptyFile extends StatefulWidget {
final list1 = ["1", "2", "3"];
EmptyFile({Key? key}) : super(key: key);
#override
_EmptyFileState createState() => _EmptyFileState(list1);
}
class _EmptyFileState extends State<EmptyFile> {
List<String> list1 ;
List<String> list2 = [];
_EmptyFileState(this.list1) {
list2 = list1;
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Example App'),
),
body:
Container(
child: RaisedButton(
onPressed: () {
print("list1.last ${list1.last}"); // the result is: list1.last 3
list2.remove("3"); // here I delete from list2 (list TWO)
print("list1.last ${list1.last}"); // the result is: list1.last 2 . Here an item from list ONE has been deleted!!!
},
child: Text('delete last Item'),
),
)
));
}
}
You can run the code online from here: https://dartpad.dev/81d0a66ac7c46258df7847fdc7a10e96?null_safety=true

Apparently, dart copy the lists by reference.
So, to copy the values of a list to another list, use the following code:
list2 = List.from(list1);
instead of:
list2 = list1;

By this
list2 = list1;
you are not actually making a copy of list1. It is the same list being assigned to list2 too.
So you need to change to this.
list2 = [...list1];

You can use spread operation:
list2 = [...list1];
That should allow any iterable to copy the values instead of using the reference.

Related

List is Empty (Flutter)

I am creating a List in Flutter and displaying it in a Column, When I Run it is just Empty and when I print the list it just prints an Array
I/flutter (24613): []
I am using this code to create the List:-
myFunction() {
return StreamBuilder(
stream:
users.orderBy('timestamp', descending: true).limit(30).snapshots(),
builder: (context, snapshot) {
List<UserList> usersList = [];
snapshot.data.documents.forEach((doc) {
User user = User.fromDocument(doc);
UserList userList = UserList(user);
usersList.add(userList);
});
return Column (children: usersList);
}
),
}
This is My User Class:-
class User {
final String id;
final String username;
final String email;
final String photoUrl;
User({
this.id,
this.username,
this.email,
this.photoUrl,
});
factory User.fromDocument(DocumentSnapshot doc) {
return User(
id: doc.data()['id'],
username: doc.data()['username'],
email: doc.data()['email'],
photoUrl: doc.data()['photoUrl'],
);
}
}
The Code Is Showing No Errors and the Column Is not Displaying, Also When I print The length of the List it Shows it is Zero:-
I/flutter (24613): 0
What Could be The problem ??
I guess we need to tweak some of your code little bit to make the logic working. :)
builder param should be specified with Type otherwise it will be of type dynamic. To be in safer side in this case it will be QuerySnapshot. So,
builder: (context, snapshot) in your code becomes
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot).
Next, there is no need of looping through foreach and instead you can try something like below.
snapshot.data.docs.map((document) { .... }
snapshot.data.documents in your code is not valid way of getting the Firestore Documents. Please refer official doc
And you need to return a widget from builder which you have done correctly. But, by mistake you are passing the List<UserList> to Column which will be expecting List<Widget>
return Column (children: usersList);
Here I can see you are passing usersList which is of type List<UserList>. So you can replace Column with ListView or similar kind of other widget since, Column doesn't support scroll.
So combining all these bits and pieces you will get the below snippet.
return StreamBuilder(
stream: FirebaseFirestore.instance
.collection('users')
.orderBy('timestamp', descending: true)
.limit(30)
.snapshots(), // Just for simplicity.
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
//When there is no data returned from the firebase.
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
return ListView(
children: snapshot.data.docs.map((document) {
return Text("Title: " + document['username']);
}).toList(),
);
},
);
For simplicity, I have returned the Text widget. You can implement your own UI there.
NOTE : This is the basic working example and you need to fine tune accordingly like using model classes instead of directly accessing based on your requirements.
Your Code
myFunction() {
return StreamBuilder(
stream:
users.orderBy('timestamp', descending: true).limit(30).snapshots(),
builder: (context, snapshot) {
List<UserList> usersList = [];
snapshot.data.documents.forEach((doc) {
User user = User.fromDocument(doc);
UserList userList = UserList(user);
usersList.add(userList);
});
return Column (children: usersList);
}
),
}
It is because you have to await for the json to actually get parse to the dart model. Second thing is forEach method is synchronous it doesn't wait for the async operation to complete, this is the reason why your list is empty.
This SO question has lot of different ways to make a list work asynchronously in flutter.
Column shows data before fetching data, so it shows empty list. For this use setstate according to your state management type ("notifylisteners" in provider) after getting data, so by this the screen will be updated and column also shows the updated list.
I'm not very sure how you're handling the scope of the variable.
Here's my minimal reproducible code which can give you some idea on how to add the items to the list.
class MyPage extends StatefulWidget {
#override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
final List<Widget> _list = [FlutterLogo()];
#override
void initState() {
super.initState();
Timer.periodic(Duration(seconds: 1), (timer) {
if (timer.tick >= 2) timer.cancel();
setState(() => _list.add(FlutterLogo()));
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(children: _list),
);
}
}
As 'Ashutosh patole' said, 'forEach' method does not wait iteration's complete.
I think that because of this reason, although you made a 'usersList',
there is no data when build widget in 'usersList'.
To fix this, you'd better change from 'forEach' to 'for'.
void main() async {
List<String> data = [ 'a', 'b', 'c'];
List<String> result = [];
data.forEach((data) async {
await Future.delayed(Duration(seconds: 1));
result.add(data);
});
print(result);
await Future.delayed(Duration(seconds: 3));
print(result);
print('-----------------');
result = [];
for (var item in data) {
await Future.delayed(Duration(seconds: 1));
result.add(item);
}
print(result);
await Future.delayed(Duration(seconds: 3));
print(result);
}
In your code, you can change like below.
List<UserList> usersList = [];
for (var doc in snapshot.data.documents) {
User user = User.fromDocument(doc);
UserList userList = UserList(user);
usersList.add(userList);
}
Before calling the data, check all fields:
Firestore Docs
Add a print() to see where the problem
FutureBuilder<DocumentSnapshot>(
future: users.doc(documentId).get(),
builder:
(BuildContext context, AsyncSnapshot<DocumentSnapshot> snapshot) {
//This
if (snapshot.hasError) {
return Text("Something went wrong");
}
print(snapshot.data);
//This
if (snapshot.hasData && !snapshot.data!.exists) {
return Text("Document does not exist");
}
print(snapshot.data);
//This
if (snapshot.connectionState == ConnectionState.done) {
Map<String, dynamic> data = snapshot.data!.data() as Map<String, dynamic>;
return Text("Full Name: ${data['full_name']} ${data['last_name']}");
}
return Text("loading");
},
);
This is what i typically use.Try out this! Please balance the brackets in the code
FutureBuilder(
future: users.orderBy('timestamp', descending: true).limit(30),
builder: (BuildContext context, AsyncSnapshot<List<User>> snapshot) {
List<User>ulist=snapshot.data;
return ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.only(top: 25,bottom: 35),
itemCount: evlist==null?0:evlist.length,
itemBuilder: (BuildContext context, int index) {
String evtime=evlist[index].fromdate.substring(11,16);
String ontime=evlist[index].fromdate.substring(0,16);
return Container(
decoration: BoxDecoration(
border: Border.all(width: 1.8,color: Colors.indigo[900]),
borderRadius: BorderRadius.circular(12.0),
color: Colors.grey[200]
),
margin:
const EdgeInsets.symmetric(horizontal: 18.0, vertical: 4.0),
child: ListTile(
leading: Icon(Icons.notifications),
title: Text(ulist[index].username.toString()),
subtitle:Text("next data"),
),
);
},
);

RadioListTile not found in test

I am having trouble with a test where I am trying to see if tapping a RadioListTile works. Simply looking the element byType with finder, doesn't find it even though the parent widget and the list tiles are displayed in the emulator. I really can't figure out what I am doing wrong.
The widget tree is as follows:
RadioQuestion
| QuestionText
| RadioChoices
| RadioListTile
class RadioChoices extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<RadioQuestionModel>(
builder: (context, model, child) => Column(
// debugPrint(model.choices) here shows data.
children: model.choices
.map((e) => RadioListTile(
title: e,
value: model.choices.indexOf(e),
groupValue: model.selectedIndex,
onChanged: (value) => model.onChanged(value),
))
.toList(),
));
}
}
testWidgets('Displays choices', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(home: Scaffold(body: RadioQuestion(questions[0]))));
var finder = find.byType(RadioChoices);
expect(finder, findsOneWidget);
finder = find.byType(RadioListTile);
expect(finder,
findsNWidgets(questions[0].possibleChoices.length)); // This fails, 0 widgets found.
});
The following TestFailure object was thrown running a test:
Expected: exactly 4 matching nodes in the widget tree
Actual: _WidgetTypeFinder:<zero widgets with type "RadioListTile<dynamic>" (ignoring offstage
widgets)>
Which: means none were found but some were expected
Converting the List to an array of RadioListTiles fixed it:
children: <RadioListTile>[]
..addAll(model.choices.map((e) => RadioListTile(
title: e,
value: model.choices.indexOf(e),
groupValue: model.selectedIndex,
onChanged: (value) => model.onChanged(value),
)))

flutter : Unsupported operation: Cannot add to an unmodifiable list

I have a ListView inside a StatelessWidget. It has items and every item contains a checkbox. When someone checks an item, I want the ListView to send this as a parameter to another page. But when I do that, it's giving me this error:
I/flutter ( 7067): The following UnsupportedError was thrown while handling a gesture:
I/flutter ( 7067): Unsupported operation: Cannot add to an unmodifiable list
I/flutter ( 7067): When the exception was thrown, this was the stack:
and this is my code
class StudentsList extends StatelessWidget {
final List<Child> mList;
StudentsList({this.mList});
#override
Widget build(BuildContext context) {
List<Child> selectedList = [];
return Container(
margin: EdgeInsets.only(top: 50, bottom: 20),
child: ListView.builder(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
itemCount: mList == null ? 0 : mList.length,
padding: EdgeInsets.only(right: 10),
itemBuilder: (BuildContext context, int position) {
return GestureDetector(
onTap: () {
if (selectedList.isEmpty) {
Navigator.push(
context,
new MaterialPageRoute(
builder: (BuildContext context) => SolokPage(
mChildList: [mList[position]],
isTeacher: true,
),
),
);
} else {
if (!selectedList.contains(mList[position])) {
selectedList.add(mList[position]);
}
Navigator.push(
context,
new MaterialPageRoute(
builder: (BuildContext context) => SolokPage(
mChildList: selectedList,
isTeacher: true,
),
),
);
}
},
child: StudentItem(
student: mList[position],
),
);
},
),
);
}
}
Stateless Widget properties are meant to be immutable
class StudentsList extends StatelessWidget {
// final means, flutter will not change value in future
final List<Child> mList;
StudentsList({this.mList});
Why ?
Because Flutter expects no business logic resides in StatelessWidget.
If we need to add new Student in Student list, it is considered as business logic.
If we need to delete some Student in Student list, it is considered as business logic.
So by using stateless widget, Flutter will only focuses on How it will be displayed on Screen, what is the width, the constraints and etc.
That's why we found final syntaxes before class properties in StatelessWidget.
Similiar to our college life. Our Grades that marked in final report, will not change even after we graduate from university. As it said to be in Final Report, then it must be final.
Stateful Widget properties are mutable
Why ?
Because flutter expects business logic resides in StatefulWidget.
Changes to be made
So I suggest to change StudentsList Widget, from this :
class StudentsList extends StatelessWidget {
final List<Child> mList; // this is the issue
StudentsList({this.mList});
to this one :
class StudentsList extends StatefulWidget {
#override
_StudentsListState createState() => _StudentsListState();
}
class _StudentsListState extends State<StudentsList> {
// final List<Child> mList; // Do not mark this as final
List<Child> mList;
...
}
Working Repository
You may look working repository that is closely-related to your issue. Github
Stateless Widgets property cannot be immutable means in simple words is that it should not contain any non-final variables.
Simply convert it to Stateful widget and inside the class _StudentsListState create your variable WITHOUT the final keyword because you are modifying the value of that List.
If you want to keep stateless (IE you just need to return some data or maybe youre using hooks) you could also try toList() to create a copy, then modify, then replace the original list
I encountered this problem in a simple function, and I solved it like this.
Future<void> createProduct({required Product product, required List<File> images}) async {
for (final image in images) {
final imageId = const Uuid().v4();
final compressedimage = await ImageCompress.instance.compressFile(image);
final taskSnapShot = await StorageService.instance.uploadProductPhoto(
file: compressedimage,
productId: product.productId,
childUUID: imageId,
);
final downloadURL = await taskSnapShot.ref.getDownloadURL();
product.imagesUrl.add(downloadURL);
// sendProduct.addImages(downloadURL: downloadURL);
}
await _collection.doc().set(
product.toMap(),
);
}
turn it into this.
Future<void> createProduct({required Product product, required List<File> images})
async {
List<String> newUrls = [];
for (final image in images) {
final imageId = const Uuid().v4();
final compressedimage = await ImageCompress.instance.compressFile(image);
final taskSnapShot = await StorageService.instance.uploadProductPhoto(
file: compressedimage,
productId: product.productId,
childUUID: imageId,
);
final downloadURL = await taskSnapShot.ref.getDownloadURL();
newUrls.add(downloadURL);
// sendProduct.addImages(downloadURL: downloadURL);
}
final sendProduct = product.copyWith(imagesUrl: newUrls );
await _collection.doc().set(
sendProduct.toMap(),
);
}

Only one item is appearing at a time on list view

I cant seem to figure out how to get all of my items in my list to display in the list view
Currently, when I click my button to display the list, only one items shows up. If I click back, and click main button again, it shows 2 items from the list. Rinse and repeat, 3 items. I can't seem to debug with print statements to see where my error lies. When I try print(trails) or other variations, it says Instance of trail model (not very helpful). Any ideas?
Here's my code:
class HomeScreen extends State<MyApp> {
int counter = 0;
Future<List<TrailModel>> fetchData() async {
counter++;
var response = await get(
'https://www.hikingproject.com/data/get-trails?lat=39.733694&lon=-121.854771&maxDistance=10&key=200419778-6a46042e219d019001dd83b13d58aa59');
final trailModel = TrailModel.fromJson(json.decode(response.body));
//trails.add(trailModel);
setState(() {
trails.add(trailModel);
});
return trails;
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("HikeLocator")),
body: new RaisedButton(
child: Text("click me"),
onPressed: () async {
final trails = await fetchData();
Navigator.push(
context,
new MaterialPageRoute(builder: (context) => new ListScreen(trails)),
);
},
),
));
}
}
class ListScreen extends StatelessWidget {
final List<TrailModel> trails;
ListScreen(this.trails);
#override
Widget build(BuildContext ctxt) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Here are your trails"),
),
body: TrailList(trails),
);
}
}
class TrailList extends StatelessWidget {
final List<TrailModel> trails;
TrailList(this.trails);
Widget build(context) {
return ListView.builder(
itemCount: trails.length,
itemBuilder: (context, int index) {
Object myText = json.encode(trails[index].trails);
List<dynamic> myText2 = json.decode(myText);
return Text(myText2[index]['name']);
},
);
}
}
class TrailModel {
Object trails;
TrailModel(this.trails);
TrailModel.fromJson(Map<String, dynamic> parsedJson) {
trails = parsedJson['trails'];
}
}
I think my problem might lie in the fetchData(), but I'm not entirely sure. Trying to at least print out the values to limit where my problem might be. (Is it only adding 1 to the list each time I press the button? Is it only rendering one when I click it? Is it fetching all the data each click or only one json object? etc.)
Thank you kindly for any assistance. Sorry, I'm kind of new to dart, so this is a huge learning curve for
There are a couple problems in your code. The main reason this doesn't work as you expect is because you're parsing all elements of the json into one TrailModel object, but then your code assumes that you'll have multiple TrailModel objects.
The easiest way to fix it up and get it working is to use the list from TrailModel.trails instead of the one in the widget.
First, in ListScreen, pass just the first element in the list.
class ListScreen extends StatelessWidget {
final List<TrailModel> trails;
...
#override
Widget build(BuildContext ctxt) {
return new Scaffold(
...
body: TrailList(trails.first),
);
}
}
Then, in TrailList, use the trails list you have from TrailModel:
class TrailList extends StatelessWidget {
final TrailModel model;
TrailList(this.model);
Widget build(context) {
return ListView.builder(
itemCount: model.trails.length,
itemBuilder: (context, int index) {
final trail = model.trails[index];
...
},
);
}
}
When I try print(trails) or other variations, it says Instance of trail model (not very helpful)
print uses the output of the toString method in your classes. You're seeing Instance of trail model because that's the default implementation you get from the super class Object. You can override it to get something more useful:
class TrailModel {
#override
String toString() => 'trails=$trails';
}

Generating new Cards based on List of Objects

I have a list of objects, is there a way to loop over a Card widget so that for each object in this list, a new card created with the attributes of this widget of interest.
The following code definitely does not work, it only serves as an illustration to show the logic behind what I am trying to achieve
//function declaration and body...
List<CardInfo> list = new List();
for (var i in cardInfo){ //cardInfo is a list of json created previously.
list.add(new CardInfo.fromJson(i));
print (list); }//tested the list and it contains the objects I want
return list;
}
...
//my widget{
#override
Widget build(BuildContext context) {
return new Dismissible(
for (i in list){
child: new Card(
child:
new ListTile(leading: new Icon(Icons.star, color: Colors.yellowAccent,),
title: new Text(list(i).field1),//field1 is a string in CardInfo
subtitle: new Text(list(i).field2),//field2 is a string in CardInfo
is this doable by any mean ?
Update
I figured out converting each JSON into object and then working with the objects directly, now _loadData() function returns an object on each iteration
_loadData() async {
var url = 'myurl';
var httpClient = createHttpClient();
var response =await httpClient.get(url);
List cardInfo = JSON.decode(response.body);
List<CardInfo> list = new List();
for ( var i in cardInfo){
var ci = new CardInfo.fromJson(i);
list.add(new CardInfo.fromJson(i));
return ci;
}
//return list;
}
....
So is there a way to set up my class constructor so that it allows me to access the fields of the returned objects ? something like this:
title: new Text(new CardInfo(_loadData()).field)
Update 2:
#override
Widget build(BuildContext context) {
return new FutureBuilder(
future: _loadData(),
builder: (BuildContext context, AsyncSnapshot<List> jsonList){
if (jsonList.hasData==true){
var cardInfo = jsonList.data[0];//is this right ?
print (cardInfo);
return new Dismissible(
child: new Column(
children:
cardInfo.map(
(CardInfo info)=>
new Card(
child: new ListTile(leading: new Icon(Icons.star, color: Colors.yellowAccent,),
title: new Text(info.field),
subtitle: new Text("Subtitle"),)
) ), ),....
My function is at the top as follows:
_loadData() async {
var url = 'myurl';
var httpClient = createHttpClient();
var response = await httpClient.get(url);
List cardInfo = JSON.decode(response.body);
List<CardInfo> list = new List();
for (var i in cardInfo) {
list.add(new CardInfo.fromJson(i));
}
return list;
}
I get the following error:
Class 'CardInfo' has no instance method 'map'.
You can use the Iterable.map method to do this. For example:
new Column(
children: list.map(
(CardInfo info) => new Card(child: new Text(info.field1)),
).toList()
)