import 'package:flutter/material.dart';
class Demo {
int no;
String value;
Demo({this.value, this.no});
}
class Control {
TextEditingController controller;
FocusNode node;
Control({this.controller, this.node});
}
class DemoPage extends StatefulWidget {
static const routeName = '/Demo';
DemoPage({Key key}) : super(key: key);
#override
_DemoPageState createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
List<Demo> txtfield;
List<Control> control;
#override
void initState() {
txtfield = [];
control = [];
// no = 0;
add();
super.initState();
}
int no;
void add() {
no = (no ?? 0) + 1;
setState(() {});
txtfield.add(Demo(no: no));
control.add(Control(
controller: TextEditingController(),
node: FocusNode(),
));
// no = no +1;
}
#override
Widget build(BuildContext context) {
// print(txtfield[0].no);
// FocusScope.of(context).requestFocus(control[control.length - 1].node);
return Scaffold(
appBar: AppBar(),
body: Center(
child: Card(
child: Container(
child: Column(
children: txtfield
.map((f) => TextField(
controller: control[f.no - 1].controller,
focusNode: control[f.no - 1].node,
autofocus: true,
))
.toList(),
),
width: 400,
padding: EdgeInsets.all(20),
),
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
add();
print(no);
FocusScope.of(context).requestFocus(control[control.length - 1].node);
},
),
);
}
}
I used above code. but I can't able to focus on a newly added text field.
when I check for that newly added text field has focus, it shows true, but I can't able to write anything in that text field.
I don't know what is an error in that code.
I search for this solution for more than 4 days. but I can't able to find solution.
At the onPressed of your floatingActionButton change this line:
FocusScope.of(context).requestFocus(control[control.length - 1].node);
with this
control[control.length - 1].node.requestFocus();
Related
I have a list of dynamic forms where I need to add and remove form fields between two fields dynamically. I am able to add/remove form fields from the bottom of the list properly.
However, when I try to add a form field in between two form fields the data for the field does not update correctly.
How can I correctly add a field in between the two fields and populate the data correctly?
import 'package:flutter/material.dart';
class DynamicFormWidget extends StatefulWidget {
const DynamicFormWidget({Key? key}) : super(key: key);
#override
State<DynamicFormWidget> createState() => _DynamicFormWidgetState();
}
class _DynamicFormWidgetState extends State<DynamicFormWidget> {
List<String?> names = [null];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Dynamic Forms'),
),
body: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
itemBuilder: (builderContext, index) => Row(
children: [
Flexible(
child: TextFormField(
initialValue: names[index],
onChanged: (name) {
names[index] = name;
debugPrint(names.toString());
},
decoration: InputDecoration(
hintText: 'Enter your name',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8))),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: IconButton(
onPressed: () {
setState(() {
if(index + 1 == names.length){
names.add( null); debugPrint('Added: $names');
} else {
names.insert(index + 1, null); debugPrint('Added [${index+1}]: $names');
}
});
},
color: Colors.green,
iconSize: 32,
icon: const Icon(Icons.add_circle)),
),
Padding(
padding: const EdgeInsets.all(8),
child: IconButton(
onPressed: (index == 0&& names.length == 1)
? null
: () {
setState(() {
names.removeAt(index);
});
debugPrint('Removed [$index]: $names');
},
color: Colors.red,
iconSize: 32,
icon: const Icon(Icons.remove_circle)),
),
],
),
separatorBuilder: (separatorContext, index) => const SizedBox(
height: 16,
),
itemCount: names.length,
),
);
}
}
Basically the problem is that Flutter is confused about who is who in your TextFormField list.
To fix this issue simply add a key to your TextFormField, so that it can be uniquely identified by Flutter:
...
child: TextFormField(
initialValue: names[index],
key: UniqueKey(), // add this line
onChanged: (name) {
...
If you want to learn more about keys and its correct use take a look at this.
The widget AnimatedList solves this problem, it keep track of the widgets as a list would do and uses a build function so it is really easy to sync elements with another list. If you end up having a wide range of forms you can make use of the InheritedWidget to simplify the code.
In this sample i'm making use of the TextEditingController to abstract from the form code part and to initialize with value (the widget inherits from the ChangeNotifier so changing the value will update the text in the form widget), for simplicity it only adds (with the generic text) and removes at an index.
To make every CustomLineForm react the others (as in: disable remove if it only remains one) use a StreamBuilder or a ListModel to notify changes and make each entry evaluate if needs to update instead of rebuilding everything.
class App extends StatelessWidget {
final print_all = ChangeNotifier();
App({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: FormList(print_notifier: print_all),
floatingActionButton: IconButton(
onPressed: print_all.notifyListeners,
icon: Icon(Icons.checklist),
),
),
);
}
}
class FormList extends StatefulWidget {
final ChangeNotifier print_notifier;
FormList({required this.print_notifier, super.key});
#override
_FormList createState() => _FormList();
}
class _FormList extends State<FormList> {
final _controllers = <TextEditingController>[];
final _list_key = GlobalKey<AnimatedListState>();
void print_all() {
for (var controller in _controllers) print(controller.text);
}
#override
void initState() {
super.initState();
widget.print_notifier.addListener(print_all);
_controllers.add(TextEditingController(text: 'Inital entrie'));
}
#override
void dispose() {
widget.print_notifier.removeListener(print_all);
for (var controller in _controllers) controller.dispose();
super.dispose();
}
void _insert(int index) {
final int at = index.clamp(0, _controllers.length - 1);
_controllers.insert(at, TextEditingController(text: 'Insert at $at'));
// AnimatedList will take what is placed in [at] so the controller
// needs to exist before adding the widget
_list_key.currentState!.insertItem(at);
}
void _remove(int index) {
final int at = index.clamp(0, _controllers.length - 1);
// The widget is replacing the original, it is used to animate the
// disposal of the widget, ex: size.y -= delta * amount
_list_key.currentState!.removeItem(at, (_, __) => Container());
_controllers[at].dispose();
_controllers.removeAt(at);
}
#override
Widget build(BuildContext context) {
return AnimatedList(
key: _list_key,
initialItemCount: _controllers.length,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
itemBuilder: (ctx, index, _) {
return CustomLineForm(
index: index,
controler: _controllers[index],
on_insert: _insert,
on_remove: _remove,
);
},
);
}
}
class CustomLineForm extends StatelessWidget {
final int index;
final void Function(int) on_insert;
final void Function(int) on_remove;
final TextEditingController controler;
const CustomLineForm({
super.key,
required this.index,
required this.controler,
required this.on_insert,
required this.on_remove,
});
#override
Widget build(BuildContext context) {
return Row(
children: [
Flexible(
child: TextFormField(
controller: controler,
),
),
IconButton(
icon: Icon(Icons.add_circle),
onPressed: () => on_insert(index),
),
IconButton(
icon: Icon(Icons.remove_circle),
onPressed: () => on_remove(index),
)
],
);
}
}
I am trying to create a view where the user can select the agenda view he wants (day, week, month...).
In my appBar, I have created an action icon where the user can select the agenda view he wants.
When I change the view, the set state does not refresh the agenda view. I do not find what I am missing.
If you could help, it will be appreciated. Thank you.
import 'package:syncfusion_flutter_calendar/calendar.dart';
import 'package:provider/provider.dart';
class CalendarWidget extends StatefulWidget {
CalendarView viewCalendar;
CalendarController _controller = CalendarController();
CalendarWidget(this.viewCalendar,this._controller, {Key? key}) : super(key: key);
#override
State<CalendarWidget> createState() => _CalendarWidgetState(viewCalendar,_controller);
}
class _CalendarWidgetState extends State<CalendarWidget> {
CalendarView viewCalendar;
CalendarController _controller;
#override
_CalendarWidgetState(this.viewCalendar,this._controller);
#override
Widget build(BuildContext context) {
return Column(
children: [
myCalendar(context,viewCalendar,_controller),
const SizedBox(height: 8.0),
],
);
}
}
Widget myCalendar (BuildContext context, view,_controler ) {
final events = Provider.of<EventProvider>(context).events;
final CalendarController _calendarControler = CalendarController();
_calendarControler.view = view;
return SfCalendar(
view: CalendarView.month,
// timeSlotViewSettings:
// const TimeSlotViewSettings(allDayPanelColor: Colors.green),
controller: _controler,
_controler.view = view,
//_controller.view = CalendarView.week,
showNavigationArrow: true,
showWeekNumber: true,
showDatePickerButton: true,
showCurrentTimeIndicator: true,
initialSelectedDate: DateTime.now(),
firstDayOfWeek: 1,
dataSource: EventDataSource(events),
onSelectionChanged: (details) {
final provider = Provider.of<EventProvider>(context, listen: false);
provider.setDate(details.date!);
},
onTap: (details) {
final provider = Provider.of<EventProvider>(context, listen: false);
if (provider.selectedDate == details.date) {
showModalBottomSheet(
context: context,
builder: (context) => const TasksWidget(),
);
}
},
onLongPress: (details) {
final provider = Provider.of<EventProvider>(context, listen: false);
provider.setDate(details.date!);
showModalBottomSheet(
context: context,
builder: (context) => const TasksWidget(),
);
},
);
}
class AgendaOrganize extends StatefulWidget {
const AgendaOrganize ({Key? key}) : super(key : key);
#override
_AgendaOrganizeState createState() => _AgendaOrganizeState();
}
class _AgendaOrganizeState extends State<AgendaOrganize> {
CalendarView viewCalendar = CalendarView.month;
final CalendarController _controller = CalendarController();
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: const MyMenu(),
appBar: AppBar(
title: const Center(
child: Text('Agenda')),
actions: <Widget>[
PopupMenuButton
(icon: const Icon(Icons.more_vert_outlined),
itemBuilder: (BuildContext context) {
return Menus.choice.map((String choice){
return PopupMenuItem(
value: choice,
child: Text(choice));
}).toList();
},
onSelected:
choiceMade,
),
IconButton(
icon: const Icon(
Icons.add_circle_outline,
color: Colors.white,
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const EventEditingPage()));
},
),
],),
body: CalendarWidget(viewCalendar),
//TODO //PROBLEME - SI J'AFFICHE PERSISTENTBOTTOMNAVBAR, affiche agenda FOR TODAY
// bottomNavigationBar: PersistentBottomNavBar(),
);
throw UnimplementedError();
}
#override
void setState(VoidCallback fn) {
viewCalendar = CalendarView.month;
super.setState(fn);
}
void choiceMade(String value) {
print(value);
setState(() {
viewCalendar = CalendarView.month;
});
}
}
class Menus {
static const List<String> choice = <String> [
'Day', 'Week', 'Work Week', 'Month','Schedule', 'Timeline Day', 'Timeline Week', 'Timeline Work Week'
];
}
Just add a key to the SfCalendar and it's going to change on every setState. Do it like the following:
Widget myCalendar(BuildContext context, CalendarView view) {
final events = Provider.of<EventProvider>(context).events;
final CalendarController _calendarControler = CalendarController();
return SfCalendar(
key: ValueKey(view), // <- Here
view: view,
...
Also, the CalendarWidget is passing the state further down to the _CalendarWidgetState itself. The _CalendarWidgetState should use widget.viewCalendar instead.
class CalendarWidget extends StatefulWidget {
CalendarView viewCalendar;
CalendarWidget(this.viewCalendar, {Key? key}) : super(key: key);
#override
State<CalendarWidget> createState() => _CalendarWidgetState();
}
class _CalendarWidgetState extends State<CalendarWidget> {
#override
Widget build(BuildContext context) {
return Column(
children: [
myCalendar(context, widget.viewCalendar),
const SizedBox(height: 8.0),
],
);
}
}
And here every choice possible:
void choiceMade(String value) {
setState(() {
switch (value) {
case 'Day':
viewCalendar = CalendarView.day;
break;
case 'Week':
viewCalendar = CalendarView.week;
break;
case 'Work Week':
viewCalendar = CalendarView.workWeek;
break;
case 'Month':
viewCalendar = CalendarView.month;
break;
case 'Schedule':
viewCalendar = CalendarView.schedule;
break;
case 'Timeline Day':
viewCalendar = CalendarView.timelineDay;
break;
case 'Timeline Week':
viewCalendar = CalendarView.timelineWeek;
break;
case 'Timeline Work Week':
viewCalendar = CalendarView.timelineWorkWeek;
break;
}
});
}
Based on the provided information, we have checked the mentioned issue “Calendar view not updating using Setstate”. View property of the SfCalendar is used to set the initial view of the calendar. For dynamic view changes, we have implemented the view property on the CalendarController. Kindly use that property from the controller for dynamic view changes. Please find the code snippet for dynamic view switching.
#override
void initState() {
_controller = CalendarController();
super.initState();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: SafeArea(
child: Column(
children: [
Container(
height: 550,
child: SfCalendar(
view: CalendarView.day,
controller: _controller,
),
),
RaisedButton(
onPressed: () => _controller.view = CalendarView.week,
child: Text('Change the view'),
),
],
),
),
)));
} ```
Also please find the breaking changes from the following changelog link.
Changelog link: https://pub.dev/packages/syncfusion_flutter_calendar/changelog
Also, you can use the allowed views property of the calendar for view navigation. Please find the UG from the following link.
UG link: https://help.syncfusion.com/flutter/calendar/date-navigations#allowed-views
We hope that this helps you. Please let us know if you need further assistance.
I made question yesterday but is unclear. my unclear question :(
I have made changes for my question here. I got some trouble getting the value from Rx object. When the first time page load, it does perfectly listen the value change, but when I add some category amount inside DateClass, the getX not working. But it working after I changing the first amount from textfield then the second textfield is listen the value change. Did I miss something?
here my code and I attach some screenshot below
First page, I adding the value then total is listening
then I add new category to insert new amount, the total not listening
I made change to first textfield value, total is listening
after I change the first value, total is listening to second textfield value
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:collection/collection.dart';
class MainClass extends StatefulWidget {
Function callback;
MainClass({Key key, this.callback}) : super(key: key);
#override
MainClassState createState() => MainClassState();
}
class MainClassState extends State<MainClass> {
RxList<DateClass> dynamicPerDate = <DateClass>[].obs;
GlobalKey<MainClassState> mKey = GlobalKey();
String stringTotal = "";
addPerDate() async {
dynamicPerDate.add(DateClass());
setState(() {});
}
calculate() {
int total = 0;
List<int> listAmount = [];
for (int i = 0; i < dynamicPerDate.value.length; i++) {
for (int j = 0; dynamicPerDate.value[i].listCategory.length > j; j++) {
var a = dynamicPerDate[i].listCategory[j].amountCategory;
print(a);
if (a != null) {
listAmount.add(a.value);
}
total = listAmount.sum;
}
}
return total.toString();
}
#override
void initState() {
// TODO: implement initState
super.initState();
addPerDate();
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: mKey,
body: Container(
child: ListView(
children: [
ListView.builder(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: dynamicPerDate.length,
itemBuilder: (_, index) => dynamicPerDate.isNotEmpty
? dynamicPerDate[index]
: Container()),
FlatButton(
onPressed: () {
addPerDate();
// calculate2();
},
child: Text('Add Date')),
SizedBox(height: 20),
Text('Total All Amount From Category Class'),
Obx(() => Text(calculate())),
],
),
),
);
}
}
class DateClass extends StatefulWidget {
RxList<CategoryClass> listCategory = <CategoryClass>[].obs;
DateClass({Key key, this.listCategory}) : super(key: key);
#override
_DateClassState createState() => _DateClassState();
}
class _DateClassState extends State<DateClass> {
addCategoryPerDate() {
widget.listCategory.add(CategoryClass());
setState(() {});
}
#override
void initState() {
// TODO: implement initState
super.initState();
widget.listCategory = <CategoryClass>[].obs;
addCategoryPerDate();
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text('Date', style: TextStyle(color: Colors.blue)),
ListView.builder(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: widget.listCategory.length,
itemBuilder: (_, index) => widget.listCategory.isNotEmpty
? widget.listCategory[index]
: Container()),
FlatButton(
onPressed: () {
addCategoryPerDate();
},
child: Text('Add Category'))
],
),
),
);
}
}
class CategoryClass extends StatefulWidget {
RxInt amountCategory = 0.obs;
TextEditingController amountController;
CategoryClass({Key key, this.amountCategory}) : super(key: key);
#override
_CategoryClassState createState() => _CategoryClassState();
}
class _CategoryClassState extends State<CategoryClass> {
MainClass mainClass = MainClass();
#override
void initState() {
// TODO: implement initState
super.initState();
widget.amountCategory = 0.obs;
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text('Amount'),
TextFormField(
controller: widget.amountController,
onChanged: (value) {
setState(() {
widget.amountCategory.value = int.parse(value);
widget.amountCategory.obs;
// MainClass().method();
});
},
keyboardType: TextInputType.number,
)
],
);
}
}
With my current code the TextField becomes focused, but the cursor and keyboard aren't triggered (requires a second tap). I believe this is because the TextField doesn't exist when the focus node is initially focused, but I'm struggling to find a solution.
Here is a simple recreation of the problem based on a Cookbook recipe:
class MyCustomForm extends StatefulWidget {
#override
_MyCustomFormState createState() => _MyCustomFormState();
}
class _MyCustomFormState extends State<MyCustomForm> {
FocusNode myFocusNode;
bool _editingField2 = false;
#override
void initState() {
super.initState();
myFocusNode = FocusNode();
myFocusNode.addListener(_focusListener);
}
#override
void dispose() {
myFocusNode.dispose();
super.dispose();
}
// Set _editingField2 to true when focusNode has focus.
_focusListener() {
if (myFocusNode.hasFocus) {
setState(() {
_editingField2 = true;
});
} else {
setState(() {
_editingField2 = false;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Text Field Focus'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// The first text field is focused on as soon as the app starts.
TextField(
autofocus: true,
),
// The second text field is created when _editingField2 (after FAB press).
_editingField2
? TextField(
focusNode: myFocusNode,
)
: Text('ayy'),
],
),
),
floatingActionButton: FloatingActionButton(
// Give focus node focus on FAB press.
onPressed: () => FocusScope.of(context).requestFocus(myFocusNode),
tooltip: 'Focus Second Text Field',
child: Icon(Icons.edit),
),
);
}
}
Here is my code, with important bits commented.
class TaskListItem extends StatefulWidget {
final Task task;
TaskListItem({#required this.task});
#override
State createState() => _TaskListItemState();
}
class _TaskListItemState extends State<TaskListItem> {
bool _isEditing;
FocusNode _focusNode;
final TextEditingController _textEditingController = TextEditingController();
#override
initState() {
super.initState();
_isEditing = false;
_textEditingController.text = widget.task.text;
_textEditingController.addListener(_handleTextFieldUpdate);
_focusNode = FocusNode(debugLabel: 'TaskListItem');
_focusNode.addListener(_handleFocusChange);
}
#override
void dispose() {
_focusNode.removeListener(_handleFocusChange);
_focusNode.dispose();
_textEditingController.dispose();
super.dispose();
}
_handleTextFieldUpdate() {
Provider.of<TaskListModel>(context, listen: false)
.updateTaskText(widget.task, _textEditingController.text);
}
// Update state to determine if Text or TextField widget is created in build().
_handleFocusChange() {
if (_focusNode.hasFocus) {
setState(() {
_isEditing = true;
});
} else {
setState(() {
_isEditing = false;
});
}
}
Widget _buildTitle() {
return Row(
children: <Widget>[
Expanded(
// Create either TextField or Text based on _isEditing value.
child: _isEditing && !widget.task.isComplete
? TextField(
focusNode: _focusNode,
controller: _textEditingController,
)
: Text(
widget.task.text,
style: widget.task.isComplete
? TextStyle(decoration: TextDecoration.lineThrough)
: null,
),
),
],
);
}
#override
Widget build(BuildContext context) {
return ListTile(
leading: Checkbox(
value: widget.task.isComplete,
//Dismiss focus when box is checked
onChanged: (bool checked) {
_focusNode.unfocus();
Provider.of<TaskListModel>(context, listen: false)
.toggleComplete(widget.task);
},
),
title: _buildTitle(),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => Provider.of<TaskListModel>(context, listen: false)
.deleteTask(widget.task),
),
onTap: () {
// I'm requesting focus here, but the Textfield doesn't exist yet?
FocusScope.of(context).requestFocus(_focusNode);
print('tapped');
},
);
}
}
What you have to do is change focus inside build, you're trying to change focus before the screen has done rebuilding that widget already. try this please, using your own code.
I'm not sure if you needed to really listen to that focus change or if you only wanted to accomplish the focus change after enabling the widget, if you do want to listen for the focus change let me know.
class MyCustomForm extends StatefulWidget {
#override
_MyCustomFormState createState() => _MyCustomFormState();
}
class _MyCustomFormState extends State<MyCustomForm> {
FocusNode myFocusNode = FocusNode();
bool _editingField2 = false;
#override
void dispose() {
myFocusNode?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
//here you do the focus request
if (_editingField2) {
FocusScope.of(context).requestFocus(myFocusNode);
}
return Scaffold(
appBar: AppBar(
title: Text('Text Field Focus'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// The first text field is focused on as soon as the app starts.
TextField(
autofocus: true,
),
// The second text field is created when _editingField2 (after FAB press).
_editingField2
? TextField(
focusNode: myFocusNode,
)
: Text('ayy'),
],
),
),
floatingActionButton: FloatingActionButton(
// Give focus node focus on FAB press.
onPressed: () {
setState(() {
_editingField2 = true;
});
},
tooltip: 'Focus Second Text Field',
child: Icon(Icons.edit),
),
);
}
}
I have a GestureDetector that need to launch a url. But if the gesture gets multiple taps, then launch is called multiple times.
In the current code im trying to use a state _isButtonTapped to control the tap. But the .whenComplete is somehow call before the launch is preformed?
_isButtonTapped = false
Widget _buildButton(String key, Text title, String url) {
_onTapped() async {
if (await canLaunch(url)) {
launch(url).whenComplete(
() => setState(() {
_isButtonTapped = false;
}),
);
}
}
return GestureDetector(
onTap: () {
_isButtonTapped ? null : _onTapped();
setState(() {
_isButtonTapped = true;
});
},
child: Container(
child: Padding(
padding: EdgeInsets.all(6.0),
child: Center(child: title),
),
),
);
}
Try this:
class _HomePageState extends State<HomePage> {
bool _isButtonTapped = false;
String _url = "https://google.ca";
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
color: Colors.blue,
width: 100,
height: 100,
child: GestureDetector(
onTap: () async {
if (!_isButtonTapped) { // only allow click if it is false
_isButtonTapped = true; // make it true when clicked
if (await canLaunch(_url)) {
await launch(_url);
_isButtonTapped = false; // once url is launched successfully, we again make it false, allowing tapping again
}
}
},
),
),
),
);
}
}
Try this? It should solve your problem.
class SafeOnTap extends StatefulWidget {
SafeOnTap({
Key? key,
required this.child,
required this.onSafeTap,
this.intervalMs = 500,
}) : super(key: key);
final Widget child;
final GestureTapCallback onSafeTap;
final int intervalMs;
#override
_SafeOnTapState createState() => _SafeOnTapState();
}
class _SafeOnTapState extends State<SafeOnTap> {
int lastTimeClicked = 0;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
final now = DateTime.now().millisecondsSinceEpoch;
if (now - lastTimeClicked < widget.intervalMs) {
return;
}
lastTimeClicked = now;
widget.onSafeTap();
},
child: widget.child,
);
}
}
You can wrap any kind of widget if you want.
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: Column(
children: [
// every click need to wait for 500ms
SafeOnTap(
onSafeTap: () => log('500ms'),
child: Container(
width: double.infinity,
height: 200,
child: Center(child: Text('500ms click me')),
),
),
// every click need to wait for 2000ms
SafeOnTap(
intervalMs: 2000,
onSafeTap: () => log('2000ms'),
child: Container(
width: double.infinity,
height: 200,
child: Center(child: Text('2000ms click me')),
),
),
],
),
),
),
);
}
}
the easiest way is in inkWell widget put doubleTap: () {},
it will do nothing, when user click multiple time
You have a bug in your code.
You are setting _isButtonTapped to true everytime you press it.
Correct you onTap function:
return GestureDetector(
onTap: () {
if (_isButtonTapped == false){
_onTapped();
setState(() {
_isButtonTapped = true;
});
},
}
//...
Regarding why the whenComplete is not beign called when you expected, that's another problem. I never used it but tacking a quick look into the docs (https://api.flutter.dev/flutter/scheduler/TickerFuture/whenComplete.html) show us that are multiple ways of achiving this, including wraping the function in an try block and use thr finally cloused as the whenCompleted. You should take a look at he docs and tried it out. Can't help more with that detail.
Hope it helps you.