Related
I have a StatefulWidget where there is a ListView holding several childs widget.
One of the child is a GridView containing some items.
What I would want to achieve is to rebuild this GridView child when a button is pressed from the Parent widget. The button is located in the bottomNavigationBar in the Parent widget.
However, when I pressed the button, it should go to the _resetFilter() method, which works. But the setState() doesn't seem to update the GridView build() method inside Child widget.
class ParentState extends State<Parent> {
// removed for brevity
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(...),
bottomNavigationBar: BottomAppBar(
child: new Row(
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 5.0),
child: SizedBox(
onPressed: () {
_resetFilter();
},
)
),
],
),
),
body: Container(
child: Form(
key: _formKey,
child: ListView(
children: <Widget>[
Column(
children: <Widget>[
Container(
child: Column(
children: <Widget>[
Container(...), // this works
Column(...),
Container(...), // this works
Container(
child: GridView.count(
// ...
children:
List.generate(oriSkills.length, (int i) {
bool isSkillExist = false;
if (_selectedSkills.contains(rc.titleCase)) {
isSkillExist = true;
} else {
isSkillExist = false;
}
return Child( // this doesn't work
id: oriSkills[i]['id'],
name: oriSkills[i]['description'],
skillSelect: isSkillExist, // this boolean showed correct value from the above logic
onChange: onSkillChange,
);
}),
),
),
],
),
)
],
)
],
)),
),
);
}
void _resetFilter() {
setState(() {
_theValue = 0.0;
searchC.text = "";
_selectedSkills = []; // this is the variable that I'd like the GridView to recreate from.
});
}
}
I tried to print one of the field name inside Child widget, but it always showing the old value instead of the new one.
Even after presing the button, it does passing correct value to ChildState.
class ChildState extends State<Child> {
final String name;
final MyCallbackFunction onChange;
bool skillSelect;
double size = 60.0;
ChildState({this.name, this.skillSelect, this.onChange});
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
}
void setSkillLevel() {
setState(() {
if (skillSelect) {
skillSelect = false;
onChange(name, false);
} else {
skillSelect = true;
onChange(name, true);
}
});
}
Color _jobSkillSelect(bool select) {
print(select); // always print old state instead of new state
return select ? Color(MyColor.skillLvlOne) : Color(MyColor.skillDefault);
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(children: <Widget>[
InkResponse(
onTap: setSkillLevel,
child: Container(
height: size,
width: size,
decoration: BoxDecoration(
image: DecorationImage(
colorFilter: ColorFilter.mode(_jobSkillSelect(skillSelect), BlendMode.color),
),
),
)),
]));
}
}
How can I update the Child widget to have the updated value from the Parent widget after reset button is pressed?
You might want to pass the values to the actual Child class. Not to its state.
The class is whats rebuilding once your parent rebuilds. So the new values will be reflected.
So your Child implementation should look something like this (don't forget to replace the onChange Type to your custom Function.
class Child extends StatefulWidget {
final String name;
final Function(void) onChange;
final bool skillSelect;
final double size;
final Function(bool) onSkillLevelChanged;
const Child({Key key, this.name, this.onChange, this.skillSelect, this.size, this.onSkillLevelChanged}) : super(key: key);
#override
_ChildState createState() => _ChildState();
}
class _ChildState extends State<Child> {
Color _jobSkillSelect(bool select) {
print(select); // always print old state instead of new state
return select ? Color(MyColor.skillLvlOne) : Color(MyColor.skillDefault);
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
InkResponse(
onTap: () {
if (widget.onSkillLevelChanged != null) {
widget.onSkillLevelChanged(!widget.skillSelect);
}
},
child: Container(
height: widget.size,
width: widget.size,
decoration: BoxDecoration(
image: DecorationImage(
colorFilter: ColorFilter.mode(_jobSkillSelect(widget.skillSelect), BlendMode.color),
),
),
)),
],
),
);
}
}
In this case the Child ist not responsible anymore for managing its skillSelect property. It simply calls a Function on its parent. The parent then builds with a new skillSelect boolean.
So you might use this child like this:
return Child( // this doesn't work
id: oriSkills[i]['id'],
name: oriSkills[i]['description'],
skillSelect: oriSkills[i]['isSkillExist'],
onChange: onSkillChange,
onSkillLevelChanged: (newSkillLevel) {
setState(() {
oriSkills[i]['isSkillExist'] = newSkillLevel;
});
},
);
I am new to flutter. in my project, there is a various check_list_tile depending upon the length of the List (attendance list). And I have used one Boolean variable. Now when I press on one checkbox it automatically checks all other checkboxes. Please help me in this (on tap one checkbox should not change the state of all other checkboxes except clicked). I have copied all code please check check_box_list field.
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'dart:async';
Map map_student_data;
Iterable iter_student_data,iter_student_key;
List list_student_data,list_student_key;
bool t=true,checkbox=false;
List list;
String validation="yes";
int i;
int year;
final FirebaseDatabase database = FirebaseDatabase.instance;
class IImca_attendence extends StatefulWidget {
#override
_IImca_attendenceState createState() => _IImca_attendenceState();
}
class _IImca_attendenceState extends State<IImca_attendence> {
#override
void initState(){
this.check_year();
super.initState();
}
DateTime date = DateTime.now();
Future check_year()async{
var k= await database.reference().child("NITTE/CLASS/MCA").once().then((DataSnapshot snapshot){
Map sea= snapshot.value;
Iterable iter=sea.keys;
list=iter.toList();
list.sublist(list.length-1);
list.sort();
setState(() {
year=list.length-2;
});
check();
});
}
Future check()async{
var m=await database.reference().child("NITTE/CLASS/MCA/${list[year].toString().toUpperCase()}/STUDENT").once().then((DataSnapshot currentyear){
map_student_data=currentyear.value;
iter_student_data=map_student_data.values;
iter_student_key=map_student_data.keys;
list_student_data=iter_student_data.toList();
list_student_key=iter_student_key.toList();
for(i=0;i<=list_student_data.length;i++){
bool ss=true;
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("II-MCA"),
centerTitle: true,
actions: <Widget>[
IconButton(icon: Icon(Icons.refresh),onPressed: (){
setState(() {
check_year();
});
})
],
),
body:
validation=="1"?
new Center(
child: Text("STUDENT DOSE NOT EXIST IN $year",style: TextStyle(color: Colors.grey,fontWeight: FontWeight.bold,fontSize: 20),),
):
new ListView.builder(
itemCount: list_student_data==null?0
:list_student_data.length,
itemBuilder: (BuildContext context,int index){
var student_detail= ['NAME : ${list_student_data[index]['NAME']}','GENDER : ${list_student_data[index]['CURRENT CLASS']}','PHOTO : ${list_student_data[index]['PHOTO']}'];
return new Container(
child: new Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Card(
child: new Container(
child: CheckboxListTile(
title: Text("${list_student_key[index]}",style:TextStyle(fontWeight: FontWeight.bold,),),
subtitle: Text("NAME : ${list_student_data[index]['NAME']}"),
value: checkbox,
onChanged: (val){
setState(() {
checkbox=val;
if(checkbox==true){
print("${list_student_data[index]['NAME']}: i am absent");
}if(checkbox==false){
print("${list_student_data[index]['NAME']}: i am present");
}
});
},
),
padding: EdgeInsets.all(5),
),
)
],
),
),
);
}
)
);
}
}
Well... you are using a global checkbox variable, so It's quite normal that if you change it, all widget depending on its state will change accordingly.
What I suggest you to do is to add the selected state inside your model class. Just as an example, assumed you have this Student class (I know you are using firebase, but for sake of time I don't)
class Student {
var name = 'foo';
var year = '2018';
var selected = false;
Student(this.name);
}
This class has is selected state inside of it.
Now assume that your snapshot give you 3 students. Always for sake of time I've embedded a local array:
class _IImca_attendenceState extends State<IImca_attendence> {
var _students = [Student('foo'), Student('pub'), Student('beer')];
...
(Ellipses are not part of code... ;-])
I suggest you to put your state variables inside the Stateful Widget scope and not onto the Global Scope.
That said you could have:
ListView.builder(
itemCount: _students.length,
itemBuilder: (BuildContext context, int index) {
return new Container(
child: new Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Card(
child: new Container(
child: CheckboxListTile(
title: Text(
_students[index].name,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle:
Text("NAME : ${_students[index].name}"),
value: _students[index].selected,
onChanged: (val) {
setState(() {
_students[index].selected = val;
if (!_students[index].selected) {
print(
'${_students[index].name}: i am absent');
}
if (_students[index].selected) {
print(
'${_students[index].name}: i am present');
}
});
},
),
padding: EdgeInsets.all(5),
),
)
],
),
),
);
}
)
You should also use an array of bool(s) of the same length of your snapshot data students array... but I'd like to suggest to track this information directly on your Student model.
UPDATE
As you are more comfortable using array I've change my code using a complementary array of bool of the same size of your student list.
All you have to do is an array (not a single value) of boolean values, the same size of your student array, lets call this list_student_present (I instead use list_student_present2)
At the beginning you initialize this in your check function a way like that:
list_student_data = iter_student_data.toList();
// This is the array you wanna use (first all false)
list_student_present = iter_student_data.map((_) => false).toList();
And then you will use this array of bool to check the state of your checkboxes:
itemBuilder: (BuildContext context, int index) {
return new Container(
child: new Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Card(
child: new Container(
child: CheckboxListTile(
title: Text(
_students[index].name,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle:
Text("NAME : ${_students[index].name}"),
value: list_student_present[index],
onChanged: (val) {
setState(() {
list_student_present[index] = val;
if (!list_student_present[index]) {
print(
'${_students[index].name}: i am absent');
}
if (list_student_present[index]) {
print(
'${_students[index].name}: i am present');
}
});
},
),
padding: EdgeInsets.all(5),
),
)
],
),
),
);
}
Do not use my Student class (I continue using it so that I'm able to show you data without firebase), continue using your students array list from your firebase snapshot.
Full code:
import 'package:flutter/material.dart';
import 'dart:async';
Map map_student_data;
Iterable iter_student_data, iter_student_key;
List list_student_data, list_student_key, list_student_present, list_student_present2;
bool t = true;
List list;
String validation = "yes";
int i;
int year;
class Student {
var name = 'foo';
var year = '2018';
var selected = false;
Student(this.name);
}
class IImca_attendence extends StatefulWidget {
#override
_IImca_attendenceState createState() => _IImca_attendenceState();
}
class _IImca_attendenceState extends State<IImca_attendence> {
var _students = [Student('foo'), Student('pub'), Student('beer')];
#override
void initState() {
this.check_year();
super.initState();
}
DateTime date = DateTime.now();
Future check_year() async {
Map sea = {1: 'atlantic', 2: 'pacific'};
Iterable iter = sea.keys;
list = iter.toList();
list.sublist(list.length - 1);
list.sort();
setState(() {
year = list.length - 2;
});
check();
}
Future check() async {
map_student_data = {
0: {'NAME': 'foo', 'CURRENT CLASS': 'pub', 'PHOTO': ''}
};
iter_student_data = map_student_data.values;
iter_student_key = map_student_data.keys;
list_student_data = iter_student_data.toList();
// This is the array you wanna use
list_student_present = iter_student_data.map((_) => false).toList();
// This is the array for my example
list_student_present2 = _students.map((_) => false).toList();
list_student_key = iter_student_key.toList();
for (i = 0; i <= list_student_data.length; i++) {
bool ss = true;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("II-MCA"),
centerTitle: true,
actions: <Widget>[
IconButton(
icon: Icon(Icons.refresh),
onPressed: () {
setState(() {
check_year();
});
})
],
),
body: validation == "1"
? new Center(
child: Text(
"STUDENT DOSE NOT EXIST IN $year",
style: TextStyle(
color: Colors.grey,
fontWeight: FontWeight.bold,
fontSize: 20),
),
)
: ListView.builder(
itemCount: _students.length,
itemBuilder: (BuildContext context, int index) {
return new Container(
child: new Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Card(
child: new Container(
child: CheckboxListTile(
title: Text(
_students[index].name,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle:
Text("NAME : ${_students[index].name}"),
value: list_student_present2[index],
onChanged: (val) {
setState(() {
list_student_present2[index] = val;
if (!list_student_present2[index]) {
print(
'${_students[index].name}: i am absent');
}
if (list_student_present2[index]) {
print(
'${_students[index].name}: i am present');
}
});
},
),
padding: EdgeInsets.all(5),
),
)
],
),
),
);
}));
}
}
I really don't like this solution. What I suggest you is to create your PODOs (Plain Old Dart Object) representing your firebase models and deserialise them from your firebase snapshots.
I've recently started programming using dart and flutter and everything has been going smoothly for my app, although recently i wanted to add drop down menu to provide the user with multiple options to pick from. everything worked as planned however when i pick a value from the list it doesn't change the value in the box, it goes back to the hint or an empty box. any help would be appreciated!
here is my code for the dropdownbutton:
Widget buildDropdownButton() {
String newValue;
return new Padding(
padding: const EdgeInsets.all(24.0),
child: new Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
new ListTile(
title: const Text('Frosting'),
trailing: new DropdownButton<String>(
hint: Text('Choose'),
onChanged: (String changedValue) {
newValue=changedValue;
setState(() {
newValue;
print(newValue);
});
},
value: newValue,
items: <String>['None', 'Chocolate', 'Vanilla', 'ButterCream']
.map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: new Text(value),
);
}).toList()),
),
],
),
);
}
The error is because you are declaring a method variable newValue you must declare that variable as global inside your StatefulWidget.
String newValue;
Widget buildDropdownButton() {
return new Padding(
padding: const EdgeInsets.all(24.0),
child: new Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
new ListTile(
title: const Text('Frosting'),
trailing: new DropdownButton<String>(
hint: Text('Choose'),
onChanged: (String changedValue) {
newValue=changedValue;
setState(() {
newValue;
print(newValue);
});
},
value: newValue,
items: <String>['None', 'Chocolate', 'Vanilla', 'ButterCream']
.map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: new Text(value),
);
}).toList()),
),
],
),
);
}
Faced same issue and none of the answers worked. Then, I found the solution in one of my old projects.
I was using it in a AlertDialog here.
So, Change DropdownButton to DropdownButtonFormField
and add onSaved exactly as onChanged:
onSaved: (value) {
setState(() {
_selectedValue = value;
});
}
That's it. It will start working.
I had this problem although I was already using the solution above.
for anyone who has this problem and the above solution does not work, try separating FutureBuilder from the dropdown. this is how your final code should look like:
class TheFuture extends StatelessWidget {
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: myFuture(),
builder: (ctx, snp) {
if (!snp.hasData) return LoadingLine();
return TheBuilder(snp.data);
},
);
}
}
class TheBuilder extends StatefulWidget {
const TheBuilder(this.mp);
final Map<String, dynamic> mp;
#override
_MessageUSScreenFilterBodyState createState() =>
_MessageUSScreenFilterBodyState();
}
class _MessageUSScreenFilterBodyState extends State<MessageUSScreenFilterBody> {
int _selectedId;
#override
Widget build(BuildContext context) {
return DropdownButton<int>(
selectedItemBuilder: (context) =>
widget.mp['myData'].map((e) => Text(e.type)).toList(),
items: widget.mp['myData']
.map(
(e) => DropdownMenuItem(
child: Text(e.type),
value: e.id,
),
)
.toList(),
value: _selectedId,
onChanged: (int _id) {
setState(() {
_selectedId = _id;
});
},
);
}
}
wrap dropdown button with StatefulBuilder and initialise newValue outside build method.
StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: Text("Change Status"),
content: Container(
padding: EdgeInsets.symmetric(horizontal: 8.0),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey, width: 1),
borderRadius: BorderRadius.circular(5)),
child: DropdownButtonHideUnderline(
child: DropdownButton(
hint: Text('Choose'),
onChanged: (String changedValue) {
setState(() {
newValue = changedValue;
print(newValue);
});
},
value: newValue,
items: <String>[
'None',
'Chocolate',
'Vanilla',
'ButterCream'
].map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: new Text(value),
);
}).toList()),
),
),
),
));
I'm using floatingActionButton to increase TextForm Fields. i.e the fields increase by 1 once the button is tapped. The fields are actually increased on tap of the button but so confused on how to get values for each fields generated.
My problems:
When the user selects a value in the dropdown, all the values in the other generated dropdown fields changes to the new one. How do I solve this?
I'd like to add all the number value of the each of the generated Grade field together and also add the value of each of the generated Course Unit field together. i.e Add(sum) the value of all Grade fields the user generated.
See my full code below:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'My Grade Point',
theme: new ThemeData(primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _isLoading = false;
final formKey = new GlobalKey<FormState>();
final scaffoldKey = new GlobalKey<ScaffoldState>();
String _course;
String _grade;
String _unit;
String _mygp;
String _units;
String _totalgrade;
int counter = 1;
void _submit() {
final form = formKey.currentState;
if (form.validate()) {
setState(() => _totalgrade = _grade);
form.save();
}
}
Widget buildfields(int index) {
return new Column(
children: <Widget>[
new TextFormField(
onSaved: (String value) {
setState((){
_course = value;
});
},
validator: (val) {
return val.isEmpty
? "Enter Course Title $index"
: null;
},
decoration: new InputDecoration(labelText: "Course Title"),
),
new Row(
children: <Widget>[
new Expanded(
child: new TextFormField(
onSaved: (value) {
setState((){
_grade = value;
});
},
keyboardType: TextInputType.number,
decoration: new InputDecoration(labelText: "Grade"),
),
),
new Expanded(
child: new DropdownButton<String>(
onChanged: (String value) { setState((){
_unit = value;
});
},
hint: new Text('Course Unit'),
value: _unit,
items: <String>["1", "2", "3", "4", "5"].map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: new Text(value),
);
}).toList(),
),
),
],
),
],
);
}
#override
Widget build(BuildContext context) {
final Size screenSize = MediaQuery.of(context).size;
var loginBtn = new RaisedButton(
onPressed: _submit,
child: new Text("CALCULATE"),
color: Colors.primaries[0],
);
var showForm = new Container(
padding: new EdgeInsets.all(20.0),
child: new Column(
children: <Widget>[
new Expanded(child: new Form(
key: formKey,
child: new ListView.builder(
itemBuilder: (BuildContext context, int index) {
return buildfields(index); },
itemCount: counter,
scrollDirection: Axis.vertical,
),
),
),
_isLoading ? new CircularProgressIndicator() : loginBtn
],
),
);
return new Scaffold(
appBar: new AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: new Text(_totalgrade.toString()),
),
body: showForm,
floatingActionButton: new FloatingActionButton(
onPressed: () {
setState(() {
counter++;
});
},
child: new Icon(Icons.add),
),
);
}
}
When the user selects a value in the dropdown, all the values in the other generated dropdown fields changes to the new one. How do I solve this?
The reason why DropdownButton children in ListView updates synchronously is because it fetches all its value from the _unit variable. I suggest using a List<Object> to manage the data of ListView items.
i.e.
class Course {
var title;
var grade;
var unit;
}
...
List<Course> _listCourse = [];
I'd like to add all the number value of the each of the generated Grade field together and also add the value of each of the generated Course Unit field together. i.e Add(sum) the value of all Grade fields the user generated.
With ListView data being managed in List<Course>, data inputted in the fields can be set in onChanged()
// Course Grade
TextFormField(
onChanged: (String value) {
setState(() {
_listCourse[index].grade = value;
});
},
)
and the values can be summed up with the help of a foreach loop.
int sumGrade = 0;
_listCourse.forEach((course) {
// Add up all Course Grade
sumGrade += num.tryParse(course.grade);
});
Here's a complete working sample based from the snippet you've shared.
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'My Grade Point',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class Course {
var title;
var grade;
var unit;
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _isLoading = false;
final formKey = new GlobalKey<FormState>();
final scaffoldKey = new GlobalKey<ScaffoldState>();
String _course;
int _grade;
String _unit;
String _mygp;
String _units;
int _totalGrade;
int counter = 1;
List<Course> _listCourse = [];
#override
void initState() {
// Initialize empty List
_listCourse.add(Course());
super.initState();
}
void _submit() {
debugPrint('List Course Length: ${_listCourse.length}');
int sumGrade = 0;
_listCourse.forEach((course) {
debugPrint('Course Title: ${course.title}');
debugPrint('Course Grade: ${course.grade}');
// Add up all Course Grade
sumGrade += num.tryParse(course.grade);
debugPrint('Course Unit: ${course.unit}');
});
final form = formKey.currentState;
if (form.validate()) {
setState(() => _totalGrade = sumGrade);
form.save();
}
}
Widget buildField(int index) {
return new Column(
children: <Widget>[
new TextFormField(
onChanged: (String value) {
setState(() {
// _course = value;
_listCourse[index].title = value;
});
},
validator: (val) {
return val.isEmpty ? "Enter Course Title $index" : null;
},
decoration: new InputDecoration(labelText: "Course Title"),
),
new Row(
children: <Widget>[
new Expanded(
child: new TextFormField(
onChanged: (value) {
setState(() {
// _grade = value;
_listCourse[index].grade = value;
});
},
keyboardType: TextInputType.number,
decoration: new InputDecoration(labelText: "Grade"),
),
),
new Expanded(
child: new DropdownButton<String>(
onChanged: (String value) {
setState(() {
// _unit = value;
_listCourse[index].unit = value;
});
},
hint: new Text('Course Unit'),
value: _listCourse[index].unit,
items: <String>["1", "2", "3", "4", "5"].map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: new Text(value),
);
}).toList(),
),
),
],
),
],
);
}
#override
Widget build(BuildContext context) {
final Size screenSize = MediaQuery.of(context).size;
var loginBtn = new RaisedButton(
onPressed: _submit,
child: new Text("CALCULATE"),
color: Colors.primaries[0],
);
var showForm = new Container(
padding: new EdgeInsets.all(20.0),
child: new Column(
children: <Widget>[
new Expanded(
child: new Form(
key: formKey,
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return buildField(index);
},
itemCount: counter,
scrollDirection: Axis.vertical,
),
),
),
_isLoading ? new CircularProgressIndicator() : loginBtn
],
),
);
return new Scaffold(
appBar: new AppBar(
title: new Text(_totalGrade.toString()),
),
body: showForm,
floatingActionButton: new FloatingActionButton(
onPressed: () {
setState(() {
// Add an empty Course object on the List
_listCourse.add(Course());
counter++;
});
},
child: new Icon(Icons.add),
),
);
}
}
How do I open a popup menu from a second widget?
final button = new PopupMenuButton(
itemBuilder: (_) => <PopupMenuItem<String>>[
new PopupMenuItem<String>(
child: const Text('Doge'), value: 'Doge'),
new PopupMenuItem<String>(
child: const Text('Lion'), value: 'Lion'),
],
onSelected: _doSomething);
final tile = new ListTile(title: new Text('Doge or lion?'), trailing: button);
I want to open the button's menu by tapping on tile.
I think it would be better do it in this way, rather than showing a PopupMenuButton
void _showPopupMenu() async {
await showMenu(
context: context,
position: RelativeRect.fromLTRB(100, 100, 100, 100),
items: [
PopupMenuItem<String>(
child: const Text('Doge'), value: 'Doge'),
PopupMenuItem<String>(
child: const Text('Lion'), value: 'Lion'),
],
elevation: 8.0,
);
}
There will be times when you would want to display _showPopupMenu at the location where you pressed on the button
Use GestureDetector for that
final tile = new ListTile(
title: new Text('Doge or lion?'),
trailing: GestureDetector(
onTapDown: (TapDownDetails details) {
_showPopupMenu(details.globalPosition);
},
child: Container(child: Text("Press Me")),
),
);
and then _showPopupMenu will be like
_showPopupMenu(Offset offset) async {
double left = offset.dx;
double top = offset.dy;
await showMenu(
context: context,
position: RelativeRect.fromLTRB(left, top, 0, 0),
items: [
...,
elevation: 8.0,
);
}
This works, but is inelegant (and has the same display problem as Rainer's solution above:
class _MyHomePageState extends State<MyHomePage> {
final GlobalKey _menuKey = GlobalKey();
#override
Widget build(BuildContext context) {
final button = PopupMenuButton(
key: _menuKey,
itemBuilder: (_) => const<PopupMenuItem<String>>[
PopupMenuItem<String>(
child: Text('Doge'), value: 'Doge'),
PopupMenuItem<String>(
child: Text('Lion'), value: 'Lion'),
],
onSelected: (_) {});
final tile =
ListTile(title: Text('Doge or lion?'), trailing: button, onTap: () {
// This is a hack because _PopupMenuButtonState is private.
dynamic state = _menuKey.currentState;
state.showButtonMenu();
});
return Scaffold(
body: Center(
child: tile,
),
);
}
}
I suspect what you're actually asking for is something like what is tracked by https://github.com/flutter/flutter/issues/254 or https://github.com/flutter/flutter/issues/8277 -- the ability to associated a label with a control and have the label be clickable -- and is a missing feature from the Flutter framework.
Screenshot:
Full code:
class MyPage extends StatelessWidget {
final GlobalKey<PopupMenuButtonState<int>> _key = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
PopupMenuButton<int>(
key: _key,
itemBuilder: (context) {
return <PopupMenuEntry<int>>[
PopupMenuItem(child: Text('0'), value: 0),
PopupMenuItem(child: Text('1'), value: 1),
];
},
),
],
),
body: RaisedButton(
onPressed: () => _key.currentState.showButtonMenu(),
child: Text('Open/Close menu'),
),
);
}
}
I found a solution to your question. You can provide a child to PopupMenuButton which can be any Widget including a ListTile (see code below). Only problem is that the PopupMenu opens on the left side of the ListTile.
final popupMenu = new PopupMenuButton(
child: new ListTile(
title: new Text('Doge or lion?'),
trailing: const Icon(Icons.more_vert),
),
itemBuilder: (_) => <PopupMenuItem<String>>[
new PopupMenuItem<String>(
child: new Text('Doge'), value: 'Doge'),
new PopupMenuItem<String>(
child: new Text('Lion'), value: 'Lion'),
],
onSelected: _doSomething,
)
I don't think there is a way to achieve this behaviour. Although you can attach an onTap attribute to the tile, you can't access the MenuButton from the 'outside'
An approach you could take is to use ExpansionPanels because they look like ListTiles and are intended to allow easy modification and editing.
if you are using Material showMenu but you menu doesn't work properly or opens in wrong place follow my answer.
this answer is based on answer of Vishal Singh.
in GestureDetector
use onLongPressStart or onTapUp for sending offset to function.
onLongPressStart: (detail){
_showPopupMenu(detail.globalPosition);
},
onLongPress is equivalent to (and is called immediately after) onLongPressStart.
onTapUp, which is called at the same time (with onTap) but includes details regarding the pointer position.
and for menu position do some thing like below
position: RelativeRect.fromDirectional(textDirection: Directionality.of(context), start: left, top: top, end: left+2, bottom: top+2)
full code
_showPopupMenu(Offset offset) async {
double left = offset.dx;
double top = offset.dy;
await showMenu(
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(AppConst.borderRadiusSmall))),
position: RelativeRect.fromDirectional(textDirection: Directionality.of(context), start: left, top: top, end: left+2, bottom: top+2),
items: _getMenuItems(menu),
elevation: 8.0,
).then((value) {
value?.onTap.call();
});
}
class MyPage extends StatefulWidget {
#override
State<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
final GlobalKey<PopupMenuButtonState<int>> _key = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
PopupMenuButton<int>(
key: _key,
itemBuilder: (context) {
return <PopupMenuEntry<int>>[
PopupMenuItem(child: Text('0'), value: 0),
PopupMenuItem(child: Text('1'), value: 1),
];
},
),
],
),
body: RaisedButton(
onPressed: () => _key.currentState.showButtonMenu(),
child: Text('Open/Close menu'),
),
);
}
}