I have made the following reusable widget in Flutter. It is used to show a dropdown list of items taken from Cloud Firestore, and when a user selects an option it should be passed to the variable answer.
However when I use my widget Survey Answer the dropdown items show as expected, but I am unable to pass the selected option out from onChanged out of my widget - print(widget.answer); shows the right item in the Dart Console, the item selected from the dropdown menu, but when I use this widget on a different page and try to print out the value of the answer variable I just get null returned.
Can someone shed some light on this please?
Reusable widget:
class SurveyAnswer extends StatefulWidget {
final String collection1;
final String document;
final String collection2;
var answer;
SurveyAnswer(
{Key key,
#required this.collection1,
#required this.document,
#required this.collection2,
#required this.answer})
: super(key: key);
#override
_SurveyAnswerState createState() => _SurveyAnswerState();
}
class _SurveyAnswerState extends State<SurveyAnswer> {
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance
.collection(widget.collection1)
.document(widget.document)
.collection(widget.collection2)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.hasError)
return Center(child: Text("Error"));
else if (!snapshot.hasData)
return Center(child: Text("Loading..."));
else if (snapshot.data.documents.isEmpty)
return Center(child: Container(width: 0, height: 0));
else {
List<DropdownMenuItem> answerItems = [];
for (int i = 0; i < snapshot.data.documents.length; i++) {
DocumentSnapshot snap = snapshot.data.documents[i];
answerItems.add(
DropdownMenuItem(
child: Text(
snap.documentID,
style: TextStyle(color: Color(0xff303841)),
),
value: "${snap.documentID}",
),
);
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FormBuilderDropdown(
attribute: 'attribute',
hint: Text('Please choose an option'),
validators: [FormBuilderValidators.required()],
items: answerItems,
onChanged: (answerValue) {
setState(() {
widget.answer = answerValue;
print(widget.answer);
});
},
),
],
);
}
});
}
}
How the above is used in my app:
SurveyAnswer(
collection1: 'MySurveys',
document: 'FirestoreTestSurvey',
collection2: 'a01',
answer: 'ans01'),
In this example, I want to create a variable ans01 which returns the value selected in the dropdown menu.
The reason I have made a reusable widget for this is that each page has a lot of survey items and otherwise my code would be a mess.
Thank you.
you should write callback function
class SurveyAnswer extends StatefulWidget {
final String collection1;
final String document;
final String collection2;
final Function(String) onAnswer;
SurveyAnswer(
{Key key,
#required this.collection1,
#required this.document,
#required this.collection2,
#required this.onAnswer})
: super(key: key);
#override
_SurveyAnswerState createState() => _SurveyAnswerState();
}
Change your on change method
onChanged: (answerValue) {
widget.onAnswer(answerValue)
}
and use like this
SurveyAnswer(
collection1: 'MySurveys',
document: 'FirestoreTestSurvey',
collection2: 'a01',
onAnswer: (ans) {
print(ans)
})
Related
The two places highlighted are the cause of the problem. In the image as shown below, after I add a task, I am not able to individually select a task, instead all the tasks that I have added get selected collectively. How do I fix this to just select the task that I click on?
This is the Tasks class that extends the ChangeNotifier:
class Tasks extends ChangeNotifier {
bool value = false;
List<String> taskList = [
'Buy Milk',
];
void addTask(String newTask) {
taskList.add(newTask);
notifyListeners();
}
}
This is the updated entire tasks.dart file:
class TaskList extends StatelessWidget {
const TaskList({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<Tasks>(
builder: (context, value, child) {
return ListView.builder(
itemCount: value.taskList.length,
itemBuilder: (context, index) {
return TaskTile(
listText: value.taskList[index],
functionCallback: (newValue) {}, //Enter Function Here.
);
},
);
},
);
}
}
class TaskTile extends StatelessWidget {
String? listText;
Function(bool?)? functionCallback;
TaskTile({this.listText, this.functionCallback, Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return CheckboxListTile(
title: Text(
listText!,
style: TextStyle(
decoration: Provider.of<Tasks>(context, listen: false).boolValue
? TextDecoration.lineThrough
: null,
),
),
activeColor: Colors.black,
value: Provider.of<Tasks>(context, listen: false).boolValue,
onChanged: functionCallback,
);
}
}
The actual problem is that you are using the same boolean value for all the check boxes' state(true/false[weather its selected or not]).
So, when you click on one checkbox it sets the value of value(variable) to true and therefore all the checkboxes read the value from the common value (which becomes true).Therefore,every box gets selected.
Solution : You may use different variables for different check boxes' state(true/false) if the number of checkboxes is limited,otherwise go for a differnet approach.
You are getting the whole class when you call provider.
In addition, value is a global variable for the class itself, not for the items inside taskList.
So if you need to modify a Task individually you can do something like this:
class Tasks extends ChangeNotifier {
bool value = false;
List<Task> taskList = [
Task('Buy Milk'),
];
void addTask(Task newTask) {
taskList.add(newTask);
notifyListeners();
}
void deltaValue(bool b, int index) {
taskList[index].value = !taskList[index].value; // Individual task value
notifyListeners();
}
}
Instead of using a List of String you can create a new class called Task to store the values:
class Task extends ChangeNotifier {
String name;
bool value = false;
Task(this.name);
}
The last step would be to use a Widget that displays all the values stored on the List.
For example you can use ListView.builder, so you have the index and you can use it to modify the individual value of a Task:
class TaskTile extends StatelessWidget {
String? listText;
TaskTile({this.listText, Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final tasks = Provider.of<Tasks>(context, listen: false);
final taskList = tasks.taskList; //ListView helps you iterate over all the elements on the list
return ListView.builder(
itemCount: taskList.length,
itemBuilder: (context, index) {
final task = taskList[index];
return CheckboxListTile(
title: Text(
listText!,
style: TextStyle(
decoration: task.value
? TextDecoration.lineThrough
: null,
),
),
activeColor: Colors.black,
value: task.value,
onChanged: (newValue) {
Provider.of<Tasks>(context, listen: false)
.deltaValue(newValue!,index); //Problem Here.
},
);
});
}
}
The method I used was that I created an extra map in the Tasks class and defined a map called taskMap and used the strings defined in the taskList and the bool value to control TaskTile.
The addTask function is used when adding tasks to the taskList elsewhere in the program, but it also adds tasks to the taskMap.
The Tasks class:
class Tasks extends ChangeNotifier {
String? task;
List<String> taskList = [
'Buy Milk',
];
Map<String, bool> taskMap = {
'Buy Milk': false,
};
void addTask(String newTask) {
taskList.add(newTask);
taskMap[newTask] = false;
notifyListeners();
}
void deltaValue(String newTask) {
taskMap[newTask] = !taskMap[newTask]!;
notifyListeners();
}
}
The entire tasks.dart file:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:todoey_flutter/main.dart';
class TaskList extends StatelessWidget {
const TaskList({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<Tasks>(
builder: (context, value, child) {
return ListView.builder(
itemCount: value.taskList.length,
itemBuilder: (context, index) {
return TaskTile(
listText: value.taskList[index],
functionCallback: (newValue) {
value.deltaValue(value.taskList[index]);
},
);
},
);
},
);
}
}
class TaskTile extends StatelessWidget {
String? listText;
Function(bool?)? functionCallback;
TaskTile({this.listText, this.functionCallback, Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return CheckboxListTile(
title: Text(
listText!,
style: TextStyle(
decoration:
Provider.of<Tasks>(context, listen: false).taskMap[listText]!
? TextDecoration.lineThrough
: null, //Bool value defined in the taskMap used.
),
),
activeColor: Colors.black,
value: Provider.of<Tasks>(context, listen: false).taskMap[listText], //Bool value defined in the taskMap used.
onChanged: functionCallback,
);
}
}
I need a DropdownButton with items depending on another DropdownButton. Sounds a bit confusing but it isnt. Here is my code with comments at the important parts in order to understand my intention.
Parent
class Parent extends StatefulWidget {
const Parent({ Key? key }) : super(key: key);
#override
State<Parent> createState() => _ParentState();
}
class _ParentState extends State<Parent> {
#override
Widget build(BuildContext context) {
return SafeArea(
child: SizedBox(
width: 500,
height: 500,
child: Column(
children: const [
// Main
DropDownWidget(collection: "MainCollection",),
// Depending
DropDownWidget(collection: ""), // Collection should equals value from Main DropDownWidget
],
),
),
);
}
}
Child
class DropDownWidget extends StatefulWidget {
final String collection;
const DropDownWidget({Key? key, required this.collection}) : super(key: key);
#override
State<DropDownWidget> createState() => _DropDownWidgetState();
}
class _DropDownWidgetState extends State<DropDownWidget> {
var selectedItem;
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection(widget.collection)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.hasError) {
return const CircularProgressIndicator();
} else {
var length = snapshot.data?.docs.length;
List<DropdownMenuItem<String>> items = [];
for (int i = 0; i < length!; i++) {
DocumentSnapshot snap = snapshot.data!.docs[i];
items.add(DropdownMenuItem(
child: Text(snap.id),
value: snap.id,
));
}
return DropdownButtonFormField<String>(
onChanged: (value) {
setState(() {
selectedItem = value;
// ********************
// PASS value TO PARENT
// ********************
});
},
value: selectedItem,
items: items);
}
});
}
}
When the Main DropdownButton changes its value, it should pass that to my parent in order to change the focused collection of my depending DropdownButton. I already solved that problem by throwing all the code in one class buts that not the way I want to go.
So maybe you can help me out :)
Thanks
Create an argument ValueChanged<String> onSelectItem in your child. Call the method when the value changes.
Then in your parent, you provide a function that needs to be called when the value changes in your child.
I have the dropdownbutton widget below, with a futurebuilder that reads a list from a google spreadsheet. The widget works good as intended.
I want to reuse this widget within the app, and simply pass a different Future
As example, I want to call the same widget, but pass different lists
mydropdownbutton ( futureList1 ),
mydropdownbutton ( futureList2 ),
mydropdownbutton ( futureList3 ),
//========================================================
// WIDGET: FUTURE DROPDOWN MENU + FUTURE LIST
//--------------------------------------------------------
class dropDownButtonWidget extends StatefulWidget {
const dropDownButtonWidget({ Key? key,}) : super(key: key);
#override
State<dropDownButtonWidget> createState() => _dropDownButtonWidgetState();
}
// --------------------------------------------------------
class _dropDownButtonWidgetState extends State<dropDownButtonWidget> {
#override
Widget build(BuildContext context) {
return Center(
child: FutureBuilder(future: futureList(),
builder: (BuildContext context, AsyncSnapshot<List> snapshot) {
if(snapshot.hasData){ List? futureDataList = snapshot.data;
futureDataList ??= ['Loading']; //if snapshot is null
return buildDropdownButton(dropdownList: futureDataList );
}else if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}else {
return Center(child: CircularProgressIndicator());
}
}
)
);
}
//----------------------------------
// DROPDOWNBUTTON EXTRACTED METHOD
DropdownButton<Object> buildDropdownButton({required List dropdownList}) {
String defaultValue = dropdownList.first; //DEFAULT SELECTED ITEM
return DropdownButton(
value: defaultValue,
onChanged: (value) => setState(() => defaultValue = value.toString()),
items: dropdownList.map((items) {
return DropdownMenuItem(value: items, child: Text(items));
}).toList(),
);
}
//----------------------------------
}
//=============================================
//=============================================
//FUTURE LIST FOR THE DROPDOWN MENU
//=============================================
Future<List> futureList() async {
var items = await ScheduleInfo.mx_schedule_WEEKDAYS_as_List(debugONOFF: 1);
return items;}
//=============================================
How can I make this widget modular and reusable?
you can send the future to this widget's constructor. first you declare a variable and set it to constructor. then you can reference that variable in the state of that class by widget.variable keyword. something like this
class dropDownButtonWidget extends StatefulWidget {
final variable;
dropDownButtonWidget(this.variable);
const dropDownButtonWidget({ Key? key,}) : super(key: key);
#override
State<dropDownButtonWidget> createState() => _dropDownButtonWidgetState();
}
class _dropDownButtonWidgetState extends State<dropDownButtonWidget> {
widget.variable //whatever you want to do with it
}
Community... after few hours of reading and testing, I found the solution to my own question. I am posting the solution for anyone else needing it.
Probably, my code can be improved, I welcome suggestions.
My question above has a working code for a dropdown button widget (fully working as today), using a Future
Below, my own answer, the same widget transformed into a reusable modular widget.
( This works only with future lists (async) )
Simple SCREEN WIDGET (with nested dropdownbutton widgets):
class Screen01 extends StatefulWidget {
const Screen01({
Key? key,
}) : super(key: key);
#override
State<Screen01> createState() => _Screen01State();
}
class _Screen01State extends State<Screen01> {
#override
Widget build(BuildContext context) {
return Center(child:
Flex(
direction: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children:
[ // each time you call the widget, you can provide a different future list
dropDownButtonWidgetModular(futureList: dropdownFutureList()),
SizedBox(width: 50,),
dropDownButtonWidgetModular(futureList: anotherFutureList())
]
),
);
}
}
DROPDOWNBUTTON WIDGET:
//========================================================
// WIDGET: FUTURE DROPDOWN MENU + FUTURE LIST
//--------------------------------------------------------
class dropDownButtonWidgetModular extends StatefulWidget {
final Future<List> futureList; // ===> ADDED FUTURE LIST
const dropDownButtonWidgetModular({ Key? key, required this.futureList}) : super(key: key); // ===> ADDED REQUIRED FUTURE LIST
#override
State<dropDownButtonWidgetModular> createState() => _dropDownButtonWidgetModularState(chosenFutureList: futureList);
}
// --------------------------------------------------------
class _dropDownButtonWidgetModularState extends State<dropDownButtonWidgetModular> {
Future<List> chosenFutureList; // ===> ADDED FUTURE LIST
String? defaultValue;
_dropDownButtonWidgetModularState({required this.chosenFutureList}); // ===> ADDED REQUIRED FUTURE LIST
#override
Widget build(BuildContext context) {
return Center(child:
FutureBuilder(future: chosenFutureList, // ===> ADDED FUTURE LIST
builder: (BuildContext context, AsyncSnapshot<List> snapshot) {
if(snapshot.hasData){ List? futureDataList = snapshot.data;
futureDataList ??= ['Loading']; //if snapshot is null
return buildDropdownButton(dropdownList: futureDataList );
}else if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}else {
return Center(child: CircularProgressIndicator());
}
}
)
);
}
//----------------------------------
// DROPDOWNBUTTON EXTRACTED METHOD
DropdownButton<Object> buildDropdownButton({required List dropdownList}) {
defaultValue ??= dropdownList.first; //DEFAULT SELECTED ITEM
return DropdownButton(
value: defaultValue,
onChanged: (value) => setState(() => defaultValue = value.toString()),
items: dropdownList.map((items) {
return DropdownMenuItem(value: items, child: Text(items));
}).toList(),
);
}
//----------------------------------
}
//=============================================
I commented some of the changes made from my original question
I have created a filter to filter UI between two dates. My problem is that the UI does not accurately reflect this, instead I notice that the former filteredList is used BUT the build .length method calculates accurately... resulting in displaying the wrong items from the list.
See code below, simplified for convenience:
homepage.dart
class _MyHomePageState extends State<HomePage> {
void _refreshFilter() {
setState(() => filteredList = myList.where((item) {
DateTime date = DateTime(item.date.year, item.date.month, item.date.day);
return date != null && (dateFilterFrom != null ? date.difference(dateFilterFrom).inDays >= 0: true) &&
(dateFilterTo != null ? dateFilterTo.difference(date).inDays >= 0: true);})
.toList());
showSnackBar(context, 'Filter refreshed!');
#override
Widget build(BuildContext context) {
return Display(filteredList);
}
display.dart
class Display extends StatefulWidget {
const Display(this.filteredList, {Key key})
: super(key: key);
final List filteredList;
#override
_DisplayState createState() => _DisplayState();
}
class _DisplayState extends State<Display> {
#override
Widget build(BuildContext context) {
return Flexible(
child: Padding(
padding: EdgeInsets.all(20.0),
child: ListView.builder(
shrinkWrap: true,
itemCount: filteredList.length,
itemBuilder: (context, index) {
return MyItem(index),
));
}
});
}
myitem.dart
class MyItem extends StatefulWidget {
MyItem(this.index, {Key key}) : super(key: key);
final int index;
#override
_MyItemState createState() => _MyItemState();
}
class _MyItemState extends State<MyItem> {
DateTime dateTime;
#override
Widget build(BuildContext context) {
return OutlineButton(
borderSide: BorderSide(color: Colors.blue),
onPressed: () {
DatePicker.showDateTimePicker(context, showTitleActions: true,
onChanged: (value) {
print('change $value in time zone ' +
value.timeZoneOffset.inHours.toString());
}, onConfirm: (value) {
print('confirm $value');
setState(() {
dateTime = value;
filteredList[widget.index].date = value;
});
}, currentTime: dateTime != null ? dateTime : DateTime.now());
},
child: Text(
dateTime != null
? dateTime.toString().substring(0, 16)
: "select date",
style: TextStyle(color: Colors.blue),
));
}
}
filteredList is in globals.dart
Any tips strongly appreciated, or simply a method to have a parent widget with function that will rebuild the builder from the child widget...
NB: When I debug filteredList print the accurate items, so this is definitely a UI/state issue
In your Display widget class, add required List parameter, something like this:
class Display extends Stateless {
final List filteredList;
const Display({required this.filteredList});
Now, in your homepage where you callDisplay(), it'll ask you for the list,
So, it will look like this now:
Widget build(BuildContext context) {
return Display(filteredList: filteredList);
This way, you can convert your listviewBuilder to be in a stateless widget, where you get better performance. And from your homepage, whenever you press the filter function, it'll rebuild your Display widget, with actual filtered list.
I'm trying to print my card widget title in the card details page but I'm getting " A non-null String must be provided to a Text widget".
Any suggestion or help on how can I fix this?.
Model.dart
class Item {
int id;
String title;
Item(
{this.id, this.title });
}
CardWidget.dart
import 'package:maxis_mobile/ui/screens/cardDetails-screen.dart';
import 'cardModel.dart';
class _CardWidgetState extends State<CardWidget> {
final item = Item();
#override
Widget build(BuildContext context) {
return Container(
child: SingleChildScrollView(
child: Card(
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CardDetails(item: item), //not sure this is right.
));
},
child: Column(children: [
Container(
child: Row(
children: [
Container(
child: Text(
widget.title, // Card Title
),
),
),
CardDetails.dart
import 'package:flutter/material.dart';
import '../shared/cardModel.dart';
class CardDetails extends StatelessWidget {
final Item item;
CardDetails({Key key, #required this.item}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
child: Text(item.title),
);
}
}
DummyData.dart
List<Item> items = [
Item(id: 0, title: '1. US Daily Retail Delivery By Brands'),
]
In _CardWidgetState you have defined an empty object. That object you passed to the CardDetails page through the constructor, and on the CardDetails page you try in Text widget call item.title but it is null (without value). You need populate this object or try with hardcode string.
Same ase here: A non-null String must be provided to a Text widget
The cause is item declared in CardWidget.dart - there is no title, so item.title in CardDetails.dart is null. To fix the error you can add default value for tile field in Item class:
class Item {
int id;
String title;
Item({this.id, this.title = ''}) : assert(title != null);
}