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),
)))
Related
I'm new to programming. I have created a flutter drawer, which works fine. However, I have learned about for loops and now want to shorten my code so that I don't show 10 x ListTiles. I now have a for loop for my ListTile. I managed to get the drawer working fine, with the Text and the Icon, but cannot seem to get the onTap to work with the routes (pages). I can get the Navigator.pushNamed to return a single page in my routes, but then this same page get returned for all my items in all my ListTiles.
I have created routes in my main.dart. I have created my class and list of pages. I have created my for loop in a function outside of the Builder and parsed Buildcontext (context) into the function, which seems to be ok. My code does not throw errors, but only return one page.
I have spend four days now reading do many similar kind of solutions, but I simply can't find a workable solution for myself. I don't seem to find a proper answer to my problem as all solutions through out some kind of error, or at best, no errors, but it does not work. Here is my code.
// MAIN.DART FILE TO ILLUSTRATE PAGE ROUTES
class MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
routes: {
'/page2': (context) => const InvoiceApp(),
'/page3': (context) => const Currency(),
'/page4': (context) => const Expenses(),
'/page5': (context) => const Reports(0),
'/page6': (context) => const Graphs(0),
'/page7': (context) => const Download(0),
'/page8': (context) => const Customer(),
'/page9': (context) => const People(0),
'/page10': (context) => const Resources(0),
'/page11': (context) => const Profile(),
},
home: Scaffold(
//CLASS ROUTES
class Routes {
String page;
Routes({required this.page});
}
//LIST
list<Routes> routeList = [
Routes(page: '/page2'),
Routes(page: '/page3'),
Routes(page: '/page4'),
Routes(page: '/page5'),
Routes(page: '/page6'),
Routes(page: '/page7'),
Routes(page: '/page8'),
Routes(page: '/page9'),
Routes(page: '/page10'),
Routes(page: '/page11'),
];
//MAIN CODE
class MenuBar extends StatelessWidget {
const MenuBar({Key? key}) : super(key: key);
List<ListTile> getMenuItems(BuildContext context, var index) {
List<ListTile> menuList = [];
for (var i = 0; i < menuItemList.length; i++) {
var item = menuItemList[i];
var loopCode = ListTile(
title: Text(item),
leading: Icon(
icons[i],
color: Colors.teal,
),
// onTap: () {
// Navigator.pushNamed(context, routeList[index].page);
// },
onTap: () {
Navigator.pushNamed(context, '/page3');
},
);
menuList.add(loopCode);
}
return menuList;
}
#override
Widget build(
BuildContext context,
) {
// getMenuItems(context);
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
const AccountHeader(),
Column(
children: getMenuItems(context, routeList),
),
],
),
);
}
List<String> menuItemList = [
'Invoices',
'Currency Converter',
'Expenses',
'Reports',
'Graphs',
'Downloads',
'Customers',
'People Management',
'Resources',
'Profile',
];
List<IconData> icons = [
Icons.receipt,
Icons.money,
Icons.money,
Icons.report,
Icons.bar_chart,
Icons.download,
Icons.people,
Icons.people,
Icons.cake,
Icons.person,
];
PLEASE NOTE:
I have 10 pages in my routes, yet my routes starts at page 2 and ends at page 11. This is not an issue as my main.dart file is my landing page for my app.
My problem is onTap. You will notice two onTap codes (one is commented out). If I use the current active one (...page3), then it works, but all 10 pages then return to page 3 once any of the ten tiles are tapped. If I use the onTap that is commented out, then the app loads, the drawer opens find, but no tile will navigate to any page. Also no errors in my code is issued.
Can you please help me. Thank you kindly.
The problem is that you're using the index that is being passed from the function instead of the index inside the loop.
replace the commented code
// onTap: () {
// Navigator.pushNamed(context, routeList[index].page);
// },
with
onTap: () {
Navigator.pushNamed(context, routeList[i].page);
},
the i is the index of the for loop.
I have a list of profile fields that I would like to create using a single widget, but I'm new to Flutter and can't seem to work out one single thing: passing a list variable through a parameter to a drop down menu. I have been able to create many drop down field widgets that work just fine, using (for example) _countries.map(), but now that I'm trying to convert the drop down field to a single widget as not to repeat myself, it gives me an error Unhandled Exception: type 'List<dynamic>' is not a subtype of type 'List<DropdownMenuItem<String>>' when I try to pass it through a variable.
I have the following code (of course removing all that I believe to be unnecessary):
class _ProfileDataState extends State<ProfileData> {
final _countries = DropDownLists.countries;
String _country;
var listType;
Widget profileDropDown(var list, var listType) {
return Card(
onTap: () async {
AlertDialog(
content: DropdownButtonFormField<String>(
isExpanded: true,
items: list.map((String value) { // <-- If I test this with _countries.map(), it works
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
isDense: true,
value: listType,
onChanged: (value) {
FocusScope.of(context).requestFocus(FocusNode());
setState(() {
this.listType = value;
});
},
)
)
}
)
}
#override
Widget build(BuildContext context) {
return profileDropDown(_countries, _country),
...
I'm trying to make a buttomtabbar, where there is a List (Stack) on the first Tap page. Somehow there is this error and I just can't figure out why...
This is my Home file:
class Home extends StatelessWidget {
final AuthService _auth = AuthService();
#override
Widget build(BuildContext context) {
return TabContainerIndexedStack();
}}
This is TabContainerIndexStack():
class TabContainerIndexedStack extends StatefulWidget {
TabContainerIndexedStack({Key key}) : super(key: key);
#override
_TabContainerIndexedStackState createState() =>
_TabContainerIndexedStackState();
}
class _TabContainerIndexedStackState extends State<TabContainerIndexedStack> {
int tabIndex = 0;
List<Widget> listScreens;
#override
void initState() {
super.initState();
listScreens = [
Tab1(),
Tab2(),
Tab3(),
];
}
// #override
// bool get wantKeepAlive =>
// true; //by default it will be null, change it to true.
#override
Widget build(BuildContext context) {
return MaterialApp(
color: Colors.yellow,
home: Scaffold(
body: IndexedStack(index: tabIndex, children: listScreens),
bottomNavigationBar: BottomNavigationBar(
currentIndex: tabIndex,
onTap: (int index) {
setState(() {
tabIndex = index;
});
},
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Tab1'),
),
BottomNavigationBarItem(
icon: Icon(Icons.report_problem),
title: Text('Tab2'),
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
title: Text('Tab3'),
),
]),
backgroundColor: Theme.of(context).primaryColor,
),
);
}
}
This is my first Tab (other ones work!!!)
class Tab1 extends StatefulWidget {
#override
_Tab1State createState() => _Tab1State();
}
class _Tab1State extends State<Tab1> with AutomaticKeepAliveClientMixin<Tab1> {
#override
void initState() {
super.initState();
print('initState Tab1');
}
#override
Widget build(BuildContext context) {
print('build Tab1');
return Scaffold(
appBar: AppBar(
title: Text('Tab1'),
),
body: AuftraegeList()
);
}
#override
bool get wantKeepAlive => true;
}
I think the problem is right there in the "body: AuftraegeList()..." above..
Here is the file AuftraegeList():
class AuftraegeList extends StatefulWidget {
#override
_AuftraegeListState createState() => _AuftraegeListState();
}
class _AuftraegeListState extends State<AuftraegeList> {
#override
Widget build(BuildContext context) {
final auftraege = Provider.of<List<Auftrag>>(context);
return ListView.builder(
itemCount: auftraege.length,
itemBuilder: (context, index){
return AuftragTile(auftrag: auftraege[index]);
},
);
}
}
I hope this is enough to solve my problem. I'm very new to Flutter, so it would be nice, if you can say where EXACTLY I have to change WHAT. Thank you so much!!!
EDIT: Here is the Code of my home.dart, which is the code, which represents the list in my main view.
class Home extends StatelessWidget {
final AuthService _auth = AuthService();
#override
Widget build(BuildContext context) {
return TabContainerIndexedStack();
/*
return StreamProvider<List<Auftrag>>.value(
value: DatabaseService().auftraege,
child: Scaffold(
bottomNavigationBar: btmBar(),
backgroundColor: Colors.blue[50],
appBar: AppBar(
title: Text('Home title'),
backgroundColor: Colors.blue,
elevation: 0.0,
actions: <Widget>[
FlatButton.icon(
icon: Icon(Icons.person),
label: Text('logout'),
onPressed: () async{
await _auth.signOut();
},
)
],
),
body: AuftraegeList(),
),
);
*/
}
}
(Its the part I commented out)
Thanks!!
EDIT (2) !!!
Latest edit:
So my first tap class now looks like this (I changed in the Widget build body [] to databaseService.auftraege after declaring databaseService at first).
class Tab1 extends StatefulWidget {
#override
_Tab1State createState() => _Tab1State();
}
class _Tab1State extends State<Tab1> with AutomaticKeepAliveClientMixin<Tab1> {
// Where should I put this line? Whereever I put this, it gives me errors (already imported services/database.dart)
final DatabaseService databaseService = Provider.of<DatabaseService()>(context);
#override
void initState() {
super.initState();
print('initState Tab1');
}
#override
Widget build(BuildContext context) {
print('build Tab1');
return Scaffold(
appBar: AppBar(
title: Text('Tab1'),
),
body: Provider(
create: (context) => databaseService.auftraege,
child: AuftraegeList(),
)
);
}
#override
bool get wantKeepAlive => true;
}
Maybe its also helpful to show you my services/database
class DatabaseService{
final String uid;
DatabaseService({ this.uid });
// collection reference
final CollectionReference auftraegeCollection = Firestore.instance.collection('auftraege');
Future updateUserData(String title, String info, int price, String user) async{
return await auftraegeCollection.document(uid).setData({
'title' : title,
'info': info,
'price': price,
'user': user,
});
}
List<Auftrag> _auftragListFromSnapshot(QuerySnapshot snapshot){
return snapshot.documents.map((doc){
return Auftrag(
title: doc.data['title']?? '',
info: doc.data['info']?? '',
price: doc.data['price']?? 0,
user: doc.data['user']?? '',
);
}).toList();
}
// get auftraege stream
Stream <List<Auftrag>> get auftraege {
return auftraegeCollection.snapshots()
.map(_auftragListFromSnapshot);
}
}
When leave the code like that, it gives me an error at this line:
final DatabaseService databaseService = Provider.of<DatabaseService()>(context);
in my tab1.dart class. It says "Only static members can be accessed in initializers" under "context" and "error: A comparison expression can't be an operand of another comparison expression" as well as "error: The operator '<' isn't defined for the class 'T Function(BuildContext, {listen: bool})'."
Maybe you know what to do. I think I just put this line at the wrong place.
### EDIT (3) ###
(Exception)
════════ Exception caught by widgets library ═══════════════════════════════════════════════════════
The following assertion was thrown building _BodyBuilder:
dependOnInheritedWidgetOfExactType>() or dependOnInheritedElement() was called before _Tab1State.initState() completed.
When an inherited widget changes, for example if the value of Theme.of() changes, its dependent widgets are rebuilt. If the dependent widget's reference to the inherited widget is in a constructor or an initState() method, then the rebuilt dependent widget will not reflect the changes in the inherited widget.
Typically references to inherited widgets should occur in widget build() methods. Alternatively, initialization based on inherited widgets can be placed in the didChangeDependencies method, which is called after initState and whenever the dependencies change thereafter.
The relevant error-causing widget was:
Scaffold file:///Users/xxxxxxxxxxxxxxx/AndroidStudioProjects/promi_prototype/lib/screens/home/tab_containter_indexedstack.dart:36:13
When the exception was thrown, this was the stack:
0 StatefulElement.dependOnInheritedElement. (package:flutter/src/widgets/framework.dart:4467:9)
1 StatefulElement.dependOnInheritedElement (package:flutter/src/widgets/framework.dart:4510:6)
2 StatefulElement.inheritFromElement (package:flutter/src/widgets/framework.dart:4458:12)
3 Element.inheritFromWidgetOfExactType (package:flutter/src/widgets/framework.dart:3556:14)
4 Provider.of (package:provider/src/provider.dart:259:19)
It says, the relevant error-causing widget was: tab_containter_indexedstack.dart, I already posted this code at the very beginning of my post. The simulator now only shows the blue background with the tabbar at the bottom. No text at the other tabs (worked before) and no error warning at tab1. Even no headings.
Greetings!! :)
.
.
.
### EDIT (4) ###
Oh my god xD Sorry for not working.. and THANK YOU for still helping!
Lets start my the error message:
The following assertion was thrown building Provider>>(dirty, state:
flutter: _DelegateWidgetState#eb784):
flutter: Tried to use Provider with a subtype of Listenable/Stream (Stream>).
flutter:
flutter: This is likely a mistake, as Provider will not automatically update dependents
flutter: when Stream> is updated. Instead, consider changing Provider for more specific
flutter: implementation that handles the update mechanism, such as:
flutter:
flutter: - ListenableProvider
flutter: - ChangeNotifierProvider
flutter: - ValueListenableProvider
flutter: - StreamProvider
The relevant error-causing widget was:
flutter: Provider>>
flutter: file:///Users/xxxxxxxxxxxx/AndroidStudioProjects/promi_prototype/lib/screens/my_tabs/tab1.dart:33:13
So there still is an issue with the Provider in tab1.dart. My guess was to change the Provider thing in AuftragList() because there I was using it the "old" way like Provider.of>(context), just like you mentioned in the edit 18 hours ago.
This is what I did (Out-commented was before):
class AuftraegeList extends StatefulWidget {
#override
_AuftraegeListState createState() => _AuftraegeListState();
}
class _AuftraegeListState extends State<AuftraegeList> {
DatabaseService databaseService ;
#override
Widget build(BuildContext context) {
databaseService = Provider.of<DatabaseService>(context);
// final auftraege = Provider.of<List<Auftrag>>(context);
return ListView.builder(
// itemCount: auftraege.length,
// itemBuilder: (context, index){
itemCount: databaseService.length, //RED MARK HERE
itemBuilder: (context, index){
return AuftragTile(auftrag: databaseService[index]); //RED MARK HERE
},
);
}
}
I thought this is ok now, but I get red marks under ".lenght" and under "[index]". Compiler sais, that I should create a getter in helpers/database.dart which was what I tried then. But no success. I deleted the getters there then.
Do you have an idea? Is it right to change the Provider thing in the AuftraegeList() even though the Compiler said issue is in tap1.dart?
.
EDIT Since you are not imediately using the value for the Stream provider in the Home class, you can use a Provider instead as follows;
class Home extends StatelessWidget {
final AuthService _auth = AuthService();
#override
Widget build(BuildContext context) {
return Provider(
create: (context) => DatabaseServices(),
child: TabContainerIndexedStack()
);
},
}
Then when you need the list, you have to call a provider of the Service class and not the list
final DatabaseService databaseService = Provider.of<DatabaseService>(context);
Then you can access an instance of the list like
databaseService.auftraege
Edit 1
Assuming you have the provider in the home class, don't need to wrap the provider around AuftraegeList() again. This is what you should do.
class _Tab1State extends State<Tab1> with AutomaticKeepAliveClientMixin<Tab1> {
final DatabaseService databaseService;
#override
void initState() {
databaseService = Provider.of<DatabaseService>(context);
super.initState();
print('initState Tab1');
}
#override
Widget build(BuildContext context) {
print('build Tab1');
return Scaffold(
appBar: AppBar(
title: Text('Tab1'),
),
body:
// The use the list directly in this class where needed like `databaseService.auftraege`
child: AuftraegeList(),
)
);
}
#override
bool get wantKeepAlive => true;
}
Note
In a stateful widget, the BuildContext is available in the initState method and the build method. So you can use BuildContext out of those methods unless you implicitly pass it.
If you need the list in AuftragList() class then get an instance
of the list using Provider.of<DatabaseService>(context)
If you need the list to automatically update when a need item is available you can use a StreamSubscription to listen to need stream and add to the list.
When calling the provider of pass the dataType and not the class. That is Provider.of<DatabaseService>(context) and not Provider.of<DatabaseService()>(context).
Edit 2
Since you are getting a stream for you List, use a steamBuilder to builder your Ui
class AuftraegeList extends StatefulWidget {
#override
_AuftraegeListState createState() => _AuftraegeListState();
}
class _AuftraegeListState extends State<AuftraegeList> {
DatabaseService databaseService ;
#override
Widget build(BuildContext context) {
databaseService = Provider.of<DatabaseService>(context);
// final auftraege = Provider.of<List<Auftrag>>(context);
return StreamBuilder<List<Auftrag>>(
stream: databaseService.auftraege,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index){
return AuftragTile(auftrag: snapshot.data[index]);
}
);
}else if(snapshot.hasError){
return Center(child: Text("An error Errored");
}
return Center(child: CircularProgessIndicator();
},
);
}
}
Hope this works.
I have a StatefulWidget which state renders different Widget depending on loading state (Loading -> Loaded/Error):
// widget
class ListNotesScreen extends StatefulWidget {
static const route = '/listNotes';
static navigateTo(BuildContext context, [bool cleanStack = true]) =>
Navigator.pushNamedAndRemoveUntil(context, route, (_) => !cleanStack);
final String title;
final ListNotesUseCase _useCase;
final VoidCallback _addNoteCallback;
ListNotesScreen(this._useCase, this._addNoteCallback, {Key key, this.title}) : super(key: key);
#override
_ListNotesScreenState createState() => _ListNotesScreenState();
}
// state
class _ListNotesScreenState extends State<ListNotesScreen> {
ListNotesLoadState _state;
Future<ListNotesResponse> _fetchNotes() async {
return widget._useCase.listNotes();
}
#override
initState() {
super.initState();
_loadNotes();
}
_loadNotes() {
setState(() {
_state = ListNotesLoadingState();
});
_fetchNotes().then((response) {
setState(() {
_state = ListNotesLoadedState(response.notes);
});
}).catchError((error) {
setState(() {
_state = ListNotesLoadErrorState(error);
});
});
}
#override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text('Notes list'),
actions: <Widget>[
IconButton(icon: Icon(Icons.add), onPressed: widget._addNoteCallback),
IconButton(icon: Icon(Icons.refresh), onPressed: () => _loadNotes())
],
),
body: _state.getWidget());
}
// loading states
// State:
#sealed
abstract class ListNotesLoadState {
Widget getWidget();
}
// Loading
class ListNotesLoadingState extends ListNotesLoadState {
#override
Widget getWidget() => Center(child: CircularProgressIndicator(value: null));
}
// Loaded
class ListNotesLoadedState extends ListNotesLoadState {
final List<Note> _notes;
ListNotesLoadedState(this._notes);
#override
Widget getWidget() => ListView.builder(
itemBuilder: (_, int index) => NoteItemWidget(this._notes[index]),
itemCount: this._notes.length,
padding: EdgeInsets.all(18.0));
}
Here is the test for the widget:
void main() {
testWidgets('Notes list is shown', (WidgetTester tester) async {
final title1 = 'Title1';
final title2 = 'Title2';
final body1 = 'Body1';
final body2 = 'Body2';
var notes = [
Note('1', title1, body1),
Note('2', title2, body2),
];
final listUseCase = TestListNotesInteractor(notes);
final widget = ListNotesScreen(listUseCase, null, title: 'List notes');
await tester.pumpWidget(widget);
await tester.pumpAndSettle();
expect(find.text('someInvalidString'), findsNothing);
expect(find.text(title1), findsOneWidget);
expect(find.text(title2), findsOneWidget);
expect(find.text(body1), findsOneWidget);
expect(find.text(body2), findsOneWidget);
// TODO: fix the test (tested manually and it works)
});
}
So widget tester is expected to wait until the state it set to loading in initState(), then _loadNotes moves it to ListNotesLoadedState and ListNotesLoadedState.getWidget() to return ListView with expected string (NoteItemWidget root and few Text with expected string).
However the test fails. What's the reason (i was able to use test interactors in the app and visually see expected texts)? How can i analyze the actual Widgets tree on test failure?
I tend to think that WidgetTester did not wait for Future to be completed (though it's expected to be mocked and be sync behind the scenes, please correct me).
One can find the project on Github (make sure to call flutter packages pub run build_runner build to generate json de-/serialize code).
I've found the reason: MaterialApp (or probably any app) should be the root of widgets tree!
final widget = MaterialApp(home: ListNotesScreen(interactor, null)); // succeeds
instead of:
final widget = ListNotesScreen(interactor, null); // fails
Also i've removed unused title property so the test code is a bit different form what i used originally:
final widget = ListNotesScreen(listUseCase, null, title: 'List notes');
It's not mentioned in the docs (is it the reason actually?) though the test code has it. Please let me know if i miss something.
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(),
);
}