I am building a Flutter mobile application for taking attendance.
I am using CheckboxListTile for displaying enrollment list. I would like to call Alert Dialog when I click or tap a list item. However, I find that CheckboxListTile does not have onTap property.
So is there any way to add onTap to CheckboxListTile? Thanks!
A part of my code for creating CheckboxListTile:
Widget staffList(AsyncSnapshot<EnrollmentListModel> snapshot) {
if (snapshot.data.staffList.length > 0) {
return ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data.staffList.length,
itemBuilder: (context, index) {
return Card(
child: CheckboxListTile(
title: Text(
'${snapshot.data.staffList[index].listName}',
style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold),
),
value: snapshot.data.staffList[index].attendanceStatus == 'Y',
onChanged: (bool value) {},
activeColor: Colors.green,
),
);
},
);
}
else {
return Center(child: Text("No enrollment for this course."));
}
}
The reason onTap is not working is because the checkboxListTile itself is clickable and if we try to apply onTap on it again, it'll conflict the original click event. So instead of wrapping your checkboxListTile in GestureDetector, you can directly pass the call to open alertDialog in your onChanged method after you set the value upon clicking on it. Sample working code below:
return Scaffold(
appBar: new AppBar(title: new Text('CheckboxListTile demo')),
body: ListView(
children: values.keys.map((String key) {
return CheckboxListTile(
title: Text(key),
value: values[key],
onChanged: (bool value) {
setState(() {
values[key] = value;
});
_showDialog();
},
);
}).toList(),
),
);
}
void _showDialog() {
showDialog(
context: context,
builder: (BuildContext context) {
// return object of type Dialog
return AlertDialog(
title: new Text("Alert Dialog title"),
content: new Text("Alert Dialog body"),
actions: <Widget>[
// usually buttons at the bottom of the dialog
new FlatButton(
child: new Text("Close"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
});
The output is, if you tap on foo checkbox, it'll check the box and will open alertDialog. Hope this answers your question.
You can wrap the CheckboxListTile inside a GestureDetector, which has a onTap property along with many others which you might find useful.
More documentation about the GestureDetector class can be found here.
Example -
Widget staffList(AsyncSnapshot<EnrollmentListModel> snapshot) {
if (snapshot.data.staffList.length > 0) {
return ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data.staffList.length,
itemBuilder: (context, index) {
return Card(
child: GestureDetector(
onTap: () {
//Add code for showing AlertDialog here
},
child: CheckboxListTile(
title: Text(
'${snapshot.data.staffList[index].listName}',
style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold),
),
value: snapshot.data.staffList[index].attendanceStatus == 'Y',
onChanged: (bool value) {},
activeColor: Colors.green,
),
),
);
},
);
}
else {
return Center(child: Text("No enrollment for this course."));
}
}
Alternative -
You can also use the InkWell widget if you only need the onTap callback. Just replace the GestureDetector in above code with InkWell.
Documentation about InkWell can be found here.
body: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
child: Text(index.toString()),
onTap: () {//Write your AlertDialog code here},
);
},
itemCount: 10));
Related
Flutter reference documentation shows an example of using CupertinoPicker class wrapped by CupertinoModalPopupRoute popup Dialog:
https://api.flutter.dev/flutter/cupertino/CupertinoPicker-class.html (dartpad snippet available)
The Dialog with a CupertinoPicker pops up, once a button is clicked. Button's onPressed calls:
void _showDialog(Widget child) {
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) => Container(
// some UI setting omitted
child: SafeArea(
top: false,
child: child,
),
));
}
where child is:
CupertinoPicker(
// some UI setting omitted
onSelectedItemChanged: (int selectedItem) {
setState(() {
_selectedFruit = selectedItem;
});
},
children:
List<Widget>.generate(_fruitNames.length, (int index) {
return Center(
child: Text(
_fruitNames[index],
),
);
}),
),
Method showCupertinoModalPopup used to push a Dialog with a Picker just above the main page.
Method's annotation mentions that "Content below the widget is dimmed with a [ModalBarrier]", and exactly ModalBarrier's handleDismiss method is called once user taps outside the Dialog's area, this leads to Navigator.pop being called, which pops the Dialog returning default void value (I cannot find a way to pass custom callback to the ModalBarrier)
Again from the showCupertinoModalPopup's annotation:
"Returns a Future that resolves to the value that was passed to
[Navigator.pop] when the popup was closed."
So the question is, what is the right way (if any) to override Navigator.pop to make showCupertinoModalPopup return a value? I don't want to use Picker's onSelectedItemChanged as it constantly changes the state while user choses right value.
Instead I would use it like:
Future<void> _showDialog(Widget child) async {
int? value = await showCupertinoModalPopup<int>(
context: context,
builder: (BuildContext context) => Container(
//omitted
child: SafeArea(
top: false,
child: child,
),
));
setState(() {
Provider.of<FruitsController>(context, listen: false).currentFruit = value;
});
}
I would appreciate your help.
Yes you can create a button to pop from showCupertinoModalPopup.
Future<void> _showDialog() async {
int? value = await showCupertinoModalPopup<int>(
context: context,
builder: (BuildContext context) => Column(
children: [
ElevatedButton(
onPressed: () {
Navigator.of(context).pop(-2); //pasing value on pop is -2
},
child: Text("Close ")),
Container(
child: SafeArea(
top: false,
child: CupertinoPicker(
onSelectedItemChanged: (int selectedItem) {
_selectedFruit = _fruitNames[selectedItem];
},
itemExtent: 33,
children:
List<Widget>.generate(_fruitNames.length, (int index) {
return Center(
child: Text(
_fruitNames[index],
),
);
}),
),
),
),
],
),
);
print(value);
}
I have wrapped ListView.builder inside Visible widget, and the button for its visible property is in a ListTile widget with variable _currencyVisible.
The widget Visible works 2 times i.e. false/hidden(default), then changes to visible when clicked, and again hides on the second click, but it doesn't work after that. Printing on console _currencyVisible shows correct data.
Here's my code:
menuItems(BuildContext context) {
bool _currencyVisible = false;
return StatefulBuilder(
builder: (BuildContext context, void Function(void Function()) setState) {
return ListView(
children: [
ListTile(
title: FutureBuilder<dynamic>(
future: getUserCurrencySymbol(),
builder:(BuildContext context, AsyncSnapshot<dynamic> snapshot) {
return Text("Currency " + snapshot.data.toString());
}),
trailing: IconButton(
icon: Icon(Icons.refresh),
onPressed: () { setState(() { _currencyVisible = !_currencyVisible; }); },
),
),
Visibility(
visible: _currencyVisible,
child: ListView.builder(
shrinkWrap: true,
itemCount:
currency.allCurrenciesList.length,
itemBuilder: (context, index) {
for (Currency c in currency.allCurrenciesList) {
currency.allCurrenciesList.removeAt(0);
return Card(
child: ListTile(
title: Text(c.country),
subtitle: Text(c.shortCurrency),
trailing: Text(c.symbol),
onTap: () {
saveUserCurrency(c.country, context);
},
),
);
}
return Text("Not Null");
},
),
),
],
);
},
);
}
You are removing all of the data from your currency list. The widget is showing correctly, but there is no data to display.
Remove this line
currency.allCurrenciesList.removeAt(0);
Don't loop through the currencies in itemBuilder. Use index instead.
Visibility(
visible: _currencyVisible,
child: ListView.builder(
shrinkWrap: true,
itemCount: currency.allCurrenciesList.length,
itemBuilder: (context, index) {
final c = currency.allCurrenciesList[index];
return Card(
child: ListTile(
title: Text(.country),
subtitle: Text(c.shortCurrency),
trailing: Text(c.symbol),
onTap: () {
saveUserCurrency(c.country, context);
},
);
}
return Text("Not Null");
),
),
Is there any other way for me to align the text within the leading and title of a ListTile without using the Row widget? The data was fetched from Firebase and then being viewed using Listview. Builder.
FutureBuilder(
future: fetchData(),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<taskData> result = snapshot.data as List<taskData>;
return ListView.builder(
scrollDirection: Axis.vertical,
itemCount: result.length,
itemBuilder: (BuildContext context, int i) {
DateTime updateDate = DateTime.fromMillisecondsSinceEpoch(result[i].date);
String formDate = DateFormat('yMMMd').format(updateDate);
return GestureDetector(
onTap: () {
NavigationController(context, 'Edit', 'Title',
result[i].task, result[i].id, updateDate);
},
child: Card(
child: ListTile(
leading: Text(formDate, style: TextStyle(fontFamily: 'ProximaNova', fontSize: 16),),
title: Text('${result[i].task}',style: TextStyle(fontFamily: 'ProximaNova',fontSize: 18),),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () {
id = result[i].id;
showDialog<void>(
context: context, builder: (context) => dialog);
},
),
),
),
);
});
} else if (snapshot.hasError) {
print('${snapshot.error}');
return Text(
"",
style: TextStyle(fontSize: 20),
);
}
// By default, show a loading spinner.
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 100.0),
child: CircularProgressIndicator(),
));
},
),
Here is the picture for reference ('title' is not aligned with 'leading' and 'trailing'):
It is working for you
ListTile(
leading: Text('2021-04-27'),
title: Text('Lion', textAlign: TextAlign.center,),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () {},
),
),
Sorry if I'm not solving the problem asked but as a suggestion how about wrapping it in a Row Widget instead of a List Tile where you center align it?
first of all i believe you leading is not aligned with title and trailing, i do not know due to what reason this is happening from your code. solution might be Put your both text in title with + operator. or in leading whichever helps your problem get solved.
I have a stand-alone class called dialogs.dart which contains a reusable alert dialog. I want this alert dialog to have tabs, but I have no idea how to implement it if it's possible. Any ideas are highly appreciated.
Here's my code for dialogs.dart
import 'package:flutter/material.dart';
enum DialogAction{yes,abort}
class Dialogs{
static int selectedRadio;
static void setSelectedRadio(int val){
selectedRadio=val;
}
static Future<DialogAction> yesAbortDialog(
BuildContext context,
String title,
String body,
)async{
final action= await showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context){
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
title: Text(title),
content: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: List<Widget>.generate(3, (int index) {
return Radio<int>(
value: index,
groupValue: selectedRadio,
onChanged: (int value) {
setState(() => selectedRadio = value);
},
);
}),
);
},
),
actions: <Widget>[
FlatButton(
onPressed: ()=>Navigator.of(context).pop(DialogAction.abort),
child: const Text('cancel'),
),
RaisedButton(
onPressed: ()=>Navigator.of(context).pop(DialogAction.yes),
child: const Text('Proceed', style: TextStyle(color: Colors.white),),
color: Colors.green,
),
],
);
}
);
return(action!=null)?action: DialogAction.abort;
}
}
Don't use 'AlertDialog' for this purpose. Use 'Dialog' class. Inside it you can configure your own child the way you like. Tie tabs and events to it. Refer this - https://api.flutter.dev/flutter/material/Dialog-class.html
You can use 'container' with margin to get the barrier effect.
I'm looking through the Flutter Gallery for the code related to the CupertinoPicker.
Here is the relevant code extract:
child: new GestureDetector(
// Blocks taps from propagating to the modal sheet and popping.
// onTap: () { },
child: new SafeArea(
child: new CupertinoPicker(
scrollController: scrollController,
itemExtent: _kPickerItemHeight,
backgroundColor: CupertinoColors.white,
onSelectedItemChanged: (int index) {
setState(() {
print(_selectedItemIndex);
_selectedItemIndex = index;
});
},
children: new List<Widget>.generate(coolColorNames.length, (int index) {
return new Center(child:
new Text(coolColorNames[index]),
);
}),
),
),
),
Now, I need a callback / listener for when the CupertinoPicker closes, in other words, when the user has made his selection and his selection is final, I need to know what his final selection is.
The use case here is that I want to make an api callback based on his final selection when the bottomsheet closes.
At the moment, I can only get the values as the picker is spun by the user as there is only a callback for onSelectedItemChanged. See gif below.
I see the BottomSheet widget has an onClosing callback: https://docs.flutter.io/flutter/material/BottomSheet-class.html
But I'm confused as to how I can get an instance of it to use as the Flutter Gallery sample is calling the bottomsheet using the following code and there is no method to get the bottomsheet from the code:
new GestureDetector(
onTap: () async {
await showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return _buildBottomPicker();
},
);
},
child: _buildMenu(),
),
Does anyone know how I can get the callback listener for this?
EDIT - Based on Remi's solution, I have added the Navigator.of(context).pop(value) code within the onTap call back. However, the CupertinoPicker is not persistent so if the user touches outside the picker, the picker dismisses itself and a null value will be returned:
Widget _buildBottomPicker() {
final FixedExtentScrollController scrollController =
new FixedExtentScrollController(initialItem: _selectedItemIndex);
return new Container(
height: _kPickerSheetHeight,
color: CupertinoColors.white,
child: new DefaultTextStyle(
style: const TextStyle(
color: CupertinoColors.black,
fontSize: 22.0,
),
child: new GestureDetector(
// Blocks taps from propagating to the modal sheet and popping.
onTap: () { Navigator.of(context).pop(_selectedItemIndex);},
child: new SafeArea(
child: new CupertinoPicker(
scrollController: scrollController,
itemExtent: _kPickerItemHeight,
backgroundColor: CupertinoColors.white,
onSelectedItemChanged: (int index) {
setState(() {
// print(_selectedItemIndex);
// Navigator.of(context).pop(index);
_selectedItemIndex = index;
});
},
children: new List<Widget>.generate(coolColorNames.length, (int index) {
return new Center(child:
new Text(coolColorNames[index]),
);
}),
),
),
),
),
);
}
It's actually much simpler than what you thought.
showDialog and it's counterparts (including showModalBottomSheet) returns a Future that contains the result. So you can do the following :
final selectedId = await showModalBottomSheet<int>(...);
The only requirement is that when poping your modal/dialog/route/whatever, you do Navigator.of(context).pop(value) to send that value.
Like the previous answer said, the value needed is returned for you in the form of a Future. While that is true, it wasn't obvious to me how to implement that. In my case, I wanted to filter a todo list by a category list on the same screen. So by using the int key returned by onSelectedItemChanged, I was able to filter like this...
Widget _buildCategoryPicker(BuildContext context, _todoBloc) {
final FixedExtentScrollController scrollController =
FixedExtentScrollController(initialItem: _selectedIndex);
return GestureDetector(
onTap: () async {
await showCupertinoModalPopup<String>(
context: context,
builder: (BuildContext context) {
return _buildBottomPicker(
CupertinoPicker(
(...)
onSelectedItemChanged: (int index) {
setState(() => _selectedIndex = index);
},
children: (...),
), // CupertinoPicker
);
},
); // await showCupertinoModalPopup
// Filters the todoList.
_todoBloc.filter.add(categories[_selectedIndex]);
},
child: _buildMenu(
...
),
);
}
You do not need to set a final on your showCupertinoModalPopup or anything like this...
final selectedId = await showCupertinoModalPopup<String>(...);
like this:
future.whenComplete(() => requestRefresh());
showModalBottomSheet(
context: context,
builder: (_) {
return GestureDetector(
onTap: () => Navigator.of(context).pop(), // closing showModalBottomSheet
child: FlutterLogo(size: 200),
);
},
);